【SSM 框架 | day27 spring MVC 和 SSM 整合】
SSM + Spring MVC 完整整合实战指南
一、SSM 架构核心认知
1.1 整体架构图
text
Client (浏览器/App) ↓ Spring MVC (Web层) ↓ Controller → 接收请求、参数校验、响应返回 ↓ Interceptor → 拦截器(认证、日志、限流) ↓ Resolver → 视图解析、异常处理 ↓ Spring Framework (业务层) ↓ Service → 业务逻辑、事务管理 ↓ AOP → 切面编程(日志、性能监控) ↓ MyBatis (数据层) ↓ Mapper → 数据访问、SQL执行 ↓ Database (MySQL/Oracle)
1.2 各层职责划分
| 层级 | 组件 | 职责 | 核心注解 |
|---|---|---|---|
| Web层 | Controller | 请求处理、参数校验 | @Controller, @RestController |
| 业务层 | Service | 业务逻辑、事务管理 | @Service, @Transactional |
| 数据层 | Mapper | 数据访问、SQL映射 | @Mapper, @Repository |
| 实体层 | Entity | 数据模型、DTO | 无注解,纯POJO |
二、完整项目结构
text
src/main/java ├── com.example │ ├── config │ │ ├── SpringConfig.java # Spring核心配置 │ │ ├── SpringMvcConfig.java # Spring MVC配置 │ │ ├── MyBatisConfig.java # MyBatis配置 │ │ └── DataSourceConfig.java # 数据源配置 │ ├── controller │ │ ├── UserController.java │ │ └── ProductController.java │ ├── service │ │ ├── UserService.java │ │ └── impl/UserServiceImpl.java │ ├── mapper │ │ └── UserMapper.java │ ├── entity │ │ └── User.java │ ├── dto │ │ └── UserDTO.java │ ├── common │ │ ├── Result.java # 统一响应 │ │ ├── BaseException.java # 基础异常 │ │ └── interceptor/ # 拦截器 │ └── Application.java # 启动类 └── resources ├── application.yml # 主配置 ├── mapper/ # MyBatis映射文件 └── static/ # 静态资源
三、核心配置整合
3.1 主启动类
java
@SpringBootApplication @MapperScan("com.example.mapper") // MyBatis接口扫描 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
3.2 Spring + Spring MVC 配置分离
java
// Spring配置类 - 业务层组件 @Configuration @ComponentScan( value = "com.example", excludeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class), @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = RestController.class) } ) @EnableTransactionManagement // 开启事务 @EnableAspectJAutoProxy(exposeProxy = true) // 开启AOP,暴露代理 public class SpringConfig { // 数据源、事务管理器等在DataSourceConfig中配置 }
java
// Spring MVC配置类 - Web层组件 @Configuration @EnableWebMvc @ComponentScan( value = "com.example.controller", useDefaultFilters = false, includeFilters = @ComponentScan.Filter( type = FilterType.ANNOTATION, classes = {Controller.class, RestController.class} ) ) public class SpringMvcConfig implements WebMvcConfigurer { // 视图解析器 @Bean public ViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); return resolver; } // 静态资源处理 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/"); } // 消息转换器 @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // Jackson JSON转换器 MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(); jacksonConverter.setObjectMapper(objectMapper()); converters.add(jacksonConverter); // 字符串转换器 StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8); converters.add(stringConverter); } @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); // 忽略未知属性 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 日期格式 mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); // 空值不序列化 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper; } // 拦截器配置 @Override public void addInterceptors(InterceptorRegistry registry) { // 日志拦截器 registry.addInterceptor(logInterceptor()) .addPathPatterns("/**") .order(1); // 认证拦截器 registry.addInterceptor(authInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/**") .order(2); } @Bean public LogInterceptor logInterceptor() { return new LogInterceptor(); } @Bean public AuthInterceptor authInterceptor() { return new AuthInterceptor(); } }
3.3 MyBatis 配置
java
@Configuration @EnableTransactionManagement public class MyBatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); // 类型别名扫描 factory.setTypeAliasesPackage("com.example.entity"); // MyBatis配置 org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); // 下划线转驼峰 configuration.setCacheEnabled(true); // 开启缓存 configuration.setLazyLoadingEnabled(false); // 关闭延迟加载 factory.setConfiguration(configuration); return factory; } @Bean public MapperScannerConfigurer mapperScannerConfigurer() { MapperScannerConfigurer scanner = new MapperScannerConfigurer(); scanner.setBasePackage("com.example.mapper"); scanner.setSqlSessionFactoryBeanName("sqlSessionFactory"); return scanner; } }
3.4 数据源配置
java
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
四、各层代码实现
4.1 实体层 (Entity)
java
@Data @NoArgsConstructor @AllArgsConstructor public class User { private Integer id; private String username; private String email; private String password; private Integer age; private LocalDateTime createTime; private LocalDateTime updateTime; // 业务方法 public boolean isValid() { return username != null && !username.trim().isEmpty() && email != null && email.contains("@"); } }
4.2 数据层 (Mapper)
java
@Mapper @Repository public interface UserMapper { // 查询方法 User selectById(Integer id); List<User> selectAll(); User selectByUsername(String username); // 插入方法 int insert(User user); // 更新方法 int update(User user); // 删除方法 int deleteById(Integer id); // 复杂查询 List<User> selectByCondition(@Param("username") String username, @Param("email") String email); // 分页查询 List<User> selectByPage(@Param("offset") Integer offset, @Param("limit") Integer limit); }
对应的Mapper XML:
xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mapper.UserMapper"> <resultMap type="User"> <id column="id" property="id" /> <result column="username" property="username" /> <result column="email" property="email" /> <result column="password" property="password" /> <result column="age" property="age" /> <result column="create_time" property="createTime" /> <result column="update_time" property="updateTime" /> </resultMap> <select resultMap="BaseResultMap"> SELECT * FROM user WHERE id = #{id} </select> <insert useGeneratedKeys="true" keyProperty="id"> INSERT INTO user (username, email, password, age, create_time) VALUES (#{username}, #{email}, #{password}, #{age}, NOW()) </insert> <select resultMap="BaseResultMap"> SELECT * FROM user <where> <if test="username != null and username != ''"> AND username LIKE CONCAT('%', #{username}, '%') </if> <if test="email != null and email != ''"> AND email = #{email} </if> </where> </select> </mapper>
4.3 业务层 (Service)
java
public interface UserService { User getUserById(Integer id); List<User> getAllUsers(); Result<User> createUser(User user); Result<User> updateUser(User user); Result<Void> deleteUser(Integer id); PageResult<User> getUsersByPage(Integer page, Integer size); }
java
@Service @Transactional @Slf4j public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override @Transactional(readOnly = true) public User getUserById(Integer id) { log.info("查询用户,ID: {}", id); return userMapper.selectById(id); } @Override @Transactional(readOnly = true) public List<User> getAllUsers() { return userMapper.selectAll(); } @Override public Result<User> createUser(User user) { // 参数校验 if (user == null || !user.isValid()) { throw new BusinessException(400, "用户信息不完整"); } // 检查用户名是否已存在 User existingUser = userMapper.selectByUsername(user.getUsername()); if (existingUser != null) { throw new BusinessException(400, "用户名已存在"); } // 保存用户 int result = userMapper.insert(user); if (result > 0) { log.info("创建用户成功: {}", user.getUsername()); return Result.success(user); } else { throw new SystemException("创建用户失败"); } } @Override public Result<User> updateUser(User user) { if (user.getId() == null) { throw new BusinessException(400, "用户ID不能为空"); } // 检查用户是否存在 User existingUser = userMapper.selectById(user.getId()); if (existingUser == null) { throw new BusinessException(404, "用户不存在"); } user.setUpdateTime(LocalDateTime.now()); int result = userMapper.update(user); if (result > 0) { return Result.success(user); } else { throw new SystemException("更新用户失败"); } } @Override public Result<Void> deleteUser(Integer id) { User existingUser = userMapper.selectById(id); if (existingUser == null) { throw new BusinessException(404, "用户不存在"); } int result = userMapper.deleteById(id); if (result > 0) { log.info("删除用户成功: {}", id); return Result.success(); } else { throw new SystemException("删除用户失败"); } } @Override @Transactional(readOnly = true) public PageResult<User> getUsersByPage(Integer page, Integer size) { if (page == null || page < 1) page = 1; if (size == null || size < 1) size = 10; Integer offset = (page - 1) * size; List<User> users = userMapper.selectByPage(offset, size); // 获取总数(实际项目中应该有专门的count查询) List<User> allUsers = userMapper.selectAll(); Integer total = allUsers.size(); Integer pages = (int) Math.ceil((double) total / size); return new PageResult<>(users, total, pages, page, size); } }
4.4 Web层 (Controller)
java
@RestController @RequestMapping("/api/users") @Validated @Slf4j public class UserController { @Autowired private UserService userService; // GET /api/users/1 @GetMapping("/{id}") public Result<User> getUser(@PathVariable @Min(1) Integer id) { log.info("查询用户详情: {}", id); User user = userService.getUserById(id); return Result.success(user); } // GET /api/users?page=1&size=10 @GetMapping public Result<PageResult<User>> getUsers( @RequestParam(defaultValue = "1") @Min(1) Integer page, @RequestParam(defaultValue = "10") @Min(1) @Max(100) Integer size) { PageResult<User> result = userService.getUsersByPage(page, size); return Result.success(result); } // POST /api/users @PostMapping public Result<User> createUser(@Valid @RequestBody User user) { log.info("创建用户: {}", user.getUsername()); return userService.createUser(user); } // PUT /api/users/1 @PutMapping("/{id}") public Result<User> updateUser(@PathVariable Integer id, @Valid @RequestBody User user) { user.setId(id); return userService.updateUser(user); } // DELETE /api/users/1 @DeleteMapping("/{id}") public Result<Void> deleteUser(@PathVariable Integer id) { return userService.deleteUser(id); } // 复杂查询 @GetMapping("/search") public Result<List<User>> searchUsers( @RequestParam(required = false) String keyword, @RequestParam(required = false) String email) { // 这里调用对应的Service方法 return Result.success(Collections.emptyList()); } }
五、核心组件实现
5.1 统一响应格式
java
@Data @JsonInclude(JsonInclude.Include.NON_NULL) public class Result<T> { private Integer code; private String message; private T data; private Long timestamp; private String path; private Result(Integer code, String message, T data, String path) { this.code = code; this.message = message; this.data = data; this.timestamp = System.currentTimeMillis(); this.path = path; } // 成功响应 public static <T> Result<T> success(T data) { return new Result<>(200, "success", data, getCurrentRequestPath()); } public static <T> Result<T> success() { return success(null); } // 失败响应 public static <T> Result<T> error(Integer code, String message) { return new Result<>(code, message, null, getCurrentRequestPath()); } // 业务错误 public static <T> Result<T> businessError(String message) { return error(400, message); } private static String getCurrentRequestPath() { try { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); return request.getRequestURI(); } catch (Exception e) { return "unknown"; } } }
5.2 分页结果封装
java
@Data @AllArgsConstructor public class PageResult<T> { private List<T> list; private Integer total; private Integer pages; private Integer page; private Integer size; public PageResult(List<T> list, Integer total, Integer pages, Integer page, Integer size) { this.list = list; this.total = total; this.pages = pages; this.page = page; this.size = size; } }
5.3 异常体系
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 BusinessException extends BaseException { public BusinessException(Integer code, String message) { super(code, message); } public BusinessException(String message) { this(400, message); } } // 系统异常 public class SystemException extends BaseException { public SystemException(Integer code, String message) { super(code, message); } public SystemException(String message) { this(500, message); } // 预定义系统异常 public static SystemException DB_ERROR = new SystemException(50001, "数据库异常"); public static SystemException NETWORK_ERROR = new SystemException(50002, "网络异常"); }
5.4 全局异常处理
java
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // 处理业务异常 @ExceptionHandler(BusinessException.class) public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) { log.warn("业务异常: {} - {}", request.getRequestURI(), e.getMessage()); return Result.error(e.getCode(), e.getMessage()); } // 处理参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public Result<Void> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest request) { String message = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); log.warn("参数校验失败: {} - {}", request.getRequestURI(), message); return Result.error(400, message); } // 处理数据绑定异常 @ExceptionHandler(BindException.class) public Result<Void> handleBindException(BindException e, HttpServletRequest request) { String message = e.getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); log.warn("数据绑定异常: {} - {}", request.getRequestURI(), message); return Result.error(400, message); } // 处理404异常 @ExceptionHandler(NoHandlerFoundException.class) public Result<Void> handleNotFoundException(NoHandlerFoundException e, HttpServletRequest request) { log.warn("接口不存在: {} {}", e.getHttpMethod(), e.getRequestURL()); return Result.error(404, "接口不存在"); } // 处理系统异常 @ExceptionHandler(SystemException.class) public Result<Void> handleSystemException(SystemException e, HttpServletRequest request) { log.error("系统异常: {} - {}", request.getRequestURI(), e.getMessage(), e); return Result.error(e.getCode(), e.getMessage()); } // 处理其他所有异常 @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e, HttpServletRequest request) { log.error("未知异常: {} - {}", request.getRequestURI(), e.getMessage(), e); return Result.error(500, "系统异常,请稍后重试"); } }
5.5 拦截器实现
java
@Component @Slf4j public class LogInterceptor implements HandlerInterceptor { private ThreadLocal<Long> startTime = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { startTime.set(System.currentTimeMillis()); String requestURI = request.getRequestURI(); String method = request.getMethod(); String queryString = request.getQueryString(); log.info("请求开始: {} {}?{}", method, requestURI, queryString); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { long cost = System.currentTimeMillis() - startTime.get(); int status = response.getStatus(); log.info("请求完成: {} {} - 状态: {} - 耗时: {}ms", request.getMethod(), request.getRequestURI(), status, cost); startTime.remove(); } }
java
@Component @Slf4j public class AuthInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } // 获取Token String token = getTokenFromRequest(request); if (token == null) { sendErrorResponse(response, 401, "未授权访问"); return false; } try { // 验证Token String username = jwtUtil.validateToken(token); // 将用户信息存入请求属性 request.setAttribute("currentUser", username); return true; } catch (BusinessException e) { sendErrorResponse(response, e.getCode(), e.getMessage()); return false; } catch (Exception e) { sendErrorResponse(response, 401, "Token验证失败"); return false; } } private String getTokenFromRequest(HttpServletRequest request) { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { return header.substring(7); } return request.getParameter("token"); } private void sendErrorResponse(HttpServletResponse response, int code, String message) throws IOException { response.setStatus(code); response.setContentType("application/json;charset=UTF-8"); Result<Void> result = Result.error(code, message); String json = new ObjectMapper().writeValueAsString(result); response.getWriter().write(json); response.getWriter().flush(); } }
六、配置文件
6.1 application.yml
yaml
server: port: 8080 servlet: context-path: / tomcat: uri-encoding: UTF-8 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 servlet: multipart: max-file-size: 10MB max-request-size: 10MB mvc: throw-exception-if-no-handler-found: true jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 serialization: write-dates-as-timestamps: false mybatis: configuration: map-underscore-to-camel-case: true cache-enabled: true lazy-loading-enabled: false type-aliases-package: com.example.entity logging: level: com.example.mapper: DEBUG com.example: INFO pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
七、测试用例
7.1 Service层测试
java
@RunWith(SpringRunner.class) @SpringBootTest @Transactional // 测试后回滚 public class UserServiceTest { @Autowired private UserService userService; @Test public void testCreateUser() { User user = new User(); user.setUsername("testuser"); user.setEmail("[email protected]"); user.setPassword("123456"); user.setAge(25); Result<User> result = userService.createUser(user); assertNotNull(result); assertEquals(200, result.getCode().intValue()); assertNotNull(result.getData().getId()); } @Test public void testCreateUserWithDuplicateUsername() { User user = new User(); user.setUsername("admin"); // 假设已存在 user.setEmail("[email protected]"); assertThrows(BusinessException.class, () -> { userService.createUser(user); }); } }
7.2 Controller层测试
java
@RunWith(SpringRunner.class) @WebMvcTest(UserController.class) public class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test public void testGetUser() throws Exception { User user = new User(1, "testuser", "[email protected]", null, 25, null, null); when(userService.getUserById(1)).thenReturn(user); mockMvc.perform(get("/api/users/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.username").value("testuser")); } @Test public void testCreateUser() throws Exception { User user = new User(null, "newuser", "[email protected]", "123456", 25, null, null); when(userService.createUser(any(User.class))) .thenReturn(Result.success(user)); mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"newuser\",\"email\":\"[email protected]\",\"password\":\"123456\",\"age\":25}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } }
八、生产环境最佳实践
8.1 性能优化
java
@Configuration public class PerformanceConfig { // 连接池优化 @Bean @ConfigurationProperties(prefix = "spring.datasource.druid") public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); // 生产环境建议配置 dataSource.setInitialSize(10); dataSource.setMinIdle(10); dataSource.setMaxActive(50); dataSource.setMaxWait(60000); dataSource.setTimeBetweenEvictionRunsMillis(60000); dataSource.setMinEvictableIdleTimeMillis(300000); return dataSource; } // MyBatis二级缓存配置 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用Jackson序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(Lazy); serializer.setObjectMapper(mapper); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } // 线程池配置 @Bean public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(100); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("async-task-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }
8.2 缓存配置
java
@Configuration @EnableCaching @Slf4j public class CacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) // 默认缓存30分钟 .disableCachingNullValues() // 不缓存空值 .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); // 针对不同缓存设置不同的过期时间 Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>(); cacheConfigurations.put("users", config.entryTtl(Duration.ofHours(1))); cacheConfigurations.put("products", config.entryTtl(Duration.ofMinutes(10))); return RedisCacheManager.builder(factory) .cacheDefaults(config) .withInitialCacheConfigurations(cacheConfigurations) .build(); } // 缓存使用示例 @Service public class UserService { @Cacheable(value = "users", key = "#id") public User getUserById(Integer id) { log.info("从数据库查询用户: {}", id); return userMapper.selectById(id); } @CacheEvict(value = "users", key = "#user.id") public void updateUser(User user) { userMapper.update(user); } @CacheEvict(value = "users", allEntries = true) public void clearUserCache() { log.info("清空用户缓存"); } } }
8.3 异步处理
java
@Service @Slf4j public class AsyncService { @Async("taskExecutor") public CompletableFuture<Void> processUserAsync(User user) { log.info("开始异步处理用户: {}", user.getUsername()); try { // 模拟耗时操作 Thread.sleep(2000); // 发送邮件、短信等 sendWelcomeEmail(user); sendSmsNotification(user); log.info("异步处理完成: {}", user.getUsername()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("异步处理被中断", e); } return CompletableFuture.completedFuture(null); } private void sendWelcomeEmail(User user) { // 发送欢迎邮件逻辑 log.info("发送欢迎邮件给: {}", user.getEmail()); } private void sendSmsNotification(User user) { // 发送短信通知逻辑 log.info("发送短信通知给用户: {}", user.getUsername()); } } // 在Controller中使用 @RestController public class UserController { @Autowired private AsyncService asyncService; @PostMapping("/users/async") public Result<String> createUserAsync(@Valid @RequestBody User user) { // 同步创建用户 userService.createUser(user); // 异步处理后续任务 asyncService.processUserAsync(user); return Result.success("用户创建成功,正在处理后续任务..."); } }
九、安全与监控
9.1 接口限流
java
@Aspect @Component @Slf4j public class RateLimitAspect { @Autowired private StringRedisTemplate redisTemplate; // 限流注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { String key() default ""; // 限流key int limit() default 100; // 限制次数 int period() default 60; // 时间周期(秒) String message() default "请求过于频繁"; } @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { String key = generateKey(joinPoint, rateLimit); int limit = rateLimit.limit(); int period = rateLimit.period(); String luaScript = "local key = KEYS[1] " + "local limit = tonumber(ARGV[1]) " + "local period = tonumber(ARGV[2]) " + "local current = redis.call('incr', key) " + "if current == 1 then " + " redis.call('expire', key, period) " + "end " + "return current <= limit"; Boolean allowed = redisTemplate.execute( new DefaultRedisScript<>(luaScript, Boolean.class), Collections.singletonList(key), String.valueOf(limit), String.valueOf(period) ); if (Boolean.FALSE.equals(allowed)) { throw new BusinessException(429, rateLimit.message()); } return joinPoint.proceed(); } private String generateKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) { String methodName = joinPoint.getSignature().getName(); String className = joinPoint.getTarget().getClass().getSimpleName(); // 获取请求IP String ip = getClientIP(); if (!rateLimit.key().isEmpty()) { return "rate_limit:" + rateLimit.key() + ":" + ip; } return "rate_limit:" + className + ":" + methodName + ":" + ip; } private String getClientIP() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); 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; } } // 使用示例 @RestController public class ApiController { @RateLimit(key = "login", limit = 5, period = 60, message = "登录尝试过于频繁") @PostMapping("/auth/login") public Result<String> login(@RequestBody LoginRequest request) { // 登录逻辑 return Result.success("登录成功"); } @RateLimit(limit = 100, period = 60) // 每分钟100次 @GetMapping("/api/data") public Result<List<Data>> getData() { return Result.success(dataService.getData()); } }
9.2 接口文档(Swagger)
java
@Configuration @EnableOpenApi public class SwaggerConfig { @Bean public Docket createRestApi() { return new Docket(DocumentationType.OAS_30) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.example.controller")) .paths(PathSelectors.any()) .build() .securitySchemes(securitySchemes()) .securityContexts(securityContexts()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("SSM项目API文档") .description("基于Spring Boot + Spring MVC + MyBatis的RESTful API") .version("1.0") .contact(new Contact("开发团队", "https://example.com", "[email protected]")) .build(); } private List<SecurityScheme> securitySchemes() { return Collections.singletonList( new HttpAuthenticationBuilder() .name("Authorization") .scheme("bearer") .bearerFormat("JWT") .build() ); } private List<SecurityContext> securityContexts() { return Collections.singletonList( SecurityContext.builder() .securityReferences(Collections.singletonList( new SecurityReference("Authorization", new AuthorizationScope[0]))) .operationSelector(o -> o.requestMappingPattern().matches("/api/.*")) .build() ); } } // 在Controller中使用Swagger注解 @RestController @RequestMapping("/api/users") @Api(tags = "用户管理") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") @ApiOperation("根据ID查询用户") @ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "int") @ApiResponses({ @ApiResponse(code = 200, message = "成功"), @ApiResponse(code = 404, message = "用户不存在") }) public Result<User> getUser(@PathVariable Integer id) { User user = userService.getUserById(id); return Result.success(user); } @PostMapping @ApiOperation("创建用户") public Result<User> createUser(@RequestBody @Valid User user) { return userService.createUser(user); } }
9.3 健康检查与监控
java
@Component public class CustomHealthIndicator implements HealthIndicator { @Autowired private DataSource dataSource; @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public Health health() { // 检查数据库连接 boolean dbHealthy = checkDatabase(); // 检查Redis连接 boolean redisHealthy = checkRedis(); // 检查磁盘空间等 if (dbHealthy && redisHealthy) { return Health.up() .withDetail("database", "连接正常") .withDetail("redis", "连接正常") .build(); } else { return Health.down() .withDetail("database", dbHealthy ? "正常" : "异常") .withDetail("redis", redisHealthy ? "正常" : "异常") .build(); } } private boolean checkDatabase() { try (Connection conn = dataSource.getConnection()) { return conn.isValid(5); } catch (Exception e) { return false; } } private boolean checkRedis() { try { redisTemplate.opsForValue().get("health_check"); return true; } catch (Exception e) { return false; } } } // 自定义指标 @Component public class CustomMetrics { private final Counter userRegistrationCounter; private final Timer apiRequestTimer; public CustomMetrics(MeterRegistry registry) { this.userRegistrationCounter = Counter.builder("user.registration.count") .description("用户注册次数") .register(registry); this.apiRequestTimer = Timer.builder("api.request.duration") .description("API请求耗时") .register(registry); } public void incrementUserRegistration() { userRegistrationCounter.increment(); } public Timer getApiRequestTimer() { return apiRequestTimer; } } // 在Service中使用 @Service public class UserService { @Autowired private CustomMetrics metrics; public Result<User> createUser(User user) { // 记录耗时 Timer.Sample sample = Timer.start(); try { // 业务逻辑 userMapper.insert(user); // 记录指标 metrics.incrementUserRegistration(); return Result.success(user); } finally { sample.stop(metrics.getApiRequestTimer()); } } }
十、部署与运维
10.1 多环境配置
yaml
# application-dev.yml spring: datasource: url: jdbc:mysql://localhost:3306/ssm_demo_dev username: dev_user password: dev_pass redis: host: localhost port: 6379 logging: level: com.example: DEBUG --- # application-test.yml spring: datasource: url: jdbc:mysql://test-db:3306/ssm_demo_test username: test_user password: test_pass redis: host: test-redis port: 6379 logging: level: com.example: INFO --- # application-prod.yml spring: datasource: url: jdbc:mysql://prod-db:3306/ssm_demo_prod username: ${DB_USERNAME} password: ${DB_PASSWORD} druid: initial-size: 20 min-idle: 20 max-active: 100 redis: host: ${REDIS_HOST} password: ${REDIS_PASSWORD} logging: level: com.example: WARN file: name: /app/logs/ssm-demo.log
10.2 Docker部署
dockerfile
# Dockerfile FROM openjdk:8-jre-alpine # 安装时区数据 RUN apk add --no-cache tzdata ENV TZ=Asia/Shanghai # 创建应用目录 RUN mkdir -p /app WORKDIR /app # 复制JAR文件 COPY target/ssm-demo-1.0.0.jar app.jar # 创建非root用户 RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser # 暴露端口 EXPOSE 8080 # 启动应用 ENTRYPOINT ["java", "-jar", "app.jar"]
yaml
# docker-compose.yml version: '3.8' services: app: build: . ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=prod - DB_USERNAME=prod_user - DB_PASSWORD=prod_password - REDIS_HOST=redis - REDIS_PASSWORD=redis_pass depends_on: - mysql - redis networks: - ssm-network mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root_pass MYSQL_DATABASE: ssm_demo_prod MYSQL_USER: prod_user MYSQL_PASSWORD: prod_password volumes: - mysql_data:/var/lib/mysql networks: - ssm-network redis: image: redis:6-alpine command: redis-server --requirepass redis_pass volumes: - redis_data:/data networks: - ssm-network volumes: mysql_data: redis_data: networks: ssm-network: driver: bridge
十一、故障排查与调试
11.1 日志配置优化
yaml
# logback-spring.xml <?xml version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_PATH" value="/app/logs"/> <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <!-- 控制台输出 --> <appender name="CONSOLE"> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <!-- 文件输出 --> <appender name="FILE"> <file>${LOG_PATH}/application.log</file> <rollingPolicy> <fileNamePattern>${LOG_PATH}/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxHistory>30</maxHistory> <timeBasedFileNamingAndTriggeringPolicy> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <!-- SQL日志 --> <logger name="com.example.mapper" level="DEBUG" additivity="false"> <appender-ref ref="FILE"/> </logger> <!-- 业务日志 --> <logger name="com.example.service" level="INFO"/> <logger name="com.example.controller" level="INFO"/> <!-- 错误日志单独文件 --> <appender name="ERROR_FILE"> <file>${LOG_PATH}/error.log</file> <filter> <level>ERROR</level> </filter> <rollingPolicy> <fileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> <appender-ref ref="ERROR_FILE"/> </root> </configuration>
11.2 调试工具类
java
@Component public class DebugUtils { private static final Logger log = LoggerFactory.getLogger(DebugUtils.class); /** * 打印方法执行时间 */ public static <T> T logExecutionTime(Supplier<T> supplier, String methodName) { long startTime = System.currentTimeMillis(); try { return supplier.get(); } finally { long endTime = System.currentTimeMillis(); log.debug("方法 {} 执行耗时: {}ms", methodName, (endTime - startTime)); } } /** * 打印SQL参数 */ public static void logSqlParams(String sql, Object... params) { if (log.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append("SQL: ").append(sql).append("\n"); sb.append("参数: "); for (int i = 0; i < params.length; i++) { sb.append(params[i]); if (i < params.length - 1) { sb.append(", "); } } log.debug(sb.toString()); } } /** * 打印请求信息 */ public static void logRequestInfo(HttpServletRequest request) { if (log.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append("请求信息: \n"); sb.append(" URL: ").append(request.getRequestURL()).append("\n"); sb.append(" 方法: ").append(request.getMethod()).append("\n"); sb.append(" IP: ").append(request.getRemoteAddr()).append("\n"); sb.append(" 参数: ").append(request.getQueryString()).append("\n"); sb.append(" User-Agent: ").append(request.getHeader("User-Agent")); log.debug(sb.toString()); } } } // 使用示例 @Service public class UserService { public User getUserById(Integer id) { return DebugUtils.logExecutionTime(() -> { return userMapper.selectById(id); }, "getUserById"); } }
十二、总结
12.1 核心要点回顾
- 架构清晰:严格遵循MVC分层,各司其职
- 配置分离:Spring管理业务层,Spring MVC管理Web层
- 事务管理:合理使用传播行为,避免事务失效
- 异常处理:统一异常处理,友好错误提示
- 性能优化:缓存、异步、连接池优化
- 安全防护:认证、授权、限流、参数校验
- 监控运维:健康检查、日志管理、指标监控
12.2 最佳实践清单
- ✅ 使用@Transactional管理事务
- ✅ 统一异常处理@ControllerAdvice
- ✅ 统一响应格式Result
- ✅ 参数校验@Valid
- ✅ 接口文档Swagger
- ✅ 缓存优化@Cacheable
- ✅ 异步处理@Async
- ✅ 接口限流@RateLimit
- ✅ 日志分级管理
- ✅ 多环境配置
- ✅ Docker容器化部署
12.3 常见问题解决方案
| 问题 | 解决方案 |
|---|---|
| 事务不生效 | 检查方法可见性、异常处理、代理调用 |
| 循环依赖 | 使用@Lazy、构造器注入 |
| 性能瓶颈 | 添加缓存、优化SQL、异步处理 |
| 内存泄漏 | 监控连接池、及时关闭资源 |
| 并发问题 | 使用线程安全组件、合理加锁 |
记住:SSM + Spring MVC 的成功 = 扎实的基础 + 规范的设计 + 持续的优化!
通过这个完整的整合指南,你应该能够构建出高质量、可维护、高性能的企业级Java应用。在实际开发中,要根据具体业务需求灵活调整,持续优化。