【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 id="BaseResultMap" 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 id="selectById" resultMap="BaseResultMap">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user (username, email, password, age, create_time)
        VALUES (#{username}, #{email}, #{password}, #{age}, NOW())
    </insert>

    <select id="selectByCondition" 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("test@example.com");
        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("test@example.com");
        
        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", "test@example.com", 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", "new@example.com", "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\":\"new@example.com\",\"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", "dev@example.com"))
                .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" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    
    <!-- 文件输出 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <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" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/error.log</file>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <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 核心要点回顾

  1. 架构清晰:严格遵循MVC分层,各司其职

  2. 配置分离:Spring管理业务层,Spring MVC管理Web层

  3. 事务管理:合理使用传播行为,避免事务失效

  4. 异常处理:统一异常处理,友好错误提示

  5. 性能优化:缓存、异步、连接池优化

  6. 安全防护:认证、授权、限流、参数校验

  7. 监控运维:健康检查、日志管理、指标监控

12.2 最佳实践清单

  • ✅ 使用@Transactional管理事务

  • ✅ 统一异常处理@ControllerAdvice

  • ✅ 统一响应格式Result

  • ✅ 参数校验@Valid

  • ✅ 接口文档Swagger

  • ✅ 缓存优化@Cacheable

  • ✅ 异步处理@Async

  • ✅ 接口限流@RateLimit

  • ✅ 日志分级管理

  • ✅ 多环境配置

  • ✅ Docker容器化部署

12.3 常见问题解决方案

问题解决方案
事务不生效检查方法可见性、异常处理、代理调用
循环依赖使用@Lazy、构造器注入
性能瓶颈添加缓存、优化SQL、异步处理
内存泄漏监控连接池、及时关闭资源
并发问题使用线程安全组件、合理加锁

记住:SSM + Spring MVC 的成功 = 扎实的基础 + 规范的设计 + 持续的优化

通过这个完整的整合指南,你应该能够构建出高质量、可维护、高性能的企业级Java应用。在实际开发中,要根据具体业务需求灵活调整,持续优化。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值