1,开启springBoot 虚拟线程

2,使用部分jdk新特性(var类型自动推断/箭头枚举/集合Of等)
feature_jdk_25
3y 1 month ago
parent a460055947
commit e6990cadee

@ -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<TaskInfo> {
@Autowired
private HandlerHolder handlerHolder;
private final HandlerHolder handlerHolder;
public SendMessageAction(HandlerHolder handlerHolder) {
this.handlerHolder = handlerHolder;
}
@Override
public void process(ProcessContext<TaskInfo> context) {
TaskInfo taskInfo = context.getProcessModel();
Integer sendChannel = taskInfo.getSendChannel();
// 参数校验
if (sendChannel == null) {
log.warn("Send channel is null, taskInfo: {}", taskInfo);
return;
}
Set<String> 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<Integer> 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);
}
}

@ -34,80 +34,59 @@ public class SensWordsAction implements BusinessProcess<TaskInfo> {
*/
@Override
public void process(ProcessContext<TaskInfo> context) {
// 获取敏感词典
Set<String> 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<TaskInfo> {
* @return
*/
private TrieNode buildTrie(Set<String> 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;
}

@ -29,12 +29,12 @@ public class SimpleLimitService extends AbstractLimitService {
@Override
public Set<String> limitFilter(AbstractDeduplicationService service, TaskInfo taskInfo, DeduplicationParam param) {
Set<String> filterReceiver = new HashSet<>(taskInfo.getReceiver().size());
var filterReceiver = new HashSet<String>(taskInfo.getReceiver().size());
// 获取redis记录
Map<String, String> readyPutRedisReceiver = new HashMap<>(taskInfo.getReceiver().size());
var readyPutRedisReceiver = new HashMap<String, String>(taskInfo.getReceiver().size());
//redis数据隔离
List<String> keys = deduplicationAllKey(service, taskInfo).stream().map(key -> LIMIT_TAG + key).collect(Collectors.toList());
Map<String, String> 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<String, String> readyPutRedisReceiver,
Map<String, String> inRedisValue, Long deduplicationTime) {
Map<String, String> keyValues = new HashMap<>(readyPutRedisReceiver.size());
for (Map.Entry<String, String> 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<String, String>(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));
}

@ -51,15 +51,14 @@ public class SlideWindowLimitService extends AbstractLimitService {
*/
@Override
public Set<String> limitFilter(AbstractDeduplicationService service, TaskInfo taskInfo, DeduplicationParam param) {
Set<String> 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<String>(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);

@ -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<?, String> consumerRecord, @Header(KafkaHeaders.GROUP_ID) String topicGroupId) {
Optional<String> kafkaMessage = Optional.ofNullable(consumerRecord.value());
if (kafkaMessage.isPresent()) {
List<TaskInfo> 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<?, String> consumerRecord) {
Optional<String> 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);
}
});
}
}

@ -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;
}

@ -34,9 +34,9 @@ public class SpringEventBusReceiverListener implements ApplicationListener<Austi
public void onApplicationEvent(AustinSpringEventBusEvent event) {
String topic = event.getAustinSpringEventSource().getTopic();
String jsonValue = event.getAustinSpringEventSource().getJsonValue();
if (topic.equals(sendTopic)) {
if (sendTopic.equals(topic)) {
springEventBusReceiver.consume(JSON.parseArray(jsonValue, TaskInfo.class));
} else if (topic.equals(recallTopic)) {
} else if (recallTopic.equals(topic)) {
springEventBusReceiver.recall(JSON.parseObject(jsonValue, RecallTaskInfo.class));
}
}

@ -6,7 +6,6 @@ import cn.hutool.core.util.ReflectUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.google.common.base.Throwables;
import com.java3y.austin.common.constant.CommonConstant;
import com.java3y.austin.common.domain.TaskInfo;
import com.java3y.austin.common.dto.model.ContentModel;
@ -27,6 +26,7 @@ import org.springframework.stereotype.Service;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author 3y
@ -84,16 +84,23 @@ public class SendAssembleAction implements BusinessProcess<SendTaskModel> {
Long messageTemplateId = sendTaskModel.getMessageTemplateId();
try {
Optional<MessageTemplate> 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<TaskInfo> 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<TaskInfo> 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<SendTaskModel> {
.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())

@ -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

@ -66,21 +66,20 @@ public class AccountUtils {
@SuppressWarnings("unchecked")
public <T> T getAccountById(Integer sendAccountId, Class<T> clazz) {
try {
Optional<ChannelAccount> 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;
}
/**

@ -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 <a href="https://bugs.openjdk.java.net/browse/JDK-8161372">https://bugs.openjdk.java.net/browse/JDK-8161372</a>
*/
public static <K, V> V computeIfAbsent(ConcurrentMap<K, V> map, K key, Function<? super K, ? extends V> 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);
}
}
}

@ -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<String> 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<RespStatusEnum> 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());
}
}

@ -80,8 +80,8 @@ public class DataServiceImpl implements DataService {
// 获取businessId并获取模板信息
businessId = getRealBusinessId(businessId);
Optional<MessageTemplate> 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<Object, Object> anchorResult = redisUtils.hGetAll(getRealBusinessId(businessId));
return Convert4Amis.getEchartsVo(anchorResult, optional.get(), businessId);
return Convert4Amis.getEchartsVo(anchorResult, messageTemplateOptional.get(), businessId);
}
@Override

@ -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

@ -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 <K, V> V computeIfAbsent(ConcurrentMap<K, V> map, K key, Function<? super K, ? extends V> 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<TaskInfo> context) {
TaskInfo taskInfo = context.getProcessModel();
Integer sendChannel = taskInfo.getSendChannel();
if (sendChannel == null) {
log.warn("Send channel is null, taskInfo: {}", taskInfo);
return;
}
Set<String> 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> messageTemplate = messageTemplateDao.findById(messageTemplateId);
if (!messageTemplate.isPresent() || messageTemplate.get().getIsDeleted().equals(CommonConstant.TRUE)) {
// ...
}
List<TaskInfo> 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<TaskInfo> 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.*` APIJDK 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 + 代码审查
Loading…
Cancel
Save