跳到主要内容SSM 框架与 Spring MVC 完整整合实战指南 | 极客日志Javajava
SSM 框架与 Spring MVC 完整整合实战指南
SSM 框架与 Spring MVC 整合实战涵盖架构设计、项目结构、核心配置及分层实现。文章详细阐述了 Controller、Service、Mapper 层的职责划分与代码编写,包括统一响应格式、分页封装、异常处理体系及拦截器实现。此外还涉及配置文件管理、单元测试、生产环境性能优化(缓存、异步、连接池)、安全监控(限流、Swagger)以及 Docker 部署方案。通过规范设计与持续优化,构建高质量企业级 Java 应用。
PentesterX4 浏览 SSM + Spring MVC 完整整合实战指南
一、SSM 架构核心认知
1.1 整体架构图
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 |
二、完整项目结构
src/main/java
├── com.example
│ ├── config
│ │ ├── SpringConfig.java
│ │ ├── SpringMvcConfig.java
│ │ ├── MyBatisConfig.java
│ │ └── 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/
└── static/
三、核心配置整合
3.1 主启动类
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.2 Spring + Spring MVC 配置分离
@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)
public class SpringConfig {
}
@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) {
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 配置
@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");
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 数据源配置
@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)
@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)
@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);
}
<?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)
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);
}
@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);
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)
@RestController
@RequestMapping("/api/users")
@Validated
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public Result<User> getUser(@PathVariable @Min(1) Integer id) {
log.info("查询用户详情:{}", id);
User user = userService.getUserById(id);
return Result.success(user);
}
@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);
}
@PostMapping
public Result<User> createUser(@Valid @RequestBody User user) {
log.info("创建用户:{}", user.getUsername());
return userService.createUser(user);
}
@PutMapping("/{id}")
public Result<User> updateUser(@PathVariable Integer id, @Valid @RequestBody User user) {
user.setId(id);
return userService.updateUser(user);
}
@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) {
return Result.success(Collections.emptyList());
}
}
五、核心组件实现
5.1 统一响应格式
@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 分页结果封装
@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 异常体系
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 全局异常处理
@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);
}
@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 拦截器实现
@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();
}
}
@Component
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String token = getTokenFromRequest(request);
if (token == null) {
sendErrorResponse(response, 401, "未授权访问");
return false;
}
try {
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
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 层测试
@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 层测试
@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 性能优化
@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;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
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 缓存配置
@Configuration
@EnableCaching
@Slf4j
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(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 异步处理
@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());
}
}
@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 接口限流
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key() default "";
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();
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)
@GetMapping("/api/data")
public Result<List<Data>> getData() {
return Result.success(dataService.getData());
}
}
9.2 接口文档(Swagger)
@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()
);
}
}
@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 健康检查与监控
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Autowired
private DataSource dataSource;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Health health() {
boolean dbHealthy = checkDatabase();
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
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 多环境配置
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
---
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
---
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
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"]
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 日志配置优化
<?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>
<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 调试工具类
@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));
}
}
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 应用。在实际开发中,要根据具体业务需求灵活调整,持续优化。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online