
1. Spring Task 定时任务实战指南定时任务是后端开发中常见的需求场景Spring 提供了简单易用的Scheduled注解来实现定时任务调度。下面我将结合实际项目经验详细介绍 Spring Task 的使用方法和注意事项。1.1 定时任务典型应用场景在实际项目中我们经常遇到需要定时执行的任务以下是一些典型场景金融业务场景信用卡每月还款提醒每月固定日期发送短信/邮件房贷每月还款提醒每月指定日期扣款理财到期提醒根据产品期限提前通知电商业务场景未支付订单超时取消30分钟未支付自动取消发货超时提醒商家超过48小时未发货提醒自动确认收货发货后15天自动确认社交业务场景生日/纪念日提醒提前1天发送祝福长期未登录用户召回30天未登录发送召回邮件系统维护场景每日凌晨执行数据备份每周清理临时文件每月生成统计报表1.2 Cron 表达式详解Cron 表达式是定时任务的核心Spring 使用的是标准的 6 位格式秒 分 时 日 月 周常见误区新手常犯的错误是使用 Quartz 的 7 位格式包含年字段周和日字段通常只定义一个另一个设为?表示不指定正确示例// 每天凌晨1点执行 Scheduled(cron 0 0 1 * * ?) public void dailyTask() { // 业务逻辑 } // 每5分钟执行一次 Scheduled(cron 0 */5 * * * ?) public void everyFiveMinutes() { // 业务逻辑 }1.3 定时任务实战案例下面是一个处理外卖订单状态的定时任务实现Slf4j Component public class OrderStatusTask { Autowired private OrderMapper orderMapper; /** * 每小时处理超时未完成的派送中订单 * 将超过1小时未完成的派送中订单自动标记为已完成 */ Scheduled(cron 0 0 * * * ?) public void processDeliveryTimeout() { log.info(开始处理超时派送订单...); // 查询1小时前处于派送中状态的订单 LocalDateTime oneHourAgo LocalDateTime.now().minusHours(1); ListOrder timeoutOrders orderMapper.findByStatusAndOrderTimeBefore( OrderStatus.DELIVERY_IN_PROGRESS, oneHourAgo ); if (!CollectionUtils.isEmpty(timeoutOrders)) { timeoutOrders.forEach(order - { order.setStatus(OrderStatus.COMPLETED); order.setCompleteTime(LocalDateTime.now()); orderMapper.update(order); log.info(订单{}超时自动完成, order.getId()); }); } } }关键点说明使用Component让 Spring 管理任务类Scheduled注解标记定时方法通过orderMapper操作数据库添加详细的日志记录1.4 定时任务最佳实践1. 任务幂等性设计定时任务可能会重复执行确保业务逻辑可以安全地多次执行使用状态机控制流程避免重复处理2. 异常处理捕获并记录异常避免任务中断重要任务可添加重试机制Scheduled(cron 0 0 2 * * ?) public void importantTask() { try { // 业务逻辑 } catch (Exception e) { log.error(定时任务执行失败, e); // 发送告警通知 alertService.sendAlert(重要任务执行失败, e.getMessage()); } }3. 分布式环境考虑单机部署时没问题但在集群环境下会重复执行解决方案使用分布式锁Redis/Zookeeper使用专门的调度框架XXL-JOB/Elastic-Job4. 性能优化大数据量处理时分批执行耗时任务考虑异步执行Async Scheduled(cron 0 0 3 * * ?) public void heavyTask() { // 耗时操作 }2. WebSocket 实时通信实战WebSocket 是实现浏览器和服务器全双工通信的协议非常适合需要实时交互的场景。2.1 WebSocket 与 HTTP 对比特性HTTPWebSocket通信模式单向请求-响应全双工双向通信连接建立每次请求新建连接一次握手持久连接头部开销每次请求完整头部首次握手后仅2-14字节帧头服务器推送需要轮询或长轮询模拟原生支持延迟高频繁建立连接低连接复用适用场景RESTful API、静态资源实时聊天、股票行情、在线游戏2.2 WebSocket 服务端实现下面是一个完整的 WebSocket 服务实现Slf4j Component ServerEndpoint(/ws/{userId}) // 定义端点路径支持路径参数 public class WebSocketServer { // 保存所有会话的线程安全Map private static final ConcurrentHashMapString, Session sessions new ConcurrentHashMap(); /** * 连接建立成功回调 */ OnOpen public void onOpen(Session session, PathParam(userId) String userId) { sessions.put(userId, session); log.info(用户{}连接建立当前在线人数{}, userId, sessions.size()); sendMessage(userId, 连接成功); } /** * 收到客户端消息回调 */ OnMessage public void onMessage(String message, PathParam(userId) String userId) { log.info(收到用户{}的消息{}, userId, message); // 处理业务逻辑... } /** * 连接关闭回调 */ OnClose public void onClose(PathParam(userId) String userId) { sessions.remove(userId); log.info(用户{}断开连接当前在线人数{}, userId, sessions.size()); } /** * 发生错误回调 */ OnError public void onError(PathParam(userId) String userId, Throwable error) { log.error(用户{}的连接发生错误, userId, error); sessions.remove(userId); } /** * 向指定用户发送消息 */ public static void sendMessage(String userId, String message) { Session session sessions.get(userId); if (session ! null session.isOpen()) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error(向用户{}发送消息失败, userId, e); } } } /** * 群发消息 */ public static void broadcast(String message) { sessions.forEach((userId, session) - { if (session.isOpen()) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error(群发消息给用户{}失败, userId, e); } } }); } }2.3 WebSocket 配置类需要配置 ServerEndpointExporter 来注册 WebSocket 端点Configuration public class WebSocketConfig { /** * 自动注册使用了ServerEndpoint注解的类 */ Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } /** * 解决WebSocket中无法注入Spring Bean的问题 */ Bean public SpringContextHolder springContextHolder() { return new SpringContextHolder(); } } // 用于获取Spring管理的Bean public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext context; Override public void setApplicationContext(ApplicationContext applicationContext) { context applicationContext; } public static T T getBean(ClassT clazz) { return context.getBean(clazz); } }2.4 WebSocket 客户端实现前端 JavaScript 连接示例// 建立WebSocket连接 const socket new WebSocket(ws://${location.host}/ws/${userId}); // 连接成功回调 socket.onopen function() { console.log(WebSocket连接已建立); }; // 接收消息回调 socket.onmessage function(event) { const message JSON.parse(event.data); console.log(收到消息:, message); // 更新UI... }; // 连接关闭回调 socket.onclose function() { console.log(WebSocket连接已关闭); // 尝试重连... }; // 发送消息 function sendMessage(content) { if (socket.readyState WebSocket.OPEN) { socket.send(JSON.stringify({ type: message, content: content })); } }2.5 WebSocket 实战技巧1. 心跳机制防止连接因超时被关闭定时发送ping/pong帧保持连接// 服务端心跳处理 OnMessage public void onPong(PongMessage pong, Session session) { // 更新最后活跃时间 } // 定时发送ping scheduledExecutor.scheduleAtFixedRate(() - { sessions.forEach((id, session) - { try { session.getBasicRemote().sendPing(ByteBuffer.wrap(ping.getBytes())); } catch (Exception e) { log.error(发送ping失败, e); } }); }, 0, 30, TimeUnit.SECONDS);2. 消息重连机制网络不稳定时自动重连客户端实现指数退避重试let reconnectAttempts 0; const maxReconnectAttempts 5; const reconnectDelay 1000; // 初始1秒 function connectWebSocket() { const socket new WebSocket(endpoint); socket.onclose function() { if (reconnectAttempts maxReconnectAttempts) { const delay reconnectDelay * Math.pow(2, reconnectAttempts); reconnectAttempts; setTimeout(connectWebSocket, delay); } }; }3. 消息序列化使用JSON或Protocol Buffers定义统一的消息格式public class WebSocketMessageT implements Serializable { private String type; // 消息类型 private T data; // 消息内容 private Long timestamp; // getters/setters... }3. MyBatis 动态SQL实战MyBatis 的强大之处在于其灵活的动态SQL功能下面通过实际案例展示如何使用。3.1 条件查询实现!-- 根据条件统计订单金额 -- select idsumByMap resultTypejava.lang.Double SELECT SUM(amount) FROM orders where if testbegin ! null AND order_time gt; #{begin} /if if testend ! null AND order_time lt; #{end} /if if teststatus ! null AND status #{status} /if /where /select注意事项XML中的特殊符号需要使用转义lt;表示 gt;表示 amp;表示 where标签会智能处理前缀AND/OR如果所有条件都不满足WHERE关键字不会出现自动去除第一个条件的AND/OR3.2 复杂动态SQL示例!-- 批量更新订单状态 -- update idbatchUpdateStatus UPDATE orders set if teststatus ! nullstatus #{status},/if if testupdateTime ! nullupdate_time #{updateTime},/if /set WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /update关键点set标签会智能处理后缀逗号foreach用于构建IN语句参数通过Map或Param注解传递3.3 动态SQL最佳实践1. 避免过度复杂当条件超过5个时考虑拆分为多个查询或者使用choose简化逻辑select idfindUsers resultTypeUser SELECT * FROM users where choose when testrole admin AND is_admin 1 /when when testrole vip AND is_vip 1 /when otherwise AND status active /otherwise /choose /where /select2. 性能优化大量数据时使用分页查询频繁查询考虑添加索引select idfindByPage resultTypeUser SELECT * FROM users ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select3. 结果映射复杂结果使用resultMap关联查询使用association和collectionresultMap idorderDetailMap typeOrder id propertyid columnid/ result propertyamount columnamount/ collection propertyitems ofTypeOrderItem id propertyid columnitem_id/ result propertyproductName columnproduct_name/ /collection /resultMap4. Java 8 时间API最佳实践Java 8 引入了全新的日期时间API位于java.time包下解决了旧API的诸多问题。4.1 核心类对比类描述不可变性时区支持典型用途LocalDate只包含日期是无生日、纪念日LocalTime只包含时间是无营业时间、会议时间LocalDateTime包含日期和时间是无订单创建时间、日志时间ZonedDateTime带时区的日期时间是有跨时区会议、航班时刻Instant时间戳Unix时间是UTC日志时间戳、性能统计Duration时间间隔秒/纳秒是无计算两个时间点的差值Period日期间隔年/月/日是无计算两个日期的差值4.2 常见操作示例1. 日期计算// 获取当前日期 LocalDate today LocalDate.now(); // 计算明天 LocalDate tomorrow today.plusDays(1); // 本月最后一天 LocalDate lastDayOfMonth today.with(TemporalAdjusters.lastDayOfMonth()); // 计算两个日期之间的天数 long daysBetween ChronoUnit.DAYS.between(startDate, endDate);2. 时间格式化// 日期 - 字符串 DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); String formatted LocalDateTime.now().format(formatter); // 字符串 - 日期 LocalDateTime parsed LocalDateTime.parse(2023-01-01 12:00:00, formatter);3. 时区转换// 获取上海时区的当前时间 ZonedDateTime shanghaiTime ZonedDateTime.now(ZoneId.of(Asia/Shanghai)); // 转换为纽约时间 ZonedDateTime newYorkTime shanghaiTime.withZoneSameInstant(ZoneId.of(America/New_York));4.3 常见问题解决方案问题1日期范围生成错误错误代码for (int i 1; i end.getDayOfMonth() - begin.getDayOfMonth(); i) { dateList.add(begin.plusDays(i)); }问题分析getDayOfMonth()只返回月份中的天数1-31跨月计算会得到错误结果正确方案LocalDate current begin; while (!current.isAfter(end)) { dateList.add(current); current current.plusDays(1); }问题2数据库交互// 保存到数据库 preparedStatement.setObject(1, LocalDateTime.now()); // 从数据库读取 LocalDateTime createTime resultSet.getObject(create_time, LocalDateTime.class);问题3JSON序列化Spring Boot默认使用Jackson需要添加依赖dependency groupIdcom.fasterxml.jackson.datatype/groupId artifactIdjackson-datatype-jsr310/artifactId /dependency配置格式化spring: jackson: serialization: write-dates-as-timestamps: false time-zone: Asia/Shanghai5. Stream API 深度解析Java 8 的 Stream API 为集合操作提供了函数式编程能力大幅简化了数据处理代码。5.1 Stream 操作分类操作类型返回值说明示例方法中间操作Stream惰性求值可链式调用filter, map, sorted终端操作非Stream触发计算产生结果或副作用forEach, collect, reduce短路操作可能提前结束不需要处理全部元素anyMatch, findFirst5.2 创建Stream的6种方式// 1. 从集合创建 ListString list Arrays.asList(a, b, c); StreamString stream1 list.stream(); // 2. 从数组创建 String[] array {a, b}; StreamString stream2 Arrays.stream(array); // 3. 使用Stream.of StreamInteger stream3 Stream.of(1, 2, 3); // 4. 生成无限流 StreamInteger iterateStream Stream.iterate(0, n - n 2); StreamDouble randomStream Stream.generate(Math::random); // 5. 空流 StreamString emptyStream Stream.empty(); // 6. 从文件创建 StreamString lines Files.lines(Paths.get(data.txt));5.3 常用中间操作1. 过滤与切片// 过滤偶数 ListInteger evenNumbers numbers.stream() .filter(n - n % 2 0) .collect(Collectors.toList()); // 去重 ListString distinctWords words.stream() .distinct() .collect(Collectors.toList()); // 分页查询 ListUser page users.stream() .skip(10) // 跳过前10条 .limit(5) // 取5条 .collect(Collectors.toList());2. 映射操作// 提取用户名 ListString names users.stream() .map(User::getName) .collect(Collectors.toList()); // 扁平化处理 ListString allTags articles.stream() .flatMap(article - article.getTags().stream()) .distinct() .collect(Collectors.toList()); // 转换为基本类型流 double total products.stream() .mapToDouble(Product::getPrice) .sum();3. 排序// 自然排序 ListString sortedNames names.stream() .sorted() .collect(Collectors.toList()); // 自定义排序 ListUser sortedUsers users.stream() .sorted(Comparator.comparing(User::getAge) .thenComparing(User::getName)) .collect(Collectors.toList()); // 逆序 ListInteger reversed numbers.stream() .sorted(Comparator.reverseOrder()) .collect(Collectors.toList());5.4 常用终端操作1. 匹配与查找// 检查是否有管理员 boolean hasAdmin users.stream() .anyMatch(user - user.isAdmin()); // 查找第一个VIP用户 OptionalUser firstVip users.stream() .filter(User::isVip) .findFirst(); // 检查所有用户都激活 boolean allActive users.stream() .allMatch(User::isActive);2. 归约操作// 求和 int sum numbers.stream() .reduce(0, Integer::sum); // 连接字符串 String concatenated strings.stream() .reduce(, String::concat); // 最大值 OptionalInteger max numbers.stream() .reduce(Integer::max);3. 收集器// 转换为List ListString nameList users.stream() .map(User::getName) .collect(Collectors.toList()); // 转换为Set SetString uniqueNames users.stream() .map(User::getName) .collect(Collectors.toSet()); // 转换为Map MapLong, User userMap users.stream() .collect(Collectors.toMap(User::getId, Function.identity())); // 分组 MapDepartment, ListUser byDept users.stream() .collect(Collectors.groupingBy(User::getDepartment)); // 分区 MapBoolean, ListUser partitioned users.stream() .collect(Collectors.partitioningBy(User::isActive)); // 统计 DoubleSummaryStatistics stats products.stream() .collect(Collectors.summarizingDouble(Product::getPrice));5.5 并行流注意事项ListInteger numbers /* 大数据量 */; // 使用并行流 int sum numbers.parallelStream() .mapToInt(Integer::intValue) .sum();最佳实践数据量足够大通常10,000才使用并行流避免共享可变状态考虑使用线程安全的收集器// 线程安全收集 ConcurrentMapDepartment, ListUser concurrentMap users.parallelStream() .collect(Collectors.groupingByConcurrent(User::getDepartment));5.6 调试技巧使用peek观察流处理过程ListInteger result Stream.of(1, 2, 3, 4, 5) .peek(n - System.out.println(原始: n)) .filter(n - n 2) .peek(n - System.out.println(过滤后: n)) .map(n - n * 2) .peek(n - System.out.println(映射后: n)) .collect(Collectors.toList());6. 项目实战经验总结在完成苍穹外卖项目的过程中我积累了一些宝贵的实战经验分享给大家6.1 定时任务优化经验任务执行时间选择避开业务高峰期如外卖午高峰11:00-13:00数据统计类任务放在凌晨执行短周期任务错开时间点不要所有任务都在整点执行长任务处理添加Async注解异步执行记录任务开始和结束时间大数据量分批处理Async Scheduled(cron 0 0 3 * * ?) public void processLargeData() { long start System.currentTimeMillis(); log.info(大数据处理任务开始); // 分批处理 int batchSize 1000; int total dataMapper.count(); for (int i 0; i total; i batchSize) { ListData batch dataMapper.findBatch(i, batchSize); processBatch(batch); } log.info(任务完成耗时{}ms, System.currentTimeMillis() - start); }6.2 WebSocket 性能优化连接管理限制单个IP的最大连接数实现心跳检测断开无效连接消息压缩大消息使用gzip压缩二进制协议比JSON更高效集群支持使用Redis Pub/Sub实现跨节点消息广播会话信息存储到Redis共享// Redis消息监听器 Component public class RedisMessageListener { Autowired private WebSocketServer webSocketServer; RedisListener(channel websocket:notice) public void onMessage(String message) { webSocketServer.broadcast(message); } }6.3 MyBatis 优化建议批量操作使用foreach实现批量插入开启rewriteBatchedStatements提升性能insert idbatchInsert INSERT INTO user(name, age) VALUES foreach collectionlist itemuser separator, (#{user.name}, #{user.age}) /foreach /insert二级缓存谨慎使用容易导致脏读适合读多写少的场景TypeHandler自定义复杂类型的处理如JSON字段、枚举转换等6.4 日期处理经验统一时区后端使用UTC时间存储前端根据用户时区显示API设计接收字符串参数ISO格式返回格式化的本地时间字符串GetMapping(/orders) public ListOrder getOrders( RequestParam DateTimeFormat(iso ISO.DATE_TIME) LocalDateTime start, RequestParam DateTimeFormat(iso ISO.DATE_TIME) LocalDateTime end) { // 业务逻辑 }数据库索引为常用的时间查询字段添加索引避免在时间字段上使用函数6.5 Stream 使用建议避免过度使用简单操作直接使用循环复杂数据处理使用Stream性能敏感场景使用基本类型流IntStream等避免在循环中创建Stream可读性平衡过长的链式调用考虑拆分为复杂操作添加注释// 好的示例清晰表达数据处理流程 ListString activeUserNames users.stream() .filter(User::isActive) // 过滤活跃用户 .sorted(comparing(User::getAge)) // 按年龄排序 .map(User::getName) // 提取姓名 .collect(toList()); // 收集结果7. 常见问题解决方案在实际开发中我遇到并解决了一些典型问题以下是记录和解决方案7.1 定时任务不执行问题现象Scheduled方法没有被调用没有错误日志排查步骤检查是否添加了EnableScheduling确认任务类被Spring管理有Component等注解检查Cron表达式格式是否正确查看是否有未处理的异常导致线程终止解决方案SpringBootApplication EnableScheduling // 确保添加此注解 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }7.2 WebSocket 连接不稳定问题现象连接频繁断开消息偶尔丢失解决方案实现心跳机制保持连接添加自动重连逻辑网络抖动处理// 前端重连逻辑 let reconnectInterval 1000; const maxReconnectInterval 30000; function connect() { const ws new WebSocket(url); ws.onclose function() { setTimeout(connect, reconnectInterval); reconnectInterval Math.min(reconnectInterval * 2, maxReconnectInterval); }; ws.onopen function() { reconnectInterval 1000; // 重置间隔 }; }7.3 MyBatis 动态SQL错误问题现象XML解析错误条件判断不生效常见原因特殊符号未转义条件判断错误参数类型不匹配解决方案!-- 使用CDATA包裹复杂SQL -- select idcomplexQuery ![CDATA[ SELECT * FROM table WHERE create_time #{start} AND (status 1 OR is_vip 1) ]] /select7.4 日期计算错误问题现象跨月/跨年计算错误时区转换不正确解决方案// 正确的日期范围计算 public static ListLocalDate getDateRange(LocalDate start, LocalDate end) { ListLocalDate dates new ArrayList(); LocalDate current start; while (!current.isAfter(end)) { dates.add(current); current current.plusDays(1); } return dates; }7.5 Stream 性能问题问题现象大数据量处理慢内存占用高优化方案使用基本类型流并行处理尽早过滤数据// 优化后的流操作 double total bigDataList.parallelStream() .filter(item - item.isValid()) // 先过滤 .mapToDouble(Item::getValue) // 避免装箱 .sum();8. 项目架构思考在完成苍穹外卖项目后我对后端架构有了更深的理解分享一些架构层面的思考8.1 分层设计优化传统三层架构Controller - Service - Mapper改进方向添加DTO层隔离实体和接口引入Manager层处理复杂业务组合明确各层职责边界8.2 模块化拆分按业务功能拆分- order-service - user-service - payment-service - delivery-service共用组件认证中心消息中心文件服务8.3 技术选型考量WebSocket扩展方案原生WebSocket轻量级适合简单场景SockJS兼容性更好支持降级STOMP协议更完善适合复杂场景定时任务方案对比方案优点缺点适用场景Scheduled简单易用功能简单不支持分布式单机简单任务Quartz功能强大配置复杂复杂调度需求XXL-JOB分布式支持有管理界面需要额外部署企业级调度系统Elastic-Job分布式弹性调度学习成本高大规模分布式任务8.4 性能优化方向数据库优化读写分离分库分表缓存策略接口优化异步处理结果缓存数据压缩JVM优化合理设置堆大小GC调优线程池配置8.5 监控与告警必备监控项WebSocket连接数定时任务执行情况接口响应时间系统资源使用率实现方案// 使用Micrometer暴露指标 Bean public MeterRegistryCustomizerPrometheusMeterRegistry metricsCommonTags() { return registry - registry.config().commonTags(application, takeaway-service); } // 定时任务监控示例 Scheduled(cron 0 0 * * * ?) public void monitoredTask() { Timer.Sample sample Timer.start(registry); try { // 业务逻辑 } finally { sample.stop(registry.timer(scheduled.task, name, hourlyJob)); } }9. 代码质量保障在项目开发过程中我特别注重代码质量的保障以下是一些实践9.1 单元测试策略WebSocket测试SpringBootTest WebAppConfiguration public class WebSocketTest { Autowired private ServerEndpointExporter exporter; Test public void testOnOpen() throws Exception { // 模拟WebSocket会话 Session session new TestSession(); WebSocketServer endpoint new WebSocketServer(); // 测试连接建立 endpoint.onOpen(session, testUser); assertTrue(WebSocketServer.containsUser(testUser)); } }定时任务测试SpringBootTest public class ScheduledTaskTest { Autowired private OrderTask orderTask; MockBean private OrderMapper orderMapper; Test public void testDeliveryTimeout() { // 准备测试数据 Order order new Order(); order.setStatus(OrderStatus.DELIVERY_IN_PROGRESS); when(orderMapper.findByStatusAndOrderTimeBefore( any(), any())) .thenReturn(Collections.singletonList(order)); // 执行定时任务 orderTask.processDeliveryTimeout(); // 验证状态更新 assertEquals(OrderStatus.COMPLETED, order.getStatus()); verify(orderMapper).update(order); } }9.2 代码审查要点定时任务是否处理了异常是否有必要的日志