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 核心要点回顾
-
架构清晰:严格遵循MVC分层,各司其职
-
配置分离:Spring管理业务层,Spring MVC管理Web层
-
事务管理:合理使用传播行为,避免事务失效
-
异常处理:统一异常处理,友好错误提示
-
性能优化:缓存、异步、连接池优化
-
安全防护:认证、授权、限流、参数校验
-
监控运维:健康检查、日志管理、指标监控
12.2 最佳实践清单
-
✅ 使用@Transactional管理事务
-
✅ 统一异常处理@ControllerAdvice
-
✅ 统一响应格式Result
-
✅ 参数校验@Valid
-
✅ 接口文档Swagger
-
✅ 缓存优化@Cacheable
-
✅ 异步处理@Async
-
✅ 接口限流@RateLimit
-
✅ 日志分级管理
-
✅ 多环境配置
-
✅ Docker容器化部署
12.3 常见问题解决方案
| 问题 | 解决方案 |
|---|---|
| 事务不生效 | 检查方法可见性、异常处理、代理调用 |
| 循环依赖 | 使用@Lazy、构造器注入 |
| 性能瓶颈 | 添加缓存、优化SQL、异步处理 |
| 内存泄漏 | 监控连接池、及时关闭资源 |
| 并发问题 | 使用线程安全组件、合理加锁 |
记住:SSM + Spring MVC 的成功 = 扎实的基础 + 规范的设计 + 持续的优化!
通过这个完整的整合指南,你应该能够构建出高质量、可维护、高性能的企业级Java应用。在实际开发中,要根据具体业务需求灵活调整,持续优化。
1299

被折叠的 条评论
为什么被折叠?



