From e6990cadee7c7046d02fb5bf3def8365d8549136 Mon Sep 17 00:00:00 2001 From: 3y Date: Sun, 2 Nov 2025 11:21:02 +0800 Subject: [PATCH] =?UTF-8?q?1=EF=BC=8C=E5=BC=80=E5=90=AFspringBoot=20?= =?UTF-8?q?=E8=99=9A=E6=8B=9F=E7=BA=BF=E7=A8=8B=202=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E9=83=A8=E5=88=86jdk=E6=96=B0=E7=89=B9=E6=80=A7?= =?UTF-8?q?=EF=BC=88var=E7=B1=BB=E5=9E=8B=E8=87=AA=E5=8A=A8=E6=8E=A8?= =?UTF-8?q?=E6=96=AD/=E7=AE=AD=E5=A4=B4=E6=9E=9A=E4=B8=BE/=E9=9B=86?= =?UTF-8?q?=E5=90=88Of=E7=AD=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/action/SendMessageAction.java | 43 +- .../handler/action/SensWordsAction.java | 125 +++--- .../limit/SimpleLimitService.java | 28 +- .../limit/SlideWindowLimitService.java | 17 +- .../handler/receiver/kafka/Receiver.java | 31 +- .../handler/receiver/redis/RedisReceiver.java | 8 +- .../SpringEventBusReceiverListener.java | 4 +- .../impl/action/send/SendAssembleAction.java | 20 +- .../support/config/OkHttpConfiguration.java | 8 +- .../austin/support/utils/AccountUtils.java | 23 +- .../support/utils/ConcurrentHashMapUtils.java | 43 -- .../web/exception/ExceptionHandlerAdvice.java | 15 +- .../web/service/impl/DataServiceImpl.java | 6 +- .../src/main/resources/application.properties | 4 + doc/CODE_OPTIMIZATION_RECOMMENDATIONS.md | 407 ++++++++++++++++++ 15 files changed, 584 insertions(+), 198 deletions(-) delete mode 100644 austin-support/src/main/java/com/java3y/austin/support/utils/ConcurrentHashMapUtils.java create mode 100644 doc/CODE_OPTIMIZATION_RECOMMENDATIONS.md diff --git a/austin-handler/src/main/java/com/java3y/austin/handler/action/SendMessageAction.java b/austin-handler/src/main/java/com/java3y/austin/handler/action/SendMessageAction.java index 2b1a6ab..2c649fd 100644 --- a/austin-handler/src/main/java/com/java3y/austin/handler/action/SendMessageAction.java +++ b/austin-handler/src/main/java/com/java3y/austin/handler/action/SendMessageAction.java @@ -1,5 +1,6 @@ package com.java3y.austin.handler.action; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import com.google.common.collect.Sets; import com.java3y.austin.common.domain.TaskInfo; @@ -7,34 +8,58 @@ import com.java3y.austin.common.enums.ChannelType; import com.java3y.austin.common.pipeline.BusinessProcess; import com.java3y.austin.common.pipeline.ProcessContext; import com.java3y.austin.handler.handler.HandlerHolder; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.Set; + /** * 发送消息,路由到对应的渠道下发消息 * * @author 3y */ +@Slf4j @Service public class SendMessageAction implements BusinessProcess { - @Autowired - private HandlerHolder handlerHolder; + + private final HandlerHolder handlerHolder; + + public SendMessageAction(HandlerHolder handlerHolder) { + this.handlerHolder = handlerHolder; + } @Override public void process(ProcessContext context) { TaskInfo taskInfo = context.getProcessModel(); + Integer sendChannel = taskInfo.getSendChannel(); + + // 参数校验 + if (sendChannel == null) { + log.warn("Send channel is null, taskInfo: {}", taskInfo); + return; + } + + Set receivers = taskInfo.getReceiver(); + if (CollUtil.isEmpty(receivers)) { + log.warn("Receivers is empty, taskInfo: {}", taskInfo); + return; + } // 微信小程序&服务号只支持单人推送,为了后续逻辑统一处理,于是在这做了单发处理 - if (ChannelType.MINI_PROGRAM.getCode().equals(taskInfo.getSendChannel()) - || ChannelType.OFFICIAL_ACCOUNT.getCode().equals(taskInfo.getSendChannel()) - || ChannelType.ALIPAY_MINI_PROGRAM.getCode().equals(taskInfo.getSendChannel())) { + Set singleReceiverChannels = Set.of( + ChannelType.MINI_PROGRAM.getCode(), + ChannelType.OFFICIAL_ACCOUNT.getCode(), + ChannelType.ALIPAY_MINI_PROGRAM.getCode() + ); + + if (singleReceiverChannels.contains(sendChannel)) { TaskInfo taskClone = ObjectUtil.cloneByStream(taskInfo); - for (String receiver : taskInfo.getReceiver()) { + for (String receiver : receivers) { taskClone.setReceiver(Sets.newHashSet(receiver)); - handlerHolder.route(taskInfo.getSendChannel()).doHandler(taskClone); + handlerHolder.route(sendChannel).doHandler(taskClone); } return; } - handlerHolder.route(taskInfo.getSendChannel()).doHandler(taskInfo); + handlerHolder.route(sendChannel).doHandler(taskInfo); } } diff --git a/austin-handler/src/main/java/com/java3y/austin/handler/action/SensWordsAction.java b/austin-handler/src/main/java/com/java3y/austin/handler/action/SensWordsAction.java index bf825e6..e4c2aba 100644 --- a/austin-handler/src/main/java/com/java3y/austin/handler/action/SensWordsAction.java +++ b/austin-handler/src/main/java/com/java3y/austin/handler/action/SensWordsAction.java @@ -34,80 +34,59 @@ public class SensWordsAction implements BusinessProcess { */ @Override public void process(ProcessContext context) { - // 获取敏感词典 Set sensDict = Optional.ofNullable(redisTemplate.opsForSet().members(SensitiveWordsConfig.SENS_WORDS_DICT)) .orElse(Collections.emptySet()); - // 如果敏感词典为空,不过滤 if (ObjectUtils.isEmpty(sensDict)) { return; } + + ContentModel contentModel = context.getProcessModel().getContentModel(); switch (context.getProcessModel().getMsgType()) { - // IM - case 10: - // 无文本内容,暂不做过滤处理 - break; - // PUSH - case 20: - PushContentModel pushContentModel = - (PushContentModel) context.getProcessModel().getContentModel(); - pushContentModel.setContent(filter(pushContentModel.getContent(), sensDict)); - break; - // SMS - case 30: - SmsContentModel smsContentModel = - (SmsContentModel) context.getProcessModel().getContentModel(); - smsContentModel.setContent(filter(smsContentModel.getContent(), sensDict)); - break; - // EMAIL - case 40: - EmailContentModel emailContentModel = - (EmailContentModel) context.getProcessModel().getContentModel(); - emailContentModel.setContent(filter(emailContentModel.getContent(), sensDict)); - break; - // OFFICIAL_ACCOUNT - case 50: - // 无文本内容,暂不做过滤处理 - break; - // MINI_PROGRAM - case 60: - // 无文本内容,暂不做过滤处理 - break; - // ENTERPRISE_WE_CHAT - case 70: - EnterpriseWeChatContentModel enterpriseWeChatContentModel = - (EnterpriseWeChatContentModel) context.getProcessModel().getContentModel(); - enterpriseWeChatContentModel.setContent(filter(enterpriseWeChatContentModel.getContent(), sensDict)); - break; - // DING_DING_ROBOT - case 80: - DingDingRobotContentModel dingDingRobotContentModel = - (DingDingRobotContentModel) context.getProcessModel().getContentModel(); - dingDingRobotContentModel.setContent(filter(dingDingRobotContentModel.getContent(), sensDict)); - break; - // DING_DING_WORK_NOTICE - case 90: - DingDingWorkContentModel dingDingWorkContentModel = - (DingDingWorkContentModel) context.getProcessModel().getContentModel(); - dingDingWorkContentModel.setContent(filter(dingDingWorkContentModel.getContent(), sensDict)); - break; - // ENTERPRISE_WE_CHAT_ROBOT - case 100: - EnterpriseWeChatRobotContentModel enterpriseWeChatRobotContentModel = - (EnterpriseWeChatRobotContentModel) context.getProcessModel().getContentModel(); - enterpriseWeChatRobotContentModel.setContent(filter(enterpriseWeChatRobotContentModel.getContent(), sensDict)); - break; - // FEI_SHU_ROBOT - case 110: - FeiShuRobotContentModel feiShuRobotContentModel = - (FeiShuRobotContentModel) context.getProcessModel().getContentModel(); - feiShuRobotContentModel.setContent(filter(feiShuRobotContentModel.getContent(), sensDict)); - break; - // ALIPAY_MINI_PROGRAM - case 120: - // 无文本内容,暂不做过滤处理 - break; - default: - break; + case 10, 50, 60, 120 -> { + // IM, OFFICIAL_ACCOUNT, MINI_PROGRAM, ALIPAY_MINI_PROGRAM: 无文本内容,暂不做过滤处理 + } + case 20 -> { + if (contentModel instanceof PushContentModel pushContentModel) { + pushContentModel.setContent(filter(pushContentModel.getContent(), sensDict)); + } + } + case 30 -> { + if (contentModel instanceof SmsContentModel smsContentModel) { + smsContentModel.setContent(filter(smsContentModel.getContent(), sensDict)); + } + } + case 40 -> { + if (contentModel instanceof EmailContentModel emailContentModel) { + emailContentModel.setContent(filter(emailContentModel.getContent(), sensDict)); + } + } + case 70 -> { + if (contentModel instanceof EnterpriseWeChatContentModel enterpriseWeChatContentModel) { + enterpriseWeChatContentModel.setContent(filter(enterpriseWeChatContentModel.getContent(), sensDict)); + } + } + case 80 -> { + if (contentModel instanceof DingDingRobotContentModel dingDingRobotContentModel) { + dingDingRobotContentModel.setContent(filter(dingDingRobotContentModel.getContent(), sensDict)); + } + } + case 90 -> { + if (contentModel instanceof DingDingWorkContentModel dingDingWorkContentModel) { + dingDingWorkContentModel.setContent(filter(dingDingWorkContentModel.getContent(), sensDict)); + } + } + case 100 -> { + if (contentModel instanceof EnterpriseWeChatRobotContentModel enterpriseWeChatRobotContentModel) { + enterpriseWeChatRobotContentModel.setContent(filter(enterpriseWeChatRobotContentModel.getContent(), sensDict)); + } + } + case 110 -> { + if (contentModel instanceof FeiShuRobotContentModel feiShuRobotContentModel) { + feiShuRobotContentModel.setContent(filter(feiShuRobotContentModel.getContent(), sensDict)); + } + } + default -> { + } } } @@ -163,11 +142,11 @@ public class SensWordsAction implements BusinessProcess { * @return */ private TrieNode buildTrie(Set sensDict) { - TrieNode root = new TrieNode(); - for (String word : sensDict) { - TrieNode node = root; - for (char c : word.toCharArray()) { - node = node.children.computeIfAbsent(c, k -> new TrieNode()); + var root = new TrieNode(); + for (var word : sensDict) { + var node = root; + for (var c : word.toCharArray()) { + node = node.children.computeIfAbsent(c, _ -> new TrieNode()); } node.isEnd = true; } diff --git a/austin-handler/src/main/java/com/java3y/austin/handler/deduplication/limit/SimpleLimitService.java b/austin-handler/src/main/java/com/java3y/austin/handler/deduplication/limit/SimpleLimitService.java index 0b152ab..05ec770 100644 --- a/austin-handler/src/main/java/com/java3y/austin/handler/deduplication/limit/SimpleLimitService.java +++ b/austin-handler/src/main/java/com/java3y/austin/handler/deduplication/limit/SimpleLimitService.java @@ -29,12 +29,12 @@ public class SimpleLimitService extends AbstractLimitService { @Override public Set limitFilter(AbstractDeduplicationService service, TaskInfo taskInfo, DeduplicationParam param) { - Set filterReceiver = new HashSet<>(taskInfo.getReceiver().size()); + var filterReceiver = new HashSet(taskInfo.getReceiver().size()); // 获取redis记录 - Map readyPutRedisReceiver = new HashMap<>(taskInfo.getReceiver().size()); + var readyPutRedisReceiver = new HashMap(taskInfo.getReceiver().size()); //redis数据隔离 - List keys = deduplicationAllKey(service, taskInfo).stream().map(key -> LIMIT_TAG + key).collect(Collectors.toList()); - Map inRedisValue = redisUtils.mGet(keys); + var keys = deduplicationAllKey(service, taskInfo).stream().map(key -> LIMIT_TAG + key).collect(Collectors.toList()); + var inRedisValue = redisUtils.mGet(keys); for (String receiver : taskInfo.getReceiver()) { String key = LIMIT_TAG + deduplicationSingleKey(service, taskInfo, receiver); @@ -62,11 +62,21 @@ public class SimpleLimitService extends AbstractLimitService { */ private void putInRedis(Map readyPutRedisReceiver, Map inRedisValue, Long deduplicationTime) { - Map keyValues = new HashMap<>(readyPutRedisReceiver.size()); - for (Map.Entry entry : readyPutRedisReceiver.entrySet()) { - String key = entry.getValue(); - if (Objects.nonNull(inRedisValue.get(key))) { - keyValues.put(key, String.valueOf(Integer.parseInt(inRedisValue.get(key)) + 1)); + var keyValues = new HashMap(readyPutRedisReceiver.size()); + for (var entry : readyPutRedisReceiver.entrySet()) { + var key = entry.getValue(); + var existingValue = inRedisValue.get(key); + if (Objects.nonNull(existingValue)) { + try { + long currentCount = Long.parseLong(existingValue); + long newCount = currentCount + 1; + if (newCount < currentCount) { + newCount = Long.MAX_VALUE; + } + keyValues.put(key, String.valueOf(newCount)); + } catch (NumberFormatException e) { + keyValues.put(key, String.valueOf(CommonConstant.TRUE)); + } } else { keyValues.put(key, String.valueOf(CommonConstant.TRUE)); } diff --git a/austin-handler/src/main/java/com/java3y/austin/handler/deduplication/limit/SlideWindowLimitService.java b/austin-handler/src/main/java/com/java3y/austin/handler/deduplication/limit/SlideWindowLimitService.java index 5c90e1b..ff963b4 100644 --- a/austin-handler/src/main/java/com/java3y/austin/handler/deduplication/limit/SlideWindowLimitService.java +++ b/austin-handler/src/main/java/com/java3y/austin/handler/deduplication/limit/SlideWindowLimitService.java @@ -51,15 +51,14 @@ public class SlideWindowLimitService extends AbstractLimitService { */ @Override public Set limitFilter(AbstractDeduplicationService service, TaskInfo taskInfo, DeduplicationParam param) { - - Set filterReceiver = new HashSet<>(taskInfo.getReceiver().size()); - long nowTime = System.currentTimeMillis(); - for (String receiver : taskInfo.getReceiver()) { - String key = LIMIT_TAG + deduplicationSingleKey(service, taskInfo, receiver); - String scoreValue = String.valueOf(IdUtil.getSnowflake().nextId()); - String score = String.valueOf(nowTime); - - final Boolean result = redisUtils.execLimitLua(redisScript, Collections.singletonList(key), + var filterReceiver = new HashSet(taskInfo.getReceiver().size()); + var nowTime = System.currentTimeMillis(); + for (var receiver : taskInfo.getReceiver()) { + var key = LIMIT_TAG + deduplicationSingleKey(service, taskInfo, receiver); + var scoreValue = String.valueOf(IdUtil.getSnowflake().nextId()); + var score = String.valueOf(nowTime); + + var result = redisUtils.execLimitLua(redisScript, Collections.singletonList(key), String.valueOf(param.getDeduplicationTime() * 1000), score, String.valueOf(param.getCountNum()), scoreValue); if (Boolean.TRUE.equals(result)) { filterReceiver.add(receiver); diff --git a/austin-handler/src/main/java/com/java3y/austin/handler/receiver/kafka/Receiver.java b/austin-handler/src/main/java/com/java3y/austin/handler/receiver/kafka/Receiver.java index a1ba275..a8da1ec 100644 --- a/austin-handler/src/main/java/com/java3y/austin/handler/receiver/kafka/Receiver.java +++ b/austin-handler/src/main/java/com/java3y/austin/handler/receiver/kafka/Receiver.java @@ -19,7 +19,6 @@ import org.springframework.kafka.support.KafkaHeaders; import org.springframework.messaging.handler.annotation.Header; import org.springframework.stereotype.Component; -import java.util.List; import java.util.Optional; /** @@ -42,18 +41,17 @@ public class Receiver implements MessageReceiver { */ @KafkaListener(topics = "#{'${austin.business.topic.name}'}", containerFactory = "filterContainerFactory") public void consumer(ConsumerRecord consumerRecord, @Header(KafkaHeaders.GROUP_ID) String topicGroupId) { - Optional kafkaMessage = Optional.ofNullable(consumerRecord.value()); - if (kafkaMessage.isPresent()) { - - List taskInfoLists = JSON.parseArray(kafkaMessage.get(), TaskInfo.class); - String messageGroupId = GroupIdMappingUtils.getGroupIdByTaskInfo(CollUtil.getFirst(taskInfoLists.iterator())); - /** - * 每个消费者组 只消费 他们自身关心的消息 - */ - if (topicGroupId.equals(messageGroupId)) { - consumeService.consume2Send(taskInfoLists); - } - } + Optional.ofNullable(consumerRecord.value()).ifPresent( + message -> { + // 使用 var 类型推断简化代码(JDK 10+) + var taskInfoLists = JSON.parseArray(message, TaskInfo.class); + var messageGroupId = GroupIdMappingUtils.getGroupIdByTaskInfo(CollUtil.getFirst(taskInfoLists.iterator())); + // 每个消费者组 只消费 他们自身关心的消息 + if (topicGroupId.equals(messageGroupId)) { + consumeService.consume2Send(taskInfoLists); + } + } + ); } /** @@ -63,10 +61,9 @@ public class Receiver implements MessageReceiver { */ @KafkaListener(topics = "#{'${austin.business.recall.topic.name}'}", groupId = "#{'${austin.business.recall.group.name}'}", containerFactory = "filterContainerFactory") public void recall(ConsumerRecord consumerRecord) { - Optional kafkaMessage = Optional.ofNullable(consumerRecord.value()); - if (kafkaMessage.isPresent()) { - RecallTaskInfo recallTaskInfo = JSON.parseObject(kafkaMessage.get(), RecallTaskInfo.class); + Optional.ofNullable(consumerRecord.value()).ifPresent(message -> { + var recallTaskInfo = JSON.parseObject(message, RecallTaskInfo.class); consumeService.consume2recall(recallTaskInfo); - } + }); } } diff --git a/austin-handler/src/main/java/com/java3y/austin/handler/receiver/redis/RedisReceiver.java b/austin-handler/src/main/java/com/java3y/austin/handler/receiver/redis/RedisReceiver.java index 88bbeaf..96ed82f 100644 --- a/austin-handler/src/main/java/com/java3y/austin/handler/receiver/redis/RedisReceiver.java +++ b/austin-handler/src/main/java/com/java3y/austin/handler/receiver/redis/RedisReceiver.java @@ -104,12 +104,12 @@ public class RedisReceiver implements MessageReceiver { stringRedisTemplate.opsForList().rightPop(topic, 20, TimeUnit.SECONDS)); message.ifPresent(consumer); } catch (Exception e) { - log.error("RedisReceiver#receiveMessage Error receiving messages from Redis topic {}: {}", - topic, e.getMessage()); + log.error("RedisReceiver#receiveMessage Error receiving messages from Redis topic {}", + topic, e); try { TimeUnit.SECONDS.sleep(10); - } catch (InterruptedException ex) { - log.error("RedisReceiver#receiveMessage interrupted: {}", e.getMessage()); + } catch (InterruptedException interruptedEx) { + log.error("RedisReceiver#receiveMessage interrupted during retry wait", interruptedEx); Thread.currentThread().interrupt(); break; } diff --git a/austin-handler/src/main/java/com/java3y/austin/handler/receiver/springeventbus/SpringEventBusReceiverListener.java b/austin-handler/src/main/java/com/java3y/austin/handler/receiver/springeventbus/SpringEventBusReceiverListener.java index 06cc2d8..e0fbc29 100644 --- a/austin-handler/src/main/java/com/java3y/austin/handler/receiver/springeventbus/SpringEventBusReceiverListener.java +++ b/austin-handler/src/main/java/com/java3y/austin/handler/receiver/springeventbus/SpringEventBusReceiverListener.java @@ -34,9 +34,9 @@ public class SpringEventBusReceiverListener implements ApplicationListener { Long messageTemplateId = sendTaskModel.getMessageTemplateId(); try { - Optional messageTemplate = messageTemplateDao.findById(messageTemplateId); - if (!messageTemplate.isPresent() || messageTemplate.get().getIsDeleted().equals(CommonConstant.TRUE)) { + var messageTemplateOptional = messageTemplateDao.findById(messageTemplateId); + if (messageTemplateOptional.isEmpty()) { context.setNeedBreak(true).setResponse(BasicResultVO.fail(RespStatusEnum.TEMPLATE_NOT_FOUND)); return; } - List taskInfos = assembleTaskInfo(sendTaskModel, messageTemplate.get()); + + var messageTemplate = messageTemplateOptional.get(); + if (messageTemplate.getIsDeleted().equals(CommonConstant.TRUE)) { + context.setNeedBreak(true).setResponse(BasicResultVO.fail(RespStatusEnum.TEMPLATE_NOT_FOUND)); + return; + } + + List taskInfos = assembleTaskInfo(sendTaskModel, messageTemplate); sendTaskModel.setTaskInfo(taskInfos); } catch (Exception e) { context.setNeedBreak(true).setResponse(BasicResultVO.fail(RespStatusEnum.SERVICE_ERROR)); - log.error("assemble task fail! templateId:{}, e:{}", messageTemplateId, Throwables.getStackTraceAsString(e)); + log.error("assemble task fail! templateId:{}", messageTemplateId, e); } } @@ -115,7 +122,8 @@ public class SendAssembleAction implements BusinessProcess { .bizId(messageParam.getBizId()) .messageTemplateId(messageTemplate.getId()) .businessId(TaskInfoUtils.generateBusinessId(messageTemplate.getId(), messageTemplate.getTemplateType())) - .receiver(new HashSet<>(Arrays.asList(messageParam.getReceiver().split(String.valueOf(StrPool.C_COMMA))))) + .receiver(Arrays.stream(messageParam.getReceiver().split(String.valueOf(StrPool.C_COMMA))) + .collect(Collectors.toSet())) .idType(messageTemplate.getIdType()) .sendChannel(messageTemplate.getSendChannel()) .templateType(messageTemplate.getTemplateType()) diff --git a/austin-support/src/main/java/com/java3y/austin/support/config/OkHttpConfiguration.java b/austin-support/src/main/java/com/java3y/austin/support/config/OkHttpConfiguration.java index 7437288..a6c586b 100644 --- a/austin-support/src/main/java/com/java3y/austin/support/config/OkHttpConfiguration.java +++ b/austin-support/src/main/java/com/java3y/austin/support/config/OkHttpConfiguration.java @@ -1,6 +1,7 @@ package com.java3y.austin.support.config; +import lombok.extern.slf4j.Slf4j; import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import org.springframework.beans.factory.annotation.Value; @@ -23,6 +24,7 @@ import java.util.concurrent.TimeUnit; * @author 3y * @date 2021/11/4 */ +@Slf4j @Configuration public class OkHttpConfiguration { @@ -50,7 +52,7 @@ public class OkHttpConfiguration { .connectTimeout(connectTimeout, TimeUnit.SECONDS) .readTimeout(readTimeout, TimeUnit.SECONDS) .writeTimeout(writeTimeout, TimeUnit.SECONDS) - .hostnameVerifier((hostname, session) -> true) + .hostnameVerifier((_, __) -> true) .build(); } @@ -82,9 +84,9 @@ public class OkHttpConfiguration { sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom()); return sslContext.getSocketFactory(); } catch (NoSuchAlgorithmException | KeyManagementException e) { - e.printStackTrace(); + log.error("Failed to create SSL socket factory", e); + throw new IllegalStateException("Failed to initialize SSL context", e); } - return null; } @Bean diff --git a/austin-support/src/main/java/com/java3y/austin/support/utils/AccountUtils.java b/austin-support/src/main/java/com/java3y/austin/support/utils/AccountUtils.java index 106027d..63b2839 100644 --- a/austin-support/src/main/java/com/java3y/austin/support/utils/AccountUtils.java +++ b/austin-support/src/main/java/com/java3y/austin/support/utils/AccountUtils.java @@ -66,21 +66,20 @@ public class AccountUtils { @SuppressWarnings("unchecked") public T getAccountById(Integer sendAccountId, Class clazz) { try { - Optional optionalChannelAccount = channelAccountDao.findById(Long.valueOf(sendAccountId)); - if (optionalChannelAccount.isPresent()) { - ChannelAccount channelAccount = optionalChannelAccount.get(); - if (clazz.equals(WxMaService.class)) { - return (T) ConcurrentHashMapUtils.computeIfAbsent(miniProgramServiceMap, channelAccount, account -> initMiniProgramService(JSON.parseObject(account.getAccountConfig(), WeChatMiniProgramAccount.class))); - } else if (clazz.equals(WxMpService.class)) { - return (T) ConcurrentHashMapUtils.computeIfAbsent(officialAccountServiceMap, channelAccount, account -> initOfficialAccountService(JSON.parseObject(account.getAccountConfig(), WeChatOfficialAccount.class))); - } else { - return JSON.parseObject(channelAccount.getAccountConfig(), clazz); - } - } + return channelAccountDao.findById(Long.valueOf(sendAccountId)) + .map(channelAccount -> { + if (WxMaService.class.equals(clazz)) { + return (T) miniProgramServiceMap.computeIfAbsent(channelAccount, account -> initMiniProgramService(JSON.parseObject(account.getAccountConfig(), WeChatMiniProgramAccount.class))); + } else if (WxMpService.class.equals(clazz)) { + return (T) officialAccountServiceMap.computeIfAbsent(channelAccount, account -> initOfficialAccountService(JSON.parseObject(account.getAccountConfig(), WeChatOfficialAccount.class))); + } + return JSON.parseObject(channelAccount.getAccountConfig(), clazz); + }) + .orElse(null); } catch (Exception e) { log.error("AccountUtils#getAccount fail! e:{}", Throwables.getStackTraceAsString(e)); + return null; } - return null; } /** diff --git a/austin-support/src/main/java/com/java3y/austin/support/utils/ConcurrentHashMapUtils.java b/austin-support/src/main/java/com/java3y/austin/support/utils/ConcurrentHashMapUtils.java deleted file mode 100644 index 917e4a8..0000000 --- a/austin-support/src/main/java/com/java3y/austin/support/utils/ConcurrentHashMapUtils.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.java3y.austin.support.utils; - -import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; - -/** - * @author kl - * @version 1.0.0 - * @description ConcurrentHashMap util - * @date 2023/2/6 10:01 - */ -public class ConcurrentHashMapUtils { - private static boolean IS_JAVA8; - - static { - try { - IS_JAVA8 = System.getProperty("java.version").startsWith("1.8."); - } catch (Exception ignore) { - // exception is ignored - IS_JAVA8 = true; - } - } - - private ConcurrentHashMapUtils() { - } - - /** - * Java 8 ConcurrentHashMap#computeIfAbsent 存在性能问题的临时解决方案 - * - * @see https://bugs.openjdk.java.net/browse/JDK-8161372 - */ - public static V computeIfAbsent(ConcurrentMap map, K key, Function func) { - if (IS_JAVA8) { - V v = map.get(key); - if (null == v) { - v = map.computeIfAbsent(key, func); - } - return v; - } else { - return map.computeIfAbsent(key, func); - } - } -} diff --git a/austin-web/src/main/java/com/java3y/austin/web/exception/ExceptionHandlerAdvice.java b/austin-web/src/main/java/com/java3y/austin/web/exception/ExceptionHandlerAdvice.java index e952876..54dbd5f 100644 --- a/austin-web/src/main/java/com/java3y/austin/web/exception/ExceptionHandlerAdvice.java +++ b/austin-web/src/main/java/com/java3y/austin/web/exception/ExceptionHandlerAdvice.java @@ -1,10 +1,9 @@ package com.java3y.austin.web.exception; +import com.google.common.base.Throwables; import com.java3y.austin.common.enums.RespStatusEnum; import com.java3y.austin.common.vo.BasicResultVO; -import org.assertj.core.util.Throwables; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -17,24 +16,24 @@ import org.springframework.web.bind.annotation.ResponseStatus; * @description 拦截异常统一返回 * @date 2023/2/9 19:03 */ +@Slf4j @ControllerAdvice(basePackages = "com.java3y.austin.web.controller") @ResponseBody public class ExceptionHandlerAdvice { - private static final Logger log = LoggerFactory.getLogger(ExceptionHandlerAdvice.class); @ExceptionHandler({Exception.class}) @ResponseStatus(HttpStatus.OK) public BasicResultVO exceptionResponse(Exception e) { - String errStackStr = Throwables.getStackTrace(e); - log.error(errStackStr); - return BasicResultVO.fail(RespStatusEnum.ERROR_500, "\r\n" + errStackStr + "\r\n"); + String errStackStr = Throwables.getStackTraceAsString(e); + log.error("Unhandled exception occurred", e); + return BasicResultVO.fail(RespStatusEnum.ERROR_500, String.format("%n%s%n", errStackStr)); } @ExceptionHandler({CommonException.class}) @ResponseStatus(HttpStatus.OK) public BasicResultVO commonResponse(CommonException ce) { - log.error(Throwables.getStackTrace(ce)); + log.error("Common exception occurred: {}", ce.getMessage(), ce); return new BasicResultVO<>(ce.getCode(), ce.getMessage(), ce.getRespStatusEnum()); } } diff --git a/austin-web/src/main/java/com/java3y/austin/web/service/impl/DataServiceImpl.java b/austin-web/src/main/java/com/java3y/austin/web/service/impl/DataServiceImpl.java index 08eb276..acf5e5e 100644 --- a/austin-web/src/main/java/com/java3y/austin/web/service/impl/DataServiceImpl.java +++ b/austin-web/src/main/java/com/java3y/austin/web/service/impl/DataServiceImpl.java @@ -80,8 +80,8 @@ public class DataServiceImpl implements DataService { // 获取businessId并获取模板信息 businessId = getRealBusinessId(businessId); - Optional optional = messageTemplateDao.findById(TaskInfoUtils.getMessageTemplateIdFromBusinessId(Long.valueOf(businessId))); - if (!optional.isPresent()) { + var messageTemplateOptional = messageTemplateDao.findById(TaskInfoUtils.getMessageTemplateIdFromBusinessId(Long.valueOf(businessId))); + if (messageTemplateOptional.isEmpty()) { return null; } @@ -92,7 +92,7 @@ public class DataServiceImpl implements DataService { */ Map anchorResult = redisUtils.hGetAll(getRealBusinessId(businessId)); - return Convert4Amis.getEchartsVo(anchorResult, optional.get(), businessId); + return Convert4Amis.getEchartsVo(anchorResult, messageTemplateOptional.get(), businessId); } @Override diff --git a/austin-web/src/main/resources/application.properties b/austin-web/src/main/resources/application.properties index 45b9f32..033bce2 100644 --- a/austin-web/src/main/resources/application.properties +++ b/austin-web/src/main/resources/application.properties @@ -133,4 +133,8 @@ management.health.rabbit.enabled=false server.shutdown=graceful ########################################## system end ########################################## +########################################## virtual threads start (JDK 25 + Spring Boot 3.5.7) ########################################## +spring.threads.virtual.enabled=true +########################################## virtual threads end ########################################## + spring.main.allow-circular-references=true \ No newline at end of file diff --git a/doc/CODE_OPTIMIZATION_RECOMMENDATIONS.md b/doc/CODE_OPTIMIZATION_RECOMMENDATIONS.md new file mode 100644 index 0000000..a58dd6a --- /dev/null +++ b/doc/CODE_OPTIMIZATION_RECOMMENDATIONS.md @@ -0,0 +1,407 @@ +# 代码优化建议报告 (JDK 25 + Spring Boot 3.5.7) + +## 📋 概述 + +本报告基于 Context7 最新文档和代码审查,针对 JDK 25 + Spring Boot 3.5.7 环境,提供代码质量、可读性和潜在 Bug 的优化建议。 + +--- + +## 🔴 严重问题(需要立即修复) + +### 1. 异常处理不当 - `printStackTrace()` + +**文件**: `austin-support/src/main/java/com/java3y/austin/support/config/OkHttpConfiguration.java:85` + +**问题**: +- 使用 `e.printStackTrace()` 输出到控制台,不利于生产环境日志管理 +- 方法返回 `null`,可能导致 NPE + +**修复建议**: +```java +@Bean +public SSLSocketFactory sslSocketFactory() { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom()); + return sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + log.error("Failed to create SSL socket factory", e); + throw new IllegalStateException("Failed to initialize SSL context", e); + } +} +``` + +**影响**: +- ⚠️ 生产环境无法正确记录错误 +- ⚠️ 可能导致 Bean 创建失败,应用启动异常 + +--- + +### 2. 异常处理工具类混用 + +**文件**: `austin-web/src/main/java/com/java3y/austin/web/exception/ExceptionHandlerAdvice.java:5` + +**问题**: +- 使用了 `org.assertj.core.util.Throwables`(测试框架的工具类) +- 应该使用 `com.google.common.base.Throwables`(项目其他地方统一使用) + +**修复建议**: +```java +import com.google.common.base.Throwables; // 替换 org.assertj.core.util.Throwables +``` + +**影响**: +- ⚠️ 引入了测试依赖到生产代码 +- ⚠️ 可能导致依赖冲突 + +--- + +### 3. JDK 版本检测逻辑过时 + +**文件**: `austin-support/src/main/java/com/java3y/austin/support/utils/ConcurrentHashMapUtils.java:17` + +**问题**: +- 使用 `System.getProperty("java.version").startsWith("1.8.")` 检测 Java 8 +- JDK 25 环境下,版本格式为 `25` 或 `25.0.x`,此逻辑失效 + +**修复建议**: +```java +static { + try { + String version = System.getProperty("java.version"); + // JDK 9+ 格式: "9", "10", "11", "25" 等 + // JDK 8 格式: "1.8.0_xxx" + IS_JAVA8 = version.startsWith("1.8.") || version.startsWith("1.7."); + } catch (Exception ignore) { + IS_JAVA8 = false; // 默认假设不是 Java 8 + } +} +``` + +**或者直接移除该检查**(因为项目已升级到 JDK 25): +```java +public static V computeIfAbsent(ConcurrentMap map, K key, Function func) { + // JDK 9+ 已修复 computeIfAbsent 性能问题,直接使用 + return map.computeIfAbsent(key, func); +} +``` + +**影响**: +- ⚠️ 可能导致性能优化逻辑错误执行 +- ⚠️ 不必要的代码复杂性 + +--- + +## 🟡 中等优先级问题(建议修复) + +### 4. Null 安全检查不足 + +**文件**: `austin-handler/src/main/java/com/java3y/austin/handler/action/SendMessageAction.java:27` + +**问题**: +- `taskInfo.getSendChannel()` 可能返回 null +- `taskInfo.getReceiver()` 可能返回 null 或空集合 + +**修复建议**: +```java +@Override +public void process(ProcessContext context) { + TaskInfo taskInfo = context.getProcessModel(); + Integer sendChannel = taskInfo.getSendChannel(); + + if (sendChannel == null) { + log.warn("Send channel is null, taskInfo: {}", taskInfo); + return; + } + + Set receivers = taskInfo.getReceiver(); + if (CollUtil.isEmpty(receivers)) { + log.warn("Receivers is empty, taskInfo: {}", taskInfo); + return; + } + + // ... 后续逻辑 +} +``` + +--- + +### 5. 资源关闭可能异常 + +**文件**: `austin-handler/src/main/java/com/java3y/austin/handler/receiver/redis/RedisReceiver.java:112` + +**问题**: +- `InterruptedException` 被捕获后,在 catch 块中又捕获了新的异常,可能导致异常信息丢失 + +**修复建议**: +```java +} catch (InterruptedException ex) { + log.error("RedisReceiver#receiveMessage interrupted", ex); + Thread.currentThread().interrupt(); + break; +} +``` + +--- + +### 6. 类型转换未进行空值检查 + +**文件**: `austin-service-api-impl/src/main/java/com/java3y/austin/service/api/impl/action/send/SendAssembleAction.java:88` + +**问题**: +- `messageTemplate.get()` 可能返回 null(虽然已检查 isPresent,但后续使用仍需注意) + +**当前代码**: +```java +Optional messageTemplate = messageTemplateDao.findById(messageTemplateId); +if (!messageTemplate.isPresent() || messageTemplate.get().getIsDeleted().equals(CommonConstant.TRUE)) { + // ... +} +List taskInfos = assembleTaskInfo(sendTaskModel, messageTemplate.get()); +``` + +**建议**: 使用 `orElseThrow()` 更清晰: +```java +MessageTemplate template = messageTemplateDao.findById(messageTemplateId) + .orElseThrow(() -> new CommonException(RespStatusEnum.TEMPLATE_NOT_FOUND)); + +if (template.getIsDeleted().equals(CommonConstant.TRUE)) { + context.setNeedBreak(true).setResponse(BasicResultVO.fail(RespStatusEnum.TEMPLATE_NOT_FOUND)); + return; +} +List taskInfos = assembleTaskInfo(sendTaskModel, template); +``` + +--- + +## 🟢 代码质量和可读性优化 + +### 7. 使用构造函数注入替代字段注入 + +**问题**: +- 项目中大量使用 `@Autowired` 字段注入(151 处) +- 不符合 Spring Boot 最佳实践(推荐构造函数注入) + +**建议**: +- 优先使用构造函数注入,提高可测试性和依赖清晰度 +- 对于可选依赖,可以使用 `@Autowired(required = false)` 或 `Optional<>` + +**示例**: +```java +// 当前方式 +@Service +public class SendMessageAction { + @Autowired + private HandlerHolder handlerHolder; +} + +// 推荐方式 +@Service +public class SendMessageAction { + private final HandlerHolder handlerHolder; + + public SendMessageAction(HandlerHolder handlerHolder) { + this.handlerHolder = handlerHolder; + } +} +``` + +--- + +### 8. 使用 Lombok 简化代码 + +**已优化文件**: `ExceptionHandlerAdvice.java` 使用了 `@Slf4j`,但其他类仍使用 `LoggerFactory` + +**建议**: +- 统一使用 `@Slf4j` 注解 +- 使用 `@RequiredArgsConstructor` 替代手动构造函数注入 + +--- + +### 9. 字符串拼接优化 + +**文件**: `austin-web/src/main/java/com/java3y/austin/web/exception/ExceptionHandlerAdvice.java:31` + +**问题**: +- 使用 `"\r\n" + errStackStr + "\r\n"` 字符串拼接 + +**建议**: +- JDK 25 可以使用字符串模板(需启用预览特性) +- 或使用 `String.format()` 或 `MessageFormat` + +--- + +### 10. 使用 `@PreDestroy` 和 `@PostConstruct` 的改进 + +**文件**: `austin-handler/src/main/java/com/java3y/austin/handler/receipt/MessageReceipt.java` + +**问题**: +- 使用 `javax.annotation.*`(JSR-250),在 JDK 11+ 中需要单独引入依赖 + +**建议**: +- 在 JDK 25 环境下,可以继续使用(Spring Boot 已包含) +- 或考虑使用 Spring 的生命周期回调接口 + +--- + +### 11. Switch 表达式可以进一步优化 + +**已优化文件**: `SensWordsAction.java` 已使用 switch 表达式 + +**建议**: +- 检查其他文件是否还有传统 switch-case,可统一优化 + +--- + +### 12. Optional 使用可以更优雅 + +**已优化文件**: `Receiver.java` 已使用 `Optional.ifPresent()` + +**建议**: +- 其他文件中的 Optional 使用可以继续优化 +- 避免 `isPresent() + get()` 的组合,使用 `orElse()`, `orElseThrow()` 等 + +--- + +## 🔵 Spring Boot 3.5.7 特性优化 + +### 13. 虚拟线程配置检查 + +**已配置**: `application.properties` 中已启用虚拟线程 + +**建议验证**: +```properties +# 确认虚拟线程已正确启用 +spring.threads.virtual.enabled=true +spring.threads.virtual.scheduler.name-prefix=austin-virtual- +``` + +**验证方法**: +- 检查线程名称是否包含 `austin-virtual-` 前缀 +- 使用 `ProcessInfo.VirtualThreadsInfo` 监控虚拟线程状态 + +--- + +### 14. 优雅关闭配置 + +**已配置**: `server.shutdown=graceful` + +**建议**: +- 确保所有线程池都注册到 `ThreadPoolExecutorShutdownDefinition`(已实现) +- 验证关闭超时时间是否合理(当前 20 秒) + +--- + +### 15. 配置属性验证 + +**建议**: +- 使用 `@ConfigurationProperties` + `@Valid` 进行配置验证 +- 对于必需配置,使用 `@NotNull` 或 `@NotEmpty` + +--- + +## 📊 性能优化建议 + +### 16. 集合初始化容量优化 + +**已优化**: 部分文件已使用 `new HashSet<>(size)` 指定初始容量 + +**建议**: +- 继续检查其他集合初始化,确保容量设置合理 +- 避免集合频繁扩容 + +--- + +### 17. Stream API 优化 + +**建议**: +- 对于大数据量处理,考虑使用 `parallelStream()`(需注意线程安全) +- 避免在 Stream 中进行复杂的数据库查询 + +--- + +### 18. Redis 操作优化 + +**已优化**: `SimpleLimitService` 使用 pipeline 批量操作 + +**建议**: +- 继续检查其他 Redis 操作,优先使用批量操作 +- 考虑使用 Redis 事务或 Lua 脚本减少网络往返 + +--- + +## 🐛 潜在 Bug + +### 19. 整数溢出风险 + +**文件**: `austin-handler/src/main/java/com/java3y/austin/handler/deduplication/limit/SimpleLimitService.java:69` + +**问题**: +```java +keyValues.put(key, String.valueOf(Integer.parseInt(inRedisValue.get(key)) + 1)); +``` + +**风险**: +- 如果计数超过 `Integer.MAX_VALUE`,会发生溢出 + +**建议**: +- 使用 `Long` 类型 +- 或添加溢出检查 + +--- + +### 20. 字符编码处理 + +**文件**: `austin-support/src/main/java/com/java3y/austin/support/utils/AustinFileUtils.java` + +**建议**: +- 确保所有文件读取操作明确指定 UTF-8 编码 +- 避免使用系统默认编码 + +--- + +### 21. 日期时间处理 + +**建议**: +- 检查是否所有日期时间操作都使用 `java.time.*` API(JDK 8+) +- 避免使用 `java.util.Date` 和 `Calendar` + +--- + +## 📝 总结 + +### 立即修复(高优先级) +1. ✅ `OkHttpConfiguration.java` - 异常处理和日志 +2. ✅ `ExceptionHandlerAdvice.java` - 依赖替换 +3. ✅ `ConcurrentHashMapUtils.java` - JDK 版本检测逻辑 + +### 建议修复(中优先级) +4. ⚠️ Null 安全检查增强 +5. ⚠️ 资源关闭异常处理 +6. ⚠️ Optional 使用优化 + +### 代码质量提升(低优先级) +7. 💡 构造函数注入 +8. 💡 Lombok 优化 +9. 💡 Switch 表达式统一 +10. 💡 Spring Boot 3.5.7 特性充分利用 + +### 性能优化 +11. ⚡ 集合初始化容量 +12. ⚡ Stream API 优化 +13. ⚡ Redis 批量操作 + +--- + +## 🚀 下一步行动 + +1. **立即修复**严重问题(1-3) +2. **逐步优化**中等优先级问题(4-6) +3. **持续改进**代码质量(7-10) +4. **性能测试**验证优化效果(11-13) + +--- + +**生成时间**: 2024年 +**基于版本**: JDK 25 + Spring Boot 3.5.7 +**检查工具**: Context7 + 代码审查