From cb19b459196971ca869b7298275cdf3babd0f468 Mon Sep 17 00:00:00 2001 From: 3god Date: Tue, 8 Oct 2024 09:38:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=91=E5=BF=AB=E5=85=851.5.0=20=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 33 ++ README.md | 46 +- docker/Dockerfile-App | 40 ++ docker/Dockerfile-Base | 11 + docker/Dockerfile-Protocol | 40 ++ docker/docker-compose.kafka.yml | 57 ++ docker/docker-compose.redis-cluster.yml | 90 +++ docker/docker-compose.redis-sentinel.yml | 49 ++ docker/docker-compose.redis-standalone.yml | 23 + docker/kafka.env | 14 + docker/queue-kafka.env | 2 + docker/schema/schema-postgres.sql | 121 ++++ docker/start.sh | 21 + jcpp-app-bootstrap/pom.xml | 84 +++ jcpp-app-bootstrap/src/layers.xml | 33 ++ .../sanbing/jcpp/JCPPServerApplication.java | 39 ++ .../src/main/resources/app-service.yml | 214 +++++++ .../src/main/resources/banner.txt | 12 + .../src/main/resources/log4j2.xml | 56 ++ .../java/sanbing/jcpp/AbstractTestBase.java | 27 + .../jcpp/app/dal/mapper/GunMapperTest.java | 72 +++ .../jcpp/app/dal/mapper/OrderMapperTest.java | 66 +++ .../jcpp/app/dal/mapper/PileMapperTest.java | 74 +++ .../app/dal/mapper/StationMapperTest.java | 55 ++ .../jcpp/app/dal/mapper/UserMapperTest.java | 44 ++ .../cache/RedisCacheConfigurationTest.java | 98 ++++ .../resources/app-service-test.properties | 2 + jcpp-app/pom.xml | 56 ++ .../jcpp/app/dal/config/DalConfig.java | 76 +++ .../config/ibatis/enums/GunOptStatusEnum.java | 22 + .../config/ibatis/enums/GunRunStatusEnum.java | 27 + .../config/ibatis/enums/OrderStatusEnum.java | 24 + .../config/ibatis/enums/OrderTypeEnum.java | 21 + .../config/ibatis/enums/OwnerTypeEnum.java | 21 + .../config/ibatis/enums/PileStatusEnum.java | 24 + .../dal/config/ibatis/enums/PileTypeEnum.java | 21 + .../ibatis/enums/StationStatusEnum.java | 26 + .../config/ibatis/enums/UserStatusEnum.java | 20 + .../ibatis/typehandlers/JsonbTypeHandler.java | 40 ++ .../ibatis/typehandlers/UUIDTypeHandler.java | 40 ++ .../java/sanbing/jcpp/app/dal/entity/Gun.java | 61 ++ .../sanbing/jcpp/app/dal/entity/Order.java | 69 +++ .../sanbing/jcpp/app/dal/entity/Pile.java | 61 ++ .../sanbing/jcpp/app/dal/entity/Station.java | 62 +++ .../sanbing/jcpp/app/dal/entity/User.java | 45 ++ .../jcpp/app/dal/mapper/GunMapper.java | 14 + .../jcpp/app/dal/mapper/OrderMapper.java | 14 + .../jcpp/app/dal/mapper/PileMapper.java | 23 + .../jcpp/app/dal/mapper/StationMapper.java | 14 + .../jcpp/app/dal/mapper/UserMapper.java | 14 + .../sanbing/jcpp/app/data/PileSession.java | 57 ++ .../AbstractCachedEntityRepository.java | 23 + .../repository/AbstractEntityRepository.java | 17 + .../CachedVersionedEntityRepository.java | 19 + .../jcpp/app/repository/PileRepository.java | 15 + .../app/repository/PileRepositoryImpl.java | 47 ++ .../jcpp/app/service/DownlinkCallService.java | 15 + .../jcpp/app/service/PileProtocolService.java | 66 +++ .../cache/pile/PileCacheEvictEvent.java | 17 + .../app/service/cache/pile/PileCacheKey.java | 47 ++ .../service/cache/pile/PileCaffeineCache.java | 22 + .../service/cache/pile/PileRedisCache.java | 33 ++ .../cache/session/PileSessionCacheKey.java | 41 ++ .../session/PileSessionCaffeineCache.java | 24 + .../cache/session/PileSessionRedisCache.java | 36 ++ .../DownlinkRestTemplateConfiguration.java | 32 ++ .../impl/DefaultDownlinkCallService.java | 93 ++++ .../impl/DefaultPileProtocolService.java | 264 +++++++++ .../queue/AbstractConsumerService.java | 66 +++ .../app/service/queue/AppConsumerStats.java | 85 +++ .../queue/AppQueueConsumerManager.java | 296 ++++++++++ .../queue/QueueConsumerManagerTask.java | 37 ++ .../app/service/queue/QueueConsumerTask.java | 78 +++ .../jcpp/app/service/queue/QueueEvent.java | 13 + .../ProtocolUplinkConsumerService.java | 236 ++++++++ jcpp-infrastructure-cache/pom.xml | 51 ++ .../infrastructure/cache/CacheConstants.java | 13 + .../jcpp/infrastructure/cache/CacheSpecs.java | 13 + .../infrastructure/cache/CacheSpecsMap.java | 22 + .../cache/CacheTransaction.java | 15 + .../cache/CacheValueWrapper.java | 11 + .../cache/CaffeineCacheTransaction.java | 48 ++ .../cache/CaffeineTransactionalCache.java | 180 ++++++ .../jcpp/infrastructure/cache/HasVersion.java | 14 + .../cache/JCPPCaffeineCacheConfiguration.java | 83 +++ .../JCPPJCPPRedisClusterConfiguration.java | 54 ++ .../JCPPJCPPRedisSentinelConfiguration.java | 59 ++ .../JCPPJCPPRedisStandaloneConfiguration.java | 78 +++ .../cache/JCPPRedisCacheConfiguration.java | 168 ++++++ .../cache/JCPPRedisSerializer.java | 18 + .../cache/RedisCacheTransaction.java | 45 ++ .../cache/RedisTransactionalCache.java | 246 ++++++++ .../cache/SimpleCacheValueWrapper.java | 35 ++ .../cache/TransactionalCache.java | 86 +++ .../infrastructure/cache/VersionedCache.java | 51 ++ .../cache/VersionedCacheKey.java | 15 + .../cache/VersionedCaffeineCache.java | 86 +++ .../cache/VersionedRedisCache.java | 155 ++++++ jcpp-infrastructure-proto/pom.xml | 50 ++ .../infrastructure/proto/ProtoConverter.java | 57 ++ .../proto/model/PricingModel.java | 78 +++ .../src/main/proto/cluster.proto | 25 + .../src/main/proto/protocol.proto | 242 ++++++++ jcpp-infrastructure-queue/pom.xml | 52 ++ .../queue/AbstractQueueConsumerTemplate.java | 190 +++++++ .../jcpp/infrastructure/queue/Callback.java | 26 + .../infrastructure/queue/DefaultQueueMsg.java | 23 + .../queue/DefaultQueueMsgHeaders.java | 29 + .../infrastructure/queue/KafkaQueueMsg.java | 38 ++ .../infrastructure/queue/PackCallback.java | 32 ++ .../queue/PackProcessingContext.java | 75 +++ .../infrastructure/queue/ProtoQueueMsg.java | 40 ++ .../jcpp/infrastructure/queue/QueueAdmin.java | 17 + .../infrastructure/queue/QueueCallback.java | 12 + .../infrastructure/queue/QueueConsumer.java | 34 ++ .../jcpp/infrastructure/queue/QueueMsg.java | 14 + .../infrastructure/queue/QueueMsgHeaders.java | 16 + .../queue/QueueMsgMetadata.java | 8 + .../infrastructure/queue/QueueProducer.java | 19 + .../queue/common/QueueConfig.java | 13 + .../queue/common/QueueConstants.java | 17 + .../queue/common/TopicPartitionInfo.java | 59 ++ .../discovery/DefaultServiceInfoProvider.java | 109 ++++ .../queue/discovery/DiscoveryProvider.java | 15 + .../discovery/DummyDiscoveryProvider.java | 41 ++ .../discovery/HashPartitionProvider.java | 199 +++++++ .../queue/discovery/PartitionProvider.java | 22 + .../queue/discovery/QueueKey.java | 33 ++ .../queue/discovery/ServiceInfoProvider.java | 26 + .../queue/discovery/ServiceType.java | 23 + .../queue/discovery/ZkDiscoveryProvider.java | 328 +++++++++++ .../discovery/event/JCPPApplicationEvent.java | 26 + .../event/JCPPApplicationEventListener.java | 54 ++ .../event/OtherServiceShutdownEvent.java | 18 + .../discovery/event/PartitionChangeEvent.java | 45 ++ .../queue/kafka/KafkaAdmin.java | 94 ++++ .../kafka/KafkaConsumerStatisticConfig.java | 29 + .../kafka/KafkaConsumerStatsService.java | 158 ++++++ .../queue/kafka/KafkaConsumerTemplate.java | 117 ++++ .../queue/kafka/KafkaDecoder.java | 16 + .../queue/kafka/KafkaProducerTemplate.java | 133 +++++ .../queue/kafka/KafkaQueueMsgMetadata.java | 17 + .../queue/kafka/KafkaSettings.java | 210 +++++++ .../queue/kafka/KafkaTopicConfigs.java | 32 ++ .../queue/memory/DefaultInMemoryStorage.java | 66 +++ .../queue/memory/InMemoryQueueConsumer.java | 106 ++++ .../queue/memory/InMemoryQueueProducer.java | 48 ++ .../queue/memory/InMemoryStorage.java | 24 + .../queue/processing/IdMsgPair.java | 22 + .../queue/provider/AppQueueFactory.java | 18 + .../provider/InMemoryAppQueueFactory.java | 48 ++ .../queue/provider/KafkaAppQueueFactory.java | 83 +++ .../queue/settings/QueueAppSettings.java | 30 + jcpp-infrastructure-stats/pom.xml | 57 ++ .../infrastructure/stats/DefaultCounter.java | 37 ++ .../stats/DefaultMessagesStats.java | 54 ++ .../stats/DefaultStatsFactory.java | 124 +++++ .../infrastructure/stats/MessagesStats.java | 33 ++ .../infrastructure/stats/StatsCounter.java | 22 + .../infrastructure/stats/StatsFactory.java | 59 ++ .../jcpp/infrastructure/stats/StatsTimer.java | 44 ++ jcpp-infrastructure-util/pom.xml | 81 +++ .../infrastructure/util/JCPPHashUtil.java | 34 ++ .../jcpp/infrastructure/util/JCPPPair.java | 19 + .../jcpp/infrastructure/util/SystemUtil.java | 95 ++++ .../util/annotation/AfterStartUp.java | 28 + .../util/annotation/AppComponent.java | 25 + .../util/annotation/ProtocolComponent.java | 49 ++ .../util/async/JCPPAsynchron.java | 54 ++ .../util/async/JCPPExecutors.java | 27 + .../JCPPForkJoinWorkerThreadFactory.java | 30 + .../util/async/JCPPThreadFactory.java | 46 ++ .../util/async/JCPPVirtualThreadFactory.java | 24 + .../infrastructure/util/codec/BCDUtil.java | 172 ++++++ .../infrastructure/util/codec/ByteUtil.java | 81 +++ .../util/codec/CP56Time2aUtil.java | 65 +++ .../util/config/ConstraintValidator.java | 97 ++++ .../util/config/ShardingThreadPool.java | 82 +++ .../util/config/ThreadPoolConfiguration.java | 43 ++ .../exception/DataValidationException.java | 16 + .../util/exception/DownlinkException.java | 19 + .../IncorrectParameterException.java | 17 + .../util/jackson/BigNumberSerializer.java | 35 ++ .../util/jackson/DataTypeModule.java | 56 ++ .../util/jackson/DateDeserializer.java | 38 ++ .../util/jackson/DateSerializer.java | 32 ++ .../util/jackson/InstantDeserializer.java | 41 ++ .../util/jackson/InstantSerializer.java | 21 + .../util/jackson/JacksonUtil.java | 215 +++++++ .../jackson/LocalDateTimeDeserializer.java | 39 ++ .../util/jackson/LocalDateTimeSerializer.java | 32 ++ .../util/jackson/LocalTimeDeserializer.java | 42 ++ .../util/jackson/LocalTimeSerializer.java | 32 ++ .../jackson/LongTimestampDeserializer.java | 33 ++ .../util/jackson/SqlDateDeserializer.java | 39 ++ .../util/jackson/SqlDateSerializer.java | 33 ++ .../util/jackson/TimestampDeserializer.java | 38 ++ .../util/jackson/TimestampSerializer.java | 33 ++ .../infrastructure/util/mdc/MDCUtils.java | 43 ++ .../util/property/JCPPProperty.java | 14 + .../util/property/PropertyUtils.java | 44 ++ .../util/trace/TraceIdGenerator.java | 69 +++ .../infrastructure/util/trace/Tracer.java | 38 ++ .../util/trace/TracerCallable.java | 41 ++ .../util/trace/TracerContextUtil.java | 71 +++ .../util/trace/TracerRunnable.java | 38 ++ .../util/validation/Length.java | 28 + .../validation/StringLengthValidator.java | 35 ++ .../util/validation/Validator.java | 55 ++ .../util/codec/BCDUtilTest.java | 28 + .../util/codec/CP56Time2aUtilTest.java | 27 + jcpp-protocol-api/pom.xml | 50 ++ .../jcpp/protocol/ProtocolBootstrap.java | 125 +++++ .../jcpp/protocol/ProtocolContext.java | 64 +++ .../protocol/ProtocolMessageProcessor.java | 66 +++ .../protocol/adapter/DownlinkController.java | 78 +++ .../adapter/config/TracerInterceptor.java | 41 ++ .../UndertowServletWebServerCustomizer.java | 24 + .../adapter/config/WebMvcConfiguration.java | 45 ++ .../jcpp/protocol/cfg/ForwarderCfg.java | 24 + .../sanbing/jcpp/protocol/cfg/KafkaCfg.java | 49 ++ .../jcpp/protocol/cfg/ListenerCfg.java | 17 + .../sanbing/jcpp/protocol/cfg/MemoryCfg.java | 18 + .../jcpp/protocol/cfg/ProtocolCfg.java | 25 + .../sanbing/jcpp/protocol/cfg/TcpCfg.java | 45 ++ .../jcpp/protocol/cfg/TcpHandlerCfg.java | 55 ++ .../protocol/cfg/enums/ForwarderType.java | 12 + .../protocol/cfg/enums/TcpHandlerType.java | 14 + .../jcpp/protocol/domain/DownlinkCmdEnum.java | 23 + .../protocol/domain/ListenerToHandlerMsg.java | 11 + .../jcpp/protocol/domain/ProtocolSession.java | 87 +++ .../protocol/domain/ProtocolUplinkMsg.java | 22 + .../protocol/domain/SessionCloseReason.java | 14 + .../protocol/domain/SessionToHandlerMsg.java | 13 + .../jcpp/protocol/forwarder/Forwarder.java | 110 ++++ .../protocol/forwarder/KafkaForwarder.java | 204 +++++++ .../protocol/forwarder/MemoryForwarder.java | 84 +++ .../listener/ChannelHandlerInitializer.java | 142 +++++ .../listener/ChannelHandlerParameter.java | 24 + .../jcpp/protocol/listener/Listener.java | 45 ++ .../listener/tcp/TcpChannelHandler.java | 238 ++++++++ .../protocol/listener/tcp/TcpListener.java | 114 ++++ .../protocol/listener/tcp/TcpSession.java | 90 +++ .../configs/BinaryHandlerConfiguration.java | 75 +++ .../tcp/configs/HandlerConfiguration.java | 37 ++ .../tcp/configs/JsonHandlerConfiguration.java | 22 + .../tcp/configs/TextHandlerConfiguration.java | 32 ++ .../tcp/decoder/JCPPHeadTailFrameDecoder.java | 145 +++++ .../JCPPLengthFieldBasedFrameDecoder.java | 217 ++++++++ .../listener/tcp/decoder/TcpMsgDecoder.java | 51 ++ .../protocol/listener/tcp/enums/ReadAct.java | 15 + .../tcp/enums/SequenceNumberLength.java | 21 + .../tcp/handler/ConnectionLimitHandler.java | 60 ++ .../tcp/handler/IdleEventHandler.java | 35 ++ .../listener/tcp/handler/TracerHandler.java | 25 + .../ProtocolSessionRegistryProvider.java | 30 + .../provider/ProtocolsConfigProvider.java | 15 + ...efaultProtocolSessionRegistryProvider.java | 115 ++++ .../impl/DefaultProtocolsConfigProvider.java | 37 ++ jcpp-protocol-bootstrap/pom.xml | 75 +++ jcpp-protocol-bootstrap/src/layers.xml | 33 ++ .../JCPPProtocolServiceApplication.java | 42 ++ .../src/main/resources/banner.txt | 12 + .../src/main/resources/log4j2.xml | 56 ++ .../src/main/resources/protocol-service.yml | 152 +++++ .../protocol/AbstractProtocolTestBase.java | 38 ++ .../adapter/DownlinkControllerTest.java | 147 +++++ jcpp-protocol-yunkuaichong/pom.xml | 35 ++ .../AbstractYunKuaiChongCmdExe.java | 154 +++++ .../YunKuaiChongDownlinkCmdExe.java | 17 + .../YunKuaiChongDwonlinkMessage.java | 40 ++ .../YunKuaiChongUplinkCmdExe.java | 33 ++ .../YunKuaiChongUplinkMessage.java | 51 ++ .../annotation/YunKuaiChongCmd.java | 19 + ...nKuaiChongV15ProtocolMessageProcessor.java | 224 ++++++++ .../YunkuaichongV150ProtocolBootstrap.java | 45 ++ .../YunKuaiChongV150BmsHandshakeULCmd.java | 118 ++++ .../cmd/YunKuaiChongV150HeartbeatULCmd.java | 78 +++ .../cmd/YunKuaiChongV150LoginAckDLCmd.java | 118 ++++ .../v150/cmd/YunKuaiChongV150LoginULCmd.java | 77 +++ ...uaiChongV150QueryPricingModelAckDLCmd.java | 93 ++++ ...unKuaiChongV150QueryPricingModelULCmd.java | 50 ++ .../YunKuaiChongV150RealTimeDataULCmd.java | 236 ++++++++ .../cmd/YunKuaiChongV150RemoteStartDLCmd.java | 73 +++ ...unKuaiChongV150RemoteStartResultULCmd.java | 93 ++++ .../cmd/YunKuaiChongV150RemoteStopDLCmd.java | 49 ++ ...YunKuaiChongV150RemoteStopResultULCmd.java | 82 +++ ...nKuaiChongV150SetPricingModelAckULCmd.java | 60 ++ .../YunKuaiChongV150SetPricingModelDLCmd.java | 91 +++ ...uaiChongV150TransactionRecordAckDLCmd.java | 53 ++ ...unKuaiChongV150TransactionRecordULCmd.java | 290 ++++++++++ ...aiChongV150VerifyPricingModelAckDLCmd.java | 62 +++ ...nKuaiChongV150VerifyPricingModelULCmd.java | 59 ++ .../YunKuaiChongV150DownlinkCmdEnum.java | 38 ++ jcpp-testing/pom.xml | 100 ++++ license-header-template.txt | 2 + pom.xml | 525 ++++++++++++++++++ 297 files changed, 18020 insertions(+), 28 deletions(-) create mode 100644 docker/Dockerfile-App create mode 100644 docker/Dockerfile-Base create mode 100644 docker/Dockerfile-Protocol create mode 100644 docker/docker-compose.kafka.yml create mode 100644 docker/docker-compose.redis-cluster.yml create mode 100644 docker/docker-compose.redis-sentinel.yml create mode 100644 docker/docker-compose.redis-standalone.yml create mode 100644 docker/kafka.env create mode 100644 docker/queue-kafka.env create mode 100644 docker/schema/schema-postgres.sql create mode 100644 docker/start.sh create mode 100644 jcpp-app-bootstrap/pom.xml create mode 100644 jcpp-app-bootstrap/src/layers.xml create mode 100644 jcpp-app-bootstrap/src/main/java/sanbing/jcpp/JCPPServerApplication.java create mode 100644 jcpp-app-bootstrap/src/main/resources/app-service.yml create mode 100644 jcpp-app-bootstrap/src/main/resources/banner.txt create mode 100644 jcpp-app-bootstrap/src/main/resources/log4j2.xml create mode 100644 jcpp-app-bootstrap/src/test/java/sanbing/jcpp/AbstractTestBase.java create mode 100644 jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/GunMapperTest.java create mode 100644 jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/OrderMapperTest.java create mode 100644 jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/PileMapperTest.java create mode 100644 jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/StationMapperTest.java create mode 100644 jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/UserMapperTest.java create mode 100644 jcpp-app-bootstrap/src/test/java/sanbing/jcpp/infrastructure/cache/RedisCacheConfigurationTest.java create mode 100644 jcpp-app-bootstrap/src/test/resources/app-service-test.properties create mode 100644 jcpp-app/pom.xml create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/DalConfig.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/GunOptStatusEnum.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/GunRunStatusEnum.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OrderStatusEnum.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OrderTypeEnum.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OwnerTypeEnum.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/PileStatusEnum.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/PileTypeEnum.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/StationStatusEnum.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/UserStatusEnum.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/typehandlers/JsonbTypeHandler.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/typehandlers/UUIDTypeHandler.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Gun.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Order.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Pile.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Station.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/User.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/GunMapper.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/OrderMapper.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/PileMapper.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/StationMapper.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/UserMapper.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/data/PileSession.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/repository/AbstractCachedEntityRepository.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/repository/AbstractEntityRepository.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/repository/CachedVersionedEntityRepository.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/repository/PileRepository.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/repository/PileRepositoryImpl.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/DownlinkCallService.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/PileProtocolService.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheEvictEvent.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheKey.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCaffeineCache.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileRedisCache.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionCacheKey.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionCaffeineCache.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionRedisCache.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/config/DownlinkRestTemplateConfiguration.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultDownlinkCallService.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultPileProtocolService.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AbstractConsumerService.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AppConsumerStats.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AppQueueConsumerManager.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueConsumerManagerTask.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueConsumerTask.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueEvent.java create mode 100644 jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/consumer/ProtocolUplinkConsumerService.java create mode 100644 jcpp-infrastructure-cache/pom.xml create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheConstants.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecs.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecsMap.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheTransaction.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheValueWrapper.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CaffeineCacheTransaction.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CaffeineTransactionalCache.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/HasVersion.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPCaffeineCacheConfiguration.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisClusterConfiguration.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisSentinelConfiguration.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisStandaloneConfiguration.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisSerializer.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/RedisCacheTransaction.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/RedisTransactionalCache.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/SimpleCacheValueWrapper.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/TransactionalCache.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCache.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCacheKey.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCaffeineCache.java create mode 100644 jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java create mode 100644 jcpp-infrastructure-proto/pom.xml create mode 100644 jcpp-infrastructure-proto/src/main/java/sanbing/jcpp/infrastructure/proto/ProtoConverter.java create mode 100644 jcpp-infrastructure-proto/src/main/java/sanbing/jcpp/infrastructure/proto/model/PricingModel.java create mode 100644 jcpp-infrastructure-proto/src/main/proto/cluster.proto create mode 100644 jcpp-infrastructure-proto/src/main/proto/protocol.proto create mode 100644 jcpp-infrastructure-queue/pom.xml create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/AbstractQueueConsumerTemplate.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/Callback.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/DefaultQueueMsg.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/DefaultQueueMsgHeaders.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/KafkaQueueMsg.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/PackCallback.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/PackProcessingContext.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/ProtoQueueMsg.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueAdmin.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueCallback.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueConsumer.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsg.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsgHeaders.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsgMetadata.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueProducer.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/QueueConfig.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/QueueConstants.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/TopicPartitionInfo.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DefaultServiceInfoProvider.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DiscoveryProvider.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DummyDiscoveryProvider.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/HashPartitionProvider.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/PartitionProvider.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/QueueKey.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ServiceInfoProvider.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ServiceType.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ZkDiscoveryProvider.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/JCPPApplicationEvent.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/JCPPApplicationEventListener.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/OtherServiceShutdownEvent.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/PartitionChangeEvent.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaAdmin.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerStatisticConfig.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerStatsService.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerTemplate.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaDecoder.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaProducerTemplate.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaQueueMsgMetadata.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaSettings.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaTopicConfigs.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/DefaultInMemoryStorage.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryQueueConsumer.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryQueueProducer.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryStorage.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/processing/IdMsgPair.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/AppQueueFactory.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/InMemoryAppQueueFactory.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/KafkaAppQueueFactory.java create mode 100644 jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/settings/QueueAppSettings.java create mode 100644 jcpp-infrastructure-stats/pom.xml create mode 100644 jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultCounter.java create mode 100644 jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultMessagesStats.java create mode 100644 jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultStatsFactory.java create mode 100644 jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/MessagesStats.java create mode 100644 jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsCounter.java create mode 100644 jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsFactory.java create mode 100644 jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsTimer.java create mode 100644 jcpp-infrastructure-util/pom.xml create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/JCPPHashUtil.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/JCPPPair.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/SystemUtil.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/AfterStartUp.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/AppComponent.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/ProtocolComponent.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPAsynchron.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPExecutors.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPForkJoinWorkerThreadFactory.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPThreadFactory.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPVirtualThreadFactory.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/BCDUtil.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/ByteUtil.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/CP56Time2aUtil.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ConstraintValidator.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ShardingThreadPool.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ThreadPoolConfiguration.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/DataValidationException.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/DownlinkException.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/IncorrectParameterException.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/BigNumberSerializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DataTypeModule.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DateDeserializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DateSerializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/InstantDeserializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/InstantSerializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/JacksonUtil.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalDateTimeDeserializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalDateTimeSerializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalTimeDeserializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalTimeSerializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LongTimestampDeserializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/SqlDateDeserializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/SqlDateSerializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/TimestampDeserializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/TimestampSerializer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/mdc/MDCUtils.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/property/JCPPProperty.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/property/PropertyUtils.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TraceIdGenerator.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/Tracer.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerCallable.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerContextUtil.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerRunnable.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/Length.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/StringLengthValidator.java create mode 100644 jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/Validator.java create mode 100644 jcpp-infrastructure-util/src/test/java/sanbing/jcpp/infrastructure/util/codec/BCDUtilTest.java create mode 100644 jcpp-infrastructure-util/src/test/java/sanbing/jcpp/infrastructure/util/codec/CP56Time2aUtilTest.java create mode 100644 jcpp-protocol-api/pom.xml create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolBootstrap.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolContext.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolMessageProcessor.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/DownlinkController.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/TracerInterceptor.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/UndertowServletWebServerCustomizer.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/WebMvcConfiguration.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ForwarderCfg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/KafkaCfg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ListenerCfg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/MemoryCfg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ProtocolCfg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/TcpCfg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/TcpHandlerCfg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/enums/ForwarderType.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/enums/TcpHandlerType.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/DownlinkCmdEnum.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ListenerToHandlerMsg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ProtocolSession.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ProtocolUplinkMsg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/SessionCloseReason.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/SessionToHandlerMsg.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/Forwarder.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/KafkaForwarder.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/MemoryForwarder.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/ChannelHandlerInitializer.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/ChannelHandlerParameter.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/Listener.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpChannelHandler.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpListener.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpSession.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/BinaryHandlerConfiguration.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/HandlerConfiguration.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/JsonHandlerConfiguration.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/TextHandlerConfiguration.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPHeadTailFrameDecoder.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPLengthFieldBasedFrameDecoder.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/TcpMsgDecoder.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/enums/ReadAct.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/enums/SequenceNumberLength.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/ConnectionLimitHandler.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/IdleEventHandler.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/TracerHandler.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/ProtocolSessionRegistryProvider.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/ProtocolsConfigProvider.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/impl/DefaultProtocolSessionRegistryProvider.java create mode 100644 jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/impl/DefaultProtocolsConfigProvider.java create mode 100644 jcpp-protocol-bootstrap/pom.xml create mode 100644 jcpp-protocol-bootstrap/src/layers.xml create mode 100644 jcpp-protocol-bootstrap/src/main/java/sanbing/jcpp/protocol/JCPPProtocolServiceApplication.java create mode 100644 jcpp-protocol-bootstrap/src/main/resources/banner.txt create mode 100644 jcpp-protocol-bootstrap/src/main/resources/log4j2.xml create mode 100644 jcpp-protocol-bootstrap/src/main/resources/protocol-service.yml create mode 100644 jcpp-protocol-bootstrap/src/test/java/sanbing/jcpp/protocol/AbstractProtocolTestBase.java create mode 100644 jcpp-protocol-bootstrap/src/test/java/sanbing/jcpp/protocol/adapter/DownlinkControllerTest.java create mode 100644 jcpp-protocol-yunkuaichong/pom.xml create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/AbstractYunKuaiChongCmdExe.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongDownlinkCmdExe.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongDwonlinkMessage.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongUplinkCmdExe.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongUplinkMessage.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/annotation/YunKuaiChongCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/YunKuaiChongV15ProtocolMessageProcessor.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/YunkuaichongV150ProtocolBootstrap.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150BmsHandshakeULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150HeartbeatULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginAckDLCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150QueryPricingModelAckDLCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150QueryPricingModelULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RealTimeDataULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStartDLCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStartResultULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStopDLCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStopResultULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150SetPricingModelAckULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150SetPricingModelDLCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150TransactionRecordAckDLCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150TransactionRecordULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150VerifyPricingModelAckDLCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150VerifyPricingModelULCmd.java create mode 100644 jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/enums/YunKuaiChongV150DownlinkCmdEnum.java create mode 100644 jcpp-testing/pom.xml create mode 100644 license-header-template.txt create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore index a1c2a23..c7b1a41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ # Compiled class file *.class +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ # Log file *.log @@ -21,3 +26,31 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/README.md b/README.md index b255b63..b7a044a 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,27 @@ # JChargePointProtocol -#### 介绍 -JAVA 充电桩协议库 +###### 一个高性能、分布式、支持海量并发量的充电桩JAVA服务端,计划支持100种协议,为充电应用提供基础能力。 -#### 软件架构 -软件架构说明 +

+ + GitHub License + + + Static Badge + +

- -#### 安装教程 - -1. xxxx -2. xxxx -3. xxxx - -#### 使用说明 - -1. xxxx -2. xxxx -3. xxxx +------------------------------ #### 参与贡献 -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 +4. 加入社群 +5. 新建 Pull Request -#### 特技 - -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) + \ No newline at end of file diff --git a/docker/Dockerfile-App b/docker/Dockerfile-App new file mode 100644 index 0000000..144da60 --- /dev/null +++ b/docker/Dockerfile-App @@ -0,0 +1,40 @@ +# +# 抖音关注:程序员三丙 +# 知识星球:https://t.zsxq.com/j9b21 +# + +FROM registry.cn-hangzhou.aliyuncs.com/sanbing/jcpp-base:latest AS base +WORKDIR /app +COPY . . +RUN mvn -U -B -T 0.8C clean install -DskipTests + +#分层 +FROM registry.cn-hangzhou.aliyuncs.com/sanbing/openjdk:21-jdk-slim-bullseye AS builder +WORKDIR /app +COPY --from=base /app/jcpp-app-bootstrap/target/application.jar application.jar +RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted + +# 执行 +FROM registry.cn-hangzhou.aliyuncs.com/sanbing/openjdk:21-jdk-slim-bullseye +WORKDIR /app +COPY --from=builder /app/extracted/dependencies/ ./ +COPY --from=builder /app/extracted/spring-boot-loader/ ./ +COPY --from=builder /app/extracted/snapshot-dependencies/ ./ +COPY --from=builder /app/extracted/application/ ./ +COPY --from=base /app/jcpp-app-bootstrap/target/conf ./config +COPY --from=base /app/docker/start.sh . + +RUN mkdir -p /var/log/sanbing && \ + mkdir -p /var/log/sanbing/jcpp && \ + mkdir -p /var/log/sanbing/accesslog && \ + mkdir -p /var/log/sanbing/gc && \ + mkdir -p /var/log/sanbing/heapdump && \ + chmod 700 -R /var/log/* + +RUN chmod a+x *.sh && mv start.sh /usr/bin + +EXPOSE 8080 8080 + +CMD ["start.sh"] + + diff --git a/docker/Dockerfile-Base b/docker/Dockerfile-Base new file mode 100644 index 0000000..770e4e3 --- /dev/null +++ b/docker/Dockerfile-Base @@ -0,0 +1,11 @@ +# +# 抖音关注:程序员三丙 +# 知识星球:https://t.zsxq.com/j9b21 +# + +FROM registry.cn-hangzhou.aliyuncs.com/sanbing/mvn:3.9.9-jdk21 AS base +WORKDIR /app +COPY . . +RUN mvn -U -B -T 0.8C clean install -DskipTests +RUN rm -rf /app + diff --git a/docker/Dockerfile-Protocol b/docker/Dockerfile-Protocol new file mode 100644 index 0000000..7aa342a --- /dev/null +++ b/docker/Dockerfile-Protocol @@ -0,0 +1,40 @@ +# +# 抖音关注:程序员三丙 +# 知识星球:https://t.zsxq.com/j9b21 +# + +FROM registry.cn-hangzhou.aliyuncs.com/sanbing/jcpp-base:latest AS base +WORKDIR /app +COPY . . +RUN mvn -U -B -T 0.8C clean install -DskipTests + +#分层 +FROM registry.cn-hangzhou.aliyuncs.com/sanbing/openjdk:21-jdk-slim-bullseye AS builder +WORKDIR /app +COPY --from=base /app/jcpp-protocol-bootstrap/target/application.jar application.jar +RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted + +# 执行 +FROM registry.cn-hangzhou.aliyuncs.com/sanbing/openjdk:21-jdk-slim-bullseye +WORKDIR /app +COPY --from=builder /app/extracted/dependencies/ ./ +COPY --from=builder /app/extracted/spring-boot-loader/ ./ +COPY --from=builder /app/extracted/snapshot-dependencies/ ./ +COPY --from=builder /app/extracted/application/ ./ +COPY --from=base /app/jcpp-protocol-bootstrap/target/conf ./config +COPY --from=base /app/docker/start.sh . + +RUN mkdir -p /var/log/sanbing && \ + mkdir -p /var/log/sanbing/jcpp && \ + mkdir -p /var/log/sanbing/accesslog && \ + mkdir -p /var/log/sanbing/gc && \ + mkdir -p /var/log/sanbing/heapdump && \ + chmod 700 -R /var/log/* + +RUN chmod a+x *.sh && mv start.sh /usr/bin + +EXPOSE 8081 8081 + +CMD ["start.sh"] + + diff --git a/docker/docker-compose.kafka.yml b/docker/docker-compose.kafka.yml new file mode 100644 index 0000000..26b3478 --- /dev/null +++ b/docker/docker-compose.kafka.yml @@ -0,0 +1,57 @@ +# +# 抖音关注:程序员三丙 +# 知识星球:https://t.zsxq.com/j9b21 +# + +networks: + sanbing-network: + driver: bridge + name: sanbing-network + ipam: + config: + - subnet: 10.10.0.0/24 + +services: + zookeeper: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/zookeeper:3.9 + restart: always + networks: + - sanbing-network + ports: + - "2181:2181" + environment: + ALLOW_ANONYMOUS_LOGIN: true + kafka: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/kafka:3.7.1 + restart: always + depends_on: + - zookeeper + networks: + - sanbing-network + ports: + - "9092:9092" + env_file: + - kafka.env + kafka-exporter: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/kafka-exporter:latest + restart: always + depends_on: + - kafka + networks: + - sanbing-network + ports: + - "9308:9308" + command: + - '--kafka.server=kafka:9092' + # 切换示例项目的队列类型为kafka + example: + restart: always + image: example:latest + depends_on: + - kafka + networks: + - sanbing-network + ports: + - "8080:8080" + env_file: + - queue-kafka.env diff --git a/docker/docker-compose.redis-cluster.yml b/docker/docker-compose.redis-cluster.yml new file mode 100644 index 0000000..eea1dfe --- /dev/null +++ b/docker/docker-compose.redis-cluster.yml @@ -0,0 +1,90 @@ +# +# 抖音关注:程序员三丙 +# 知识星球:https://t.zsxq.com/j9b21 +# + +networks: + sanbing-network: + driver: bridge + name: sanbing-network + ipam: + config: + - subnet: 10.10.0.0/24 + +services: +# Redis cluster + redis-node-0: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis-cluster:7.4 + restart: always + networks: + - sanbing-network + environment: + - 'REDIS_PASSWORD=sanbing' + - 'REDISCLI_AUTH=sanbing' + - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' + + redis-node-1: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis-cluster:7.4 + restart: always + networks: + - sanbing-network + depends_on: + - redis-node-0 + environment: + - 'REDIS_PASSWORD=sanbing' + - 'REDISCLI_AUTH=sanbing' + - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' + + redis-node-2: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis-cluster:7.4 + restart: always + networks: + - sanbing-network + depends_on: + - redis-node-1 + environment: + - 'REDIS_PASSWORD=sanbing' + - 'REDISCLI_AUTH=sanbing' + - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' + + redis-node-3: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis-cluster:7.4 + restart: always + networks: + - sanbing-network + depends_on: + - redis-node-2 + environment: + - 'REDIS_PASSWORD=sanbing' + - 'REDISCLI_AUTH=sanbing' + - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' + + redis-node-4: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis-cluster:7.4 + restart: always + networks: + - sanbing-network + depends_on: + - redis-node-3 + environment: + - 'REDIS_PASSWORD=sanbing' + - 'REDISCLI_AUTH=sanbing' + - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' + + redis-node-5: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis-cluster:7.4 + restart: always + networks: + - sanbing-network + depends_on: + - redis-node-0 + - redis-node-1 + - redis-node-2 + - redis-node-3 + - redis-node-4 + environment: + - 'REDIS_PASSWORD=sanbing' + - 'REDISCLI_AUTH=sanbing' + - 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5' + - 'REDIS_CLUSTER_REPLICAS=1' + - 'REDIS_CLUSTER_CREATOR=yes' diff --git a/docker/docker-compose.redis-sentinel.yml b/docker/docker-compose.redis-sentinel.yml new file mode 100644 index 0000000..d769b33 --- /dev/null +++ b/docker/docker-compose.redis-sentinel.yml @@ -0,0 +1,49 @@ +# +# 抖音关注:程序员三丙 +# 知识星球:https://t.zsxq.com/j9b21 +# + +networks: + sanbing-network: + driver: bridge + name: sanbing-network + ipam: + config: + - subnet: 10.10.0.0/24 + +services: + redis-master: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis:7.4 + restart: always + networks: + - sanbing-network + environment: + - 'REDIS_REPLICATION_MODE=master' + - 'REDIS_PASSWORD=sanbing' + + redis-slave: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis:7.4 + restart: always + networks: + - sanbing-network + environment: + - 'REDIS_REPLICATION_MODE=slave' + - 'REDIS_MASTER_HOST=redis-master' + - 'REDIS_MASTER_PASSWORD=sanbing' + - 'REDIS_PASSWORD=sanbing' + depends_on: + - redis-master + + redis-sentinel: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis-sentinel:7.4 + restart: always + networks: + - sanbing-network + environment: + - 'REDIS_MASTER_HOST=redis-master' + - 'REDIS_MASTER_SET=mymaster' + - 'REDIS_SENTINEL_PASSWORD=sanbing' + - 'REDIS_MASTER_PASSWORD=sanbing' + depends_on: + - redis-master + - redis-slave diff --git a/docker/docker-compose.redis-standalone.yml b/docker/docker-compose.redis-standalone.yml new file mode 100644 index 0000000..8f118a5 --- /dev/null +++ b/docker/docker-compose.redis-standalone.yml @@ -0,0 +1,23 @@ +# +# 抖音关注:程序员三丙 +# 知识星球:https://t.zsxq.com/j9b21 +# + +networks: + sanbing-network: + driver: bridge + name: sanbing-network + ipam: + config: + - subnet: 10.10.0.0/24 + +services: + redis: + image: registry.cn-hangzhou.aliyuncs.com/sanbing/redis:7.4 + restart: always + networks: + - sanbing-network + ports: + - '6379:6379' + environment: + - 'REDIS_PASSWORD=sanbing' diff --git a/docker/kafka.env b/docker/kafka.env new file mode 100644 index 0000000..08123fd --- /dev/null +++ b/docker/kafka.env @@ -0,0 +1,14 @@ +KAFKA_CFG_NODE_ID=0 +ALLOW_PLAINTEXT_LISTENER=yes +KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 +KAFKA_CFG_LISTENERS=INSIDE://:9093,OUTSIDE://:9092 +KAFKA_CFG_ADVERTISED_LISTENERS=INSIDE://:9093,OUTSIDE://kafka:9092 +KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT +KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true +KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INSIDE +KAFKA_CFG_LOG_RETENTION_BYTES=1073741824 +KAFKA_CFG_SEGMENT_BYTES=268435456 +KAFKA_CFG_LOG_RETENTION_MS=300000 +KAFKA_CFG_LOG_CLEANUP_POLICY=delete + + diff --git a/docker/queue-kafka.env b/docker/queue-kafka.env new file mode 100644 index 0000000..1294636 --- /dev/null +++ b/docker/queue-kafka.env @@ -0,0 +1,2 @@ +QUEUE_TYPE=kafka +KAFKA_SERVERS=kafka:9092 diff --git a/docker/schema/schema-postgres.sql b/docker/schema/schema-postgres.sql new file mode 100644 index 0000000..ee2c0be --- /dev/null +++ b/docker/schema/schema-postgres.sql @@ -0,0 +1,121 @@ +-- +-- 抖音关注:程序员三丙 +-- 知识星球:https://t.zsxq.com/j9b21 +-- + +CREATE TABLE IF NOT EXISTS jcpp_user +( + id uuid not null + constraint owner_pkey + primary key, + created_time timestamp default CURRENT_TIMESTAMP not null, + additional_info jsonb, + status varchar(16) not null, + user_name varchar(255) not null, + user_credentials jsonb not null, + version int default 1 +); + +CREATE UNIQUE INDEX IF NOT EXISTS uni_user_name + on jcpp_user (user_name); + +CREATE TABLE IF NOT EXISTS jcpp_station +( + id uuid not null + constraint station_pkey + primary key, + created_time timestamp default CURRENT_TIMESTAMP not null, + additional_info jsonb, + station_name varchar(255) not null, + station_code varchar(255) not null, + owner_id uuid not null, + longitude double precision not null, + latitude double precision not null, + owner_type varchar(16) not null, + province varchar(255), + city varchar(255), + county varchar(255), + address varchar(255), + status varchar(16) not null, + version int default 1 +); + +CREATE UNIQUE INDEX IF NOT EXISTS uni_station_code + on jcpp_station (station_code); + +CREATE TABLE IF NOT EXISTS jcpp_pile +( + id uuid not null + constraint pile_pkey + primary key, + created_time timestamp default CURRENT_TIMESTAMP not null, + additional_info jsonb, + pile_name varchar(255) not null, + pile_code varchar(255) not null, + protocol varchar(255) not null, + station_id uuid not null, + owner_id uuid not null, + owner_type varchar(16) not null, + brand varchar(255), + model varchar(255), + manufacturer varchar(255), + status varchar(16) not null, + type varchar(16) not null, + version int default 1 +); + +CREATE UNIQUE INDEX IF NOT EXISTS uni_pile_code + on jcpp_pile (pile_code); + + +CREATE TABLE IF NOT EXISTS jcpp_gun +( + id uuid not null + primary key, + created_time timestamp default CURRENT_TIMESTAMP not null, + additional_info varchar(255), + gun_no varchar(255) not null, + gun_name varchar(255) not null, + gun_code varchar(255) not null, + station_id uuid not null, + pile_id uuid not null, + owner_id uuid not null, + owner_type varchar(16) not null, + run_status varchar(16) not null, + run_status_updated_time timestamp not null, + opt_status varchar(16) not null, + version int default 1 +); + +CREATE UNIQUE INDEX IF NOT EXISTS uni_gun_code + on jcpp_gun (gun_code); + +CREATE TABLE IF NOT EXISTS jcpp_order +( + id uuid not null + primary key, + internal_order_no varchar(255) not null, + external_order_no varchar(255) not null, + pile_order_No varchar(255) not null, + created_time timestamp default CURRENT_TIMESTAMP not null, + additional_info jsonb, + updated_time timestamp, + cancelled_time timestamp, + status varchar(16) not null, + type varchar(16) not null, + creator_id uuid not null, + station_id uuid not null, + pile_id uuid not null, + gun_id uuid not null, + plate_no varchar(64), + settlement_amount bigint default 0 not null, + settlement_details jsonb, + electricity_quantity numeric(16, 9) default 0 not null +); + +CREATE UNIQUE INDEX IF NOT EXISTS uni_internal_order_no + on jcpp_order (internal_order_no); + +CREATE UNIQUE INDEX IF NOT EXISTS uni_external_order_no + on jcpp_order (external_order_no); + diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 0000000..33ea7fa --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# 抖音关注:程序员三丙 +# 知识星球:https://t.zsxq.com/j9b21 +# + +echo "Starting Server ..." + +export JAVA_APP_OPTS="-XX:+UseContainerSupport -XX:InitialRAMPercentage=10 -XX:MaxRAMPercentage=70 \ + -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/sanbing/gc/gc.log:time,uptime,level,tags:filecount=10,filesize=10M \ + -XX:+HeapDumpOnOutOfMemoryError \ + -XX:HeapDumpPath=/var/log/sanbing/heapdump/ \ + -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark \ + -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10 \ + -Xss512k -XX:MaxDirectMemorySize=128M -XX:G1ReservePercent=20 \ + -XX:-OmitStackTraceInFastThrow \ + -Dlogging.config=/app/config/log4j2.xml" + +#export JAVA_OPTS_EXTEND="-Xdebug -Xrunjdwp:transport=dt_socket,address=0.0.0.0:8000,server=y,suspend=n" + +exec java $JAVA_APP_OPTS $JAVA_OPTS_EXTEND $JAVA_OPTS -Dnetworkaddress.cache.ttl=60 -jar /app/application.jar diff --git a/jcpp-app-bootstrap/pom.xml b/jcpp-app-bootstrap/pom.xml new file mode 100644 index 0000000..1297db4 --- /dev/null +++ b/jcpp-app-bootstrap/pom.xml @@ -0,0 +1,84 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-app-bootstrap + jar + JChargePointProtocol Application Bootstrap Module + App引导程序 + + + ${basedir}/.. + 3.4.4 + + + + + sanbing + jcpp-app + + + sanbing + jcpp-protocol-yunkuaichong + + + org.testcontainers + junit-jupiter + test + + + + + application + + + org.springframework.boot + spring-boot-maven-plugin + + false + ZIP + sanbing.jcpp.JCPPServerApplication + true + + true + ${project.basedir}/src/layers.xml + + + + + + repackage + build-info + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/jcpp-app-bootstrap/src/layers.xml b/jcpp-app-bootstrap/src/layers.xml new file mode 100644 index 0000000..ebf721a --- /dev/null +++ b/jcpp-app-bootstrap/src/layers.xml @@ -0,0 +1,33 @@ + + + + + + org/springframework/boot/loader/** + + + + + + + + + *:*:*SNAPSHOT + + + + + dependencies + spring-boot-loader + snapshot-dependencies + application + + diff --git a/jcpp-app-bootstrap/src/main/java/sanbing/jcpp/JCPPServerApplication.java b/jcpp-app-bootstrap/src/main/java/sanbing/jcpp/JCPPServerApplication.java new file mode 100644 index 0000000..4631858 --- /dev/null +++ b/jcpp-app-bootstrap/src/main/java/sanbing/jcpp/JCPPServerApplication.java @@ -0,0 +1,39 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp; + +import org.springframework.boot.Banner; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.util.Arrays; + +/** + * @author baigod + */ +@SpringBootApplication +@EnableAsync +@EnableScheduling +public class JCPPServerApplication { + + private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; + private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "app-service"; + + public static void main(String[] args) { + new SpringApplicationBuilder(JCPPServerApplication.class).bannerMode(Banner.Mode.LOG).run(updateArguments(args)); + } + + private static String[] updateArguments(String[] args) { + if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) { + String[] modifiedArgs = new String[args.length + 1]; + System.arraycopy(args, 0, modifiedArgs, 0, args.length); + modifiedArgs[args.length] = DEFAULT_SPRING_CONFIG_PARAM; + return modifiedArgs; + } + return args; + } +} \ No newline at end of file diff --git a/jcpp-app-bootstrap/src/main/resources/app-service.yml b/jcpp-app-bootstrap/src/main/resources/app-service.yml new file mode 100644 index 0000000..175b136 --- /dev/null +++ b/jcpp-app-bootstrap/src/main/resources/app-service.yml @@ -0,0 +1,214 @@ +server: + address: "${HTTP_BIND_ADDRESS:0.0.0.0}" + port: "${HTTP_BIND_PORT:8080}" + undertow: + buffer-size: "${SERVER_UNDERTOW_BUFFER_SIZE:16384}" + directBuffers: "${SERVER_UNDERTOW_DIRECT_BUFFERS:true}" + threads: + io: "${SERVER_UNDERTOW_THREADS_IO:4}" + worker: "${SERVER_UNDERTOW_THREADS_WORKER:128}" + max-http-post-size: "${SERVER_UNDERTOW_MAX_HTTP_POST_SIZE:10MB}" + no-request-timeout: "${SERVER_UNDERTOW_NO_REQUEST_TIMEOUT:10000}" + accesslog: + enabled: true + pattern: "%t %a %r %s (%D ms)" + dir: /var/log/sanbing/accesslog + options: + server: + record-request-start-time: true + +spring: + application: + name: "${SPRING_APPLICATION_NAME:java-charge-point-server}" + datasource: + driver-class-name: "${SPRING_DRIVER_CLASS_NAME:org.postgresql.Driver}" + url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://10.102.12.102:30135/jcpp}" + username: "${SPRING_DATASOURCE_USERNAME:postgres}" + password: "${SPRING_DATASOURCE_PASSWORD:postgres}" + hikari: + leak-detection-threshold: "${SPRING_DATASOURCE_HIKARI_LEAK_DETECTION_THRESHOLD:0}" + maximum-pool-size: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:16}" + register-mbeans: "${SPRING_DATASOURCE_HIKARI_REGISTER_MBEANS:false}" + +mybatis-plus: + type-handlers-package: sanbing.jcpp.app.dal.config.ibatis.typehandlers + +management: + endpoints: + web: + exposure: + include: '${METRICS_ENDPOINTS_EXPOSE:prometheus,health}' + endpoint: + health: + show-details: always + +metrics: + enabled: "${METRICS_ENABLED:true}" + timer: + percentiles: "${METRICS_TIMER_PERCENTILES:0.5}" + +# 应用程序服务注册中心配置 +zk: + enabled: "${ZOOKEEPER_ENABLED:true}" + url: "${ZOOKEEPER_URL:zookeeper:2181}" + retry-interval-ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" + connection-timeout-ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" + session-timeout-ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" + zk-dir: "${ZOOKEEPER_NODES_DIR:/jcpp}" + recalculate-delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" + +# 队列配置 +queue: + # 可选 kafka、memory + type: "${QUEUE_TYPE:memory}" + partitions: + hash_function_name: "${QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 + in_memory: + stats: + print-interval-ms: "${QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}" + kafka: + bootstrap-servers: "${KAFKA_SERVERS:kafka:9092}" + ssl: + enabled: "${KAFKA_SSL_ENABLED:false}" + truststore-location: "${KAFKA_SSL_TRUSTSTORE_LOCATION:}" + truststore-password: "${KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + keystore-location: "${KAFKA_SSL_KEYSTORE_LOCATION:}" + keystore-password: "${KAFKA_SSL_KEYSTORE_PASSWORD:}" + key-password: "${KAFKA_SSL_KEY_PASSWORD:}" + acks: "${KAFKA_ACKS:1}" + retries: "${KAFKA_RETRIES:1}" + compression-type: "${KAFKA_COMPRESSION_TYPE:lz4}" # none, gzip, snappy, lz4, zstd + batch-size: "${KAFKA_BATCH_SIZE:1048576}" + linger-ms: "${KAFKA_LINGER_MS:1}" + max-request-size: "${KAFKA_MAX_REQUEST_SIZE:1048576}" + max-in-flight-requests-per-connection: "${KAFKA_MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION:5}" + buffer-memory: "${BUFFER_MEMORY:33554432}" + replication-factor: "${QUEUE_KAFKA_REPLICATION_FACTOR:1}" + max-poll-interval-ms: "${QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}" + max-poll-records: "${QUEUE_KAFKA_MAX_POLL_RECORDS:10240}" + max-partition-fetch-bytes: "${QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" + fetch-max-bytes: "${QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request-timeout-ms: "${QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" + session-timeout-ms: "${QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" + auto-offset-reset: "${QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" + other-inline: "${QUEUE_KAFKA_OTHER_PROPERTIES:}" + topic-properties: + app: "${QUEUE_KAFKA_APP_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + consumer-stats: + enabled: "${QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" + print-interval-ms: "${QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}" + kafka-response-timeout-ms: "${QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}" + app: + topic: "${QUEUE_APP_TOPIC:protocol_uplink}" + poll-interval: "${QUEUE_APP_POLL_INTERVAL_MS:5}" + pack-processing-timeout: "${QUEUE_APP_PACK_PROCESSING_TIMEOUT_MS:2000}" + consumer-per-partition: "${QUEUE_APP_CONSUMER_PER_PARTITION:true}" + partitions: "${QUEUE_APP_PARTITIONS:10}" + # 可选 protobuf(推荐)、json,需要跟..forwarder.kafka.encoder保持一致 + decoder: "${QUEUE_APP_DECODER:protobuf}" + stats: + enabled: "${QUEUE_APP_STATS_ENABLED:true}" + print-interval-ms: "${QUEUE_APP_STATS_PRINT_INTERVAL_MS:60000}" + +# 应用程序缓存配置 +cache: + type: "${CACHE_TYPE:caffeine}" # caffeine or redis + specs: + piles: + timeToLiveInMinutes: "${CACHE_SPECS_PILES_TTL:15}" + maxSize: "${CACHE_SPECS_PILES_MAX_SIZE:1000}" + pileSessions: + timeToLiveInMinutes: "${CACHE_SPECS_PILE_SESSIONS_TTL:1440}" + maxSize: "${CACHE_SPECS_PILE_SESSIONS_MAX_SIZE:100000}" + +redis: + connection: + type: "${REDIS_CONNECTION_TYPE:standalone}" + standalone: + host: "${REDIS_HOST:redis}" + port: "${REDIS_PORT:6379}" + useDefaultClientConfig: "${REDIS_USE_DEFAULT_CLIENT_CONFIG:true}" + clientName: "${REDIS_CLIENT_NAME:standalone}" + commandTimeout: "${REDIS_CLIENT_COMMAND_TIMEOUT:30000}" + shutdownTimeout: "${REDIS_CLIENT_SHUTDOWN_TIMEOUT:1000}" + readTimeout: "${REDIS_CLIENT_READ_TIMEOUT:60000}" + usePoolConfig: "${REDIS_CLIENT_USE_POOL_CONFIG:false}" + cluster: + nodes: "${REDIS_NODES:redis-node-0:6379,redis-node-1:6379,redis-node-2:6379,redis-node-3:6379,redis-node-4:6379,redis-node-5:6379}" + max-redirects: "${REDIS_MAX_REDIRECTS:12}" + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:false}" + sentinel: + master: "${REDIS_MASTER:mymaster}" + sentinels: "${REDIS_SENTINELS:redis-sentinel:26379}" + password: "${REDIS_SENTINEL_PASSWORD:sanbing}" + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:false}" + db: "${REDIS_DB:0}" + password: "${REDIS_PASSWORD:sanbing}" + pool_config: + maxTotal: "${REDIS_POOL_CONFIG_MAX_TOTAL:128}" + maxIdle: "${REDIS_POOL_CONFIG_MAX_IDLE:128}" + minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:false}" + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:false}" + testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" + minEvictableMs: "${REDIS_POOL_CONFIG_MIN_EVICTABLE_MS:60000}" + evictionRunsMs: "${REDIS_POOL_CONFIG_EVICTION_RUNS_MS:30000}" + maxWaitMills: "${REDIS_POOL_CONFIG_MAX_WAIT_MS:60000}" + numberTestsPerEvictionRun: "${REDIS_POOL_CONFIG_NUMBER_TESTS_PER_EVICTION_RUN:3}" + blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" + evictTtlInMs: "${REDIS_EVICT_TTL_MS:60000}" + +service: + # 服务类型:纯协议解析前置 - protocol,纯应用后端 - app,单体服务(包含protocol和app) - monolith + type: "${SERVICE_TYPE:monolith}" + # 可自定义的服务ID,如果不指定,则默认为HOSTNAME + id: "${SERVICE_ID:}" + protocols: + sessions: + default-inactivity-timeout-in-sec: "${PROTOCOLS_SESSIONS_DEFAULT_INACTIVITY_TIMEOUT_IN_SEC:600}" + default-state-check-interval-in-sec: "${PROTOCOLS_SESSIONS_DEFAULT_STATE_CHECK_INTERVAL_IN_SEC:60}" + yunkuaichongV150: + enabled: "${PROTOCOLS_YUNKUAICHONGV150_ENABLED:true}" + listener: + tcp: + bind-address: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_BIND_ADDRESS:0.0.0.0}" + bind-port: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_BIND_PORT:38001}" + boss-group-thread_count: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_BOSS_GROUP_THREADS:4}" + worker-group-thread-count: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_WORKER_GROUP_THREADS:16}" + so-keep-alive: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_SO_KEEPALIVE:true}" + so-backlog: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_SO_BACKLOG:128}" + so-rcvbuf: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_SO_RCVBUF:65536}" + so-sndbuf: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_SO_SNDBUF:65536}" + nodelay: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_NODELAY:true}" + handler: + idle-timeout-seconds: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_HANDLER_IDLE_TIMEOUT_SECONDS:600}" + max_connections: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_HANDLER_MAX_CONNECTIONS:100000}" + # 默认为二进制类型的拆包器 + # 可选JSON类型的拆包器 "${PROTOCOLS_YUNKUAICHONGV150_NETTY_HANDLER_BINARY_CONFIGURATION:type:JSON}" + # 可选纯文本类型的拆包器 "${PROTOCOLS_YUNKUAICHONGV150_NETTY_HANDLER_BINARY_CONFIGURATION:type:TEXT;maxFrameLength:128;stripDelimiter:true;messageSeparator:null;charsetName:UTF-8}" + configuration: "${PROTOCOLS_YUNKUAICHONGV150_NETTY_HANDLER_BINARY_CONFIGURATION:type:BINARY;decoder:sanbing.jcpp.protocol.listener.tcp.decoder.JCPPLengthFieldBasedFrameDecoder;byteOrder:LITTLE_ENDIAN;head:68;lengthFieldOffset:1;lengthFieldLength:1;lengthAdjustment:2;initialBytesToStrip:0}" + forwarder: + # 如果是单体服务,可选kafka、memory,未来计划扩展RocketMQ, GRpc、REST + type: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_TYPE:memory}" + memory: + topic: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_MEMORY_TOPIC:protocol_uplink}" + kafka: + topic: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_TOPIC:protocol_uplink}" + jcpp-partition: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_JCPP_PARTITION:true}" # 是否利用JCPP的分片框架 + # 以下配置只有在service.type为protocol时且jcpp-partition为false时才生效 + bootstrap-servers: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_SERVERS:10.102.12.102:9092}" + acks: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_ACKS:1}" + # # 可选 protobuf(推荐)、json + encoder: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_ENCODER:protobuf}" + retries: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_RETRIES:1}" + compression-type: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_COMPRESSION_TYPE:lz4}" # none, gzip, snappy, lz4, zstd + batch-size: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_BATCH_SIZE:16384}" + linger-ms: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_LINGER_MS:0}" + buffer-memory: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_BUFFER_MEMORY:33554432}" + other-properties: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_QUEUE_KAFKA_OTHER_PROPERTIES:}" + +thread-pool: + sharding: + hash_function_name: "${THREAD_POOL_SHARDING_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 + parallelism: "${THREAD_POOL_SHARDING_PARALLELISM:128}" + stats-print-interval-ms: "${THREAD_POOL_SHARDING_STATS_PRINT_INTERVAL_MS:10000}" diff --git a/jcpp-app-bootstrap/src/main/resources/banner.txt b/jcpp-app-bootstrap/src/main/resources/banner.txt new file mode 100644 index 0000000..0c37cd9 --- /dev/null +++ b/jcpp-app-bootstrap/src/main/resources/banner.txt @@ -0,0 +1,12 @@ + + ___ ________ ________ ________ + |\ \|\ ____\|\ __ \|\ __ \ + \ \ \ \ \___|\ \ \|\ \ \ \|\ \ + __ \ \ \ \ \ \ \ ____\ \ ____\ +|\ \\_\ \ \ \____\ \ \___|\ \ \___| +\ \________\ \_______\ \__\ \ \__\ + \|________|\|_______|\|__| \|__| + +=================================================== +:: ${application.title} :: ${application.formatted-version} +=================================================== \ No newline at end of file diff --git a/jcpp-app-bootstrap/src/main/resources/log4j2.xml b/jcpp-app-bootstrap/src/main/resources/log4j2.xml new file mode 100644 index 0000000..082044b --- /dev/null +++ b/jcpp-app-bootstrap/src/main/resources/log4j2.xml @@ -0,0 +1,56 @@ + + + + + /var/log/sanbing/jcpp + %d{yyyy-MM-dd HH:mm:ss:SSS} [%X{TRACE_ID}] [%t] %p %c{1} %m%n%throwable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/AbstractTestBase.java b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/AbstractTestBase.java new file mode 100644 index 0000000..42f038e --- /dev/null +++ b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/AbstractTestBase.java @@ -0,0 +1,27 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +/** + * @author baigod + */ +@ActiveProfiles("test") +@SpringBootTest(classes = JCPPServerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class AbstractTestBase { + + static { + System.setProperty("spring.config.name", "app-service"); + } + + protected final Logger log = LoggerFactory.getLogger(this.getClass()); +} \ No newline at end of file diff --git a/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/GunMapperTest.java b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/GunMapperTest.java new file mode 100644 index 0000000..592c781 --- /dev/null +++ b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/GunMapperTest.java @@ -0,0 +1,72 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import sanbing.jcpp.AbstractTestBase; +import sanbing.jcpp.app.dal.config.ibatis.enums.GunOptStatusEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.GunRunStatusEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum; +import sanbing.jcpp.app.dal.entity.Gun; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static sanbing.jcpp.app.dal.mapper.PileMapperTest.NORMAL_PILE_ID; +import static sanbing.jcpp.app.dal.mapper.StationMapperTest.NORMAL_STATION_ID; +import static sanbing.jcpp.app.dal.mapper.UserMapperTest.NORMAL_USER_ID; + +/** + * @author baigod + */ +public class GunMapperTest extends AbstractTestBase { + static final UUID[] NORMAL_GUN_ID = new UUID[]{ + UUID.fromString("8f1ffb5b-e536-4f2b-8cd0-31f7d0348a44"), + UUID.fromString("ae256617-b747-4110-b27a-00773e03bed1"), + UUID.fromString("d15dbb29-ea2f-4094-b448-dff853e9275f"), + UUID.fromString("b4a2de24-d7ff-4828-a0d8-2429a6253f9c"), + UUID.fromString("f505f7e2-9e1c-4251-8f7f-9a8eae84372a"), + UUID.fromString("0c5bab7b-786b-4e05-ab26-618c3f5a6086"), + UUID.fromString("2db4ad92-e353-4ac2-a2b0-942cb778eca6"), + UUID.fromString("203833e7-0a44-4f1c-935e-cd43e6dbbf46"), + UUID.fromString("3f3a61e9-de55-4177-9b4e-3a1d8c529890"), + UUID.fromString("cf1a8970-5aa9-4636-a76e-d6bcf98b4a07") + }; + @Resource + GunMapper gunMapper; + + @Test + void curdTest() { + gunMapper.delete(Wrappers.lambdaQuery()); + + for (int i = 0; i < NORMAL_PILE_ID.length; i++) { + UUID pileId = NORMAL_PILE_ID[i]; + UUID gunId = NORMAL_GUN_ID[i]; + Gun gun = Gun.builder() + .id(gunId) + .createdTime(LocalDateTime.now()) + .additionalInfo(JacksonUtil.newObjectNode()) + .gunNo("01") + .gunName("三丙的1号枪") + .gunCode("20231212000001-" + (i + 1)) + .stationId(NORMAL_STATION_ID) + .pileId(pileId) + .ownerId(NORMAL_USER_ID) + .ownerType(OwnerTypeEnum.C) + .runStatus(GunRunStatusEnum.IDLE) + .runStatusUpdatedTime(LocalDateTime.now()) + .optStatus(GunOptStatusEnum.AVAILABLE) + .build(); + + gunMapper.insertOrUpdate(gun); + + log.info("{}", gunMapper.selectById(gunId)); + } + + } +} \ No newline at end of file diff --git a/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/OrderMapperTest.java b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/OrderMapperTest.java new file mode 100644 index 0000000..eb2a24f --- /dev/null +++ b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/OrderMapperTest.java @@ -0,0 +1,66 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + +import cn.hutool.core.math.Money; +import cn.hutool.core.util.IdUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.Test; +import sanbing.jcpp.AbstractTestBase; +import sanbing.jcpp.app.dal.config.ibatis.enums.OrderStatusEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.OrderTypeEnum; +import sanbing.jcpp.app.dal.entity.Order; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import static sanbing.jcpp.app.dal.mapper.GunMapperTest.NORMAL_GUN_ID; +import static sanbing.jcpp.app.dal.mapper.PileMapperTest.NORMAL_PILE_ID; +import static sanbing.jcpp.app.dal.mapper.StationMapperTest.NORMAL_STATION_ID; +import static sanbing.jcpp.app.dal.mapper.UserMapperTest.NORMAL_USER_ID; + +/** + * @author baigod + */ +public class OrderMapperTest extends AbstractTestBase { + + @Resource + OrderMapper orderMapper; + + @Test + void testOrderMapper() { + orderMapper.delete(Wrappers.lambdaQuery()); + + Order order = Order.builder() + .id(UUID.randomUUID()) + .internalOrderNo(IdUtil.getSnowflake(1, 1).nextIdStr()) + .externalOrderNo(IdUtil.getSnowflake(1, 1).nextIdStr()) + .pileOrderNo(RandomStringUtils.randomNumeric(16)) + .createdTime(LocalDateTime.now()) + .additionalInfo(JacksonUtil.newObjectNode()) + .updatedTime(LocalDateTime.now()) + .cancelledTime(null) + .status(OrderStatusEnum.IN_CHARGING) + .type(OrderTypeEnum.CHARGE) + .creatorId(NORMAL_USER_ID) + .stationId(NORMAL_STATION_ID) + .pileId(NORMAL_PILE_ID[0]) + .gunId(NORMAL_GUN_ID[0]) + .plateNo("浙A88888") + .settlementAmount(new Money(100D).getCent()) + .settlementDetails(JacksonUtil.newObjectNode()) + .electricityQuantity(new BigDecimal("100")) + .build(); + + orderMapper.insertOrUpdate(order); + + log.info("{}", orderMapper.selectById(order.getId())); + + } +} \ No newline at end of file diff --git a/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/PileMapperTest.java b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/PileMapperTest.java new file mode 100644 index 0000000..b3afb7b --- /dev/null +++ b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/PileMapperTest.java @@ -0,0 +1,74 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import sanbing.jcpp.AbstractTestBase; +import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.PileStatusEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.PileTypeEnum; +import sanbing.jcpp.app.dal.entity.Pile; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; + +import java.text.DecimalFormat; +import java.time.LocalDateTime; +import java.util.UUID; + +import static sanbing.jcpp.app.dal.mapper.StationMapperTest.NORMAL_STATION_ID; +import static sanbing.jcpp.app.dal.mapper.UserMapperTest.NORMAL_USER_ID; + +/** + * @author baigod + */ +public class PileMapperTest extends AbstractTestBase { + static final UUID[] NORMAL_PILE_ID = new UUID[]{ + UUID.fromString("fd7b3f60-db6c-4347-bff3-3c922985b95c"), + UUID.fromString("fa621927-6458-4e09-9666-99c52230db2b"), + UUID.fromString("afec0b0a-ad82-4923-97da-70e4a5d5e2c6"), + UUID.fromString("3e45ae30-2848-4d5a-a7b8-bd8504a6713d"), + UUID.fromString("349ff65e-ce8e-435a-928b-52fdef2828f2"), + UUID.fromString("e60d5b2d-8014-4f8f-b828-e207e6cf4a8f"), + UUID.fromString("8f010829-b505-4e57-8b93-6bdf981ac4e1"), + UUID.fromString("081842e2-9e74-4abb-aeab-b2cbfeb7a335"), + UUID.fromString("f04cf40a-0fbe-40f7-a07c-5b663ad68e98"), + UUID.fromString("ec522751-e1d3-4117-a887-3bdae7892369") + }; + + @Resource + PileMapper pileMapper; + + @Test + void curdTest() { + pileMapper.delete(Wrappers.lambdaQuery()); + + for (int i = 0; i < 10; i++) { + UUID pileId = NORMAL_PILE_ID[i]; + Pile pile = Pile.builder() + .id(pileId) + .createdTime(LocalDateTime.now()) + .additionalInfo(JacksonUtil.newObjectNode()) + .pileName(String.format("三丙家的%d号充电桩", i + 1)) + .pileCode("202312120000" + new DecimalFormat("00").format(i + 1)) + .protocol("yunkuaichongV150") + .stationId(NORMAL_STATION_ID) + .ownerId(NORMAL_USER_ID) + .ownerType(OwnerTypeEnum.C) + .brand("星星") + .model("10A") + .manufacturer("星星") + .status(PileStatusEnum.IDLE) + .type(PileTypeEnum.AC) + .build(); + + pileMapper.insertOrUpdate(pile); + + log.info("{}", pileMapper.selectById(pileId)); + + } + } +} \ No newline at end of file diff --git a/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/StationMapperTest.java b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/StationMapperTest.java new file mode 100644 index 0000000..63a6f02 --- /dev/null +++ b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/StationMapperTest.java @@ -0,0 +1,55 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import sanbing.jcpp.AbstractTestBase; +import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.StationStatusEnum; +import sanbing.jcpp.app.dal.entity.Station; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static sanbing.jcpp.app.dal.mapper.UserMapperTest.NORMAL_USER_ID; + +/** + * @author baigod + */ +class StationMapperTest extends AbstractTestBase { + static final UUID NORMAL_STATION_ID = UUID.fromString("07d80c81-fe99-4a1f-a6aa-dc4d798b5626"); + + @Resource + StationMapper stationMapper; + + @Test + void curdTest() { + stationMapper.delete(Wrappers.lambdaQuery()); + + Station station = Station.builder() + .id(NORMAL_STATION_ID) + .createdTime(LocalDateTime.now()) + .additionalInfo(JacksonUtil.newObjectNode()) + .stationName("三丙家专属充电站") + .stationCode("S20241001001") + .ownerId(NORMAL_USER_ID) + .longitude(120.107936F) + .latitude(30.267014F) + .ownerType(OwnerTypeEnum.C) + .province("浙江省") + .city("杭州市") + .county("西湖区") + .address("西溪路552-1号") + .status(StationStatusEnum.OPERATIONAL) + .build(); + + stationMapper.insertOrUpdate(station); + + log.info("{}", stationMapper.selectById(NORMAL_STATION_ID)); + } +} \ No newline at end of file diff --git a/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/UserMapperTest.java b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/UserMapperTest.java new file mode 100644 index 0000000..be710a4 --- /dev/null +++ b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/app/dal/mapper/UserMapperTest.java @@ -0,0 +1,44 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import sanbing.jcpp.AbstractTestBase; +import sanbing.jcpp.app.dal.config.ibatis.enums.UserStatusEnum; +import sanbing.jcpp.app.dal.entity.User; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * @author baigod + */ +class UserMapperTest extends AbstractTestBase { + static final UUID NORMAL_USER_ID = UUID.fromString("21cbf909-a23a-4396-840a-f34061f59f95"); + + @Resource + private UserMapper userMapper; + + @Test + void curdTest() { + userMapper.delete(Wrappers.lambdaQuery()); + + User user = User.builder() + .id(NORMAL_USER_ID) + .createdTime(LocalDateTime.now()) + .additionalInfo(JacksonUtil.newObjectNode()) + .status(UserStatusEnum.ENABLE) + .userName("sanbing") + .userCredentials(JacksonUtil.newObjectNode()) + .build(); + + userMapper.insertOrUpdate(user); + + log.info("{}", userMapper.selectById(NORMAL_USER_ID)); + } +} \ No newline at end of file diff --git a/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/infrastructure/cache/RedisCacheConfigurationTest.java b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/infrastructure/cache/RedisCacheConfigurationTest.java new file mode 100644 index 0000000..1c41e34 --- /dev/null +++ b/jcpp-app-bootstrap/src/test/java/sanbing/jcpp/infrastructure/cache/RedisCacheConfigurationTest.java @@ -0,0 +1,98 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.core.*; +import sanbing.jcpp.AbstractTestBase; + +import java.time.Duration; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.stream.IntStream; + +class RedisCacheConfigurationTest extends AbstractTestBase { + + @Resource + RedisTemplate redisTemplate; + + @Resource + ReactiveRedisTemplate reactiveRedisTemplate; + + final static int testTimes = 10_000; + final static String hashKey = "hashKey"; + + @Test + @Order(1) + void kvTest() { + ValueOperations valueOperations = redisTemplate.opsForValue(); + + IntStream.range(0, testTimes).forEach(i -> { + String key = "field:" + i; + String value = "value:" + i; + valueOperations.set(key, value, Duration.ofMinutes(1)); + }); + + Object o = valueOperations.get("field:1000"); + System.out.println(Objects.requireNonNull(o).getClass() + " : " + o); + } + + @Test + @Order(2) + void hashTest() { + HashOperations hashOperations = redisTemplate.opsForHash(); + + IntStream.range(0, testTimes).forEach(i -> { + String key = "field:" + i; + String value = "value:" + i; + hashOperations.put(hashKey, key, value); + }); + + redisTemplate.expire(hashKey, Duration.ofMinutes(1)); + + Map slowKey = hashOperations.entries(hashKey); + System.out.println("map size:" + slowKey.size()); + } + + + @Test + @Order(3) + void reactiveKVTest() { + ReactiveValueOperations valueOperations = reactiveRedisTemplate.opsForValue(); + + IntStream.range(0, testTimes).forEach(i -> { + String key = "field:" + i; + String value = "value:" + i; + valueOperations.set(key, value, Duration.ofMinutes(1)).block(); + }); + + Object o = valueOperations.get("field:1000").block(); + System.out.println(Objects.requireNonNull(o).getClass() + " : " + o); + } + + @Test + @Order(4) + void reactiveHashTest() throws InterruptedException { + ReactiveHashOperations hashOperations = reactiveRedisTemplate.opsForHash(); + + IntStream.range(0, testTimes).forEach(i -> { + String key = "field:" + i; + String value = "value:" + i; + hashOperations.put(hashKey, key, value).block(); + }); + + redisTemplate.expire(hashKey, Duration.ofMinutes(1)); + + CountDownLatch latch = new CountDownLatch(1); + hashOperations.entries(hashKey).collectList().subscribe(entries -> { + System.out.println("size:" + entries.size()); + latch.countDown(); + }); + latch.await(); + } +} \ No newline at end of file diff --git a/jcpp-app-bootstrap/src/test/resources/app-service-test.properties b/jcpp-app-bootstrap/src/test/resources/app-service-test.properties new file mode 100644 index 0000000..9e61ffc --- /dev/null +++ b/jcpp-app-bootstrap/src/test/resources/app-service-test.properties @@ -0,0 +1,2 @@ +redis.connection.type=cluster +redis.cluster.nodes=10.102.12.101:30700,10.102.12.101:32027,10.102.12.101:30767,10.102.12.101:30250,10.102.12.101:30612,10.102.12.101:32303 diff --git a/jcpp-app/pom.xml b/jcpp-app/pom.xml new file mode 100644 index 0000000..03b28dc --- /dev/null +++ b/jcpp-app/pom.xml @@ -0,0 +1,56 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-app + jar + JChargePointProtocol App Module + 应用模块 + + + ${basedir}/.. + + + + + sanbing + jcpp-protocol-api + + + sanbing + jcpp-infrastructure-queue + + + sanbing + jcpp-infrastructure-cache + + + org.postgresql + postgresql + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + + diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/DalConfig.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/DalConfig.java new file mode 100644 index 0000000..56de407 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/DalConfig.java @@ -0,0 +1,76 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.zaxxer.hikari.HikariDataSource; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; + +@Configuration +@EnableAutoConfiguration(exclude = {RedisAutoConfiguration.class}) +@MapperScan({"sanbing.jcpp.app.dal.mapper"}) +public class DalConfig { + + @Bean + @ConfigurationProperties("spring.datasource") + public DataSourceProperties dataSourceProperties() { + return new DataSourceProperties(); + } + + @Primary + @ConfigurationProperties(prefix = "spring.datasource.hikari") + @Bean + public DataSource dataSource(@Qualifier("dataSourceProperties") DataSourceProperties dataSourceProperties) { + return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); + } + + @Primary + @Bean + public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Primary + @Bean + public NamedParameterJdbcTemplate namedParameterJdbcTemplate(@Qualifier("dataSource") DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); + } + + @Primary + @Bean + public TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) { + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + transactionTemplate.setIsolationLevel(TransactionTemplate.ISOLATION_READ_COMMITTED); + transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED); + return transactionTemplate; + } + + @Bean + @Primary + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); + PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); + paginationInnerInterceptor.setDbType(DbType.POSTGRE_SQL); + mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor); + return mybatisPlusInterceptor; + } + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/GunOptStatusEnum.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/GunOptStatusEnum.java new file mode 100644 index 0000000..f3caa6a --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/GunOptStatusEnum.java @@ -0,0 +1,22 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +/** + * @author baigod + */ +public enum GunOptStatusEnum implements IEnum { + AVAILABLE, // 可用状态 + IN_MAINTENANCE, // 维护中状态 + OUT_OF_SERVICE, // 停用状态 + RESERVED; // 已预约状态 + + @Override + public String getValue() { + return name(); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/GunRunStatusEnum.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/GunRunStatusEnum.java new file mode 100644 index 0000000..d2d0e92 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/GunRunStatusEnum.java @@ -0,0 +1,27 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +/** + * @author baigod + */ +public enum GunRunStatusEnum implements IEnum { + IDLE, // 空闲 + INSERTED, // 已插枪 + CHARGING, // 充电中 + CHARGE_COMPLETE, // 充电完成 + DISCHARGE_READY, // 放电准备 + DISCHARGING, // 放电中 + DISCHARGE_COMPLETE, // 放电完成 + RESERVED, // 预约 + FAULT; // 故障 + + @Override + public String getValue() { + return name(); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OrderStatusEnum.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OrderStatusEnum.java new file mode 100644 index 0000000..11aa6d6 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OrderStatusEnum.java @@ -0,0 +1,24 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +public enum OrderStatusEnum implements IEnum { + PENDING, + IN_CHARGING, + COMPLETED, + CANCELLED, + TERMINATED, + FAILED, + REFUNDED; + + + @Override + public String getValue() { + return name(); + } + +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OrderTypeEnum.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OrderTypeEnum.java new file mode 100644 index 0000000..b72a85d --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OrderTypeEnum.java @@ -0,0 +1,21 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +/** + * @author baigod + */ +public enum OrderTypeEnum implements IEnum { + CHARGE, + + DISCHARGE; + + @Override + public String getValue() { + return name(); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OwnerTypeEnum.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OwnerTypeEnum.java new file mode 100644 index 0000000..7c142b9 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/OwnerTypeEnum.java @@ -0,0 +1,21 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +/** + * @author baigod + */ +public enum OwnerTypeEnum implements IEnum { + C, + B, + G; + + @Override + public String getValue() { + return name(); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/PileStatusEnum.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/PileStatusEnum.java new file mode 100644 index 0000000..f8e20a6 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/PileStatusEnum.java @@ -0,0 +1,24 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +/** + * @author baigod + */ +public enum PileStatusEnum implements IEnum { + IDLE, // 空闲 + WORKING, // 工作中 + FAULT, // 故障 + MAINTENANCE, // 维护中 + OFFLINE, // 离线 + ; + + @Override + public String getValue() { + return name(); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/PileTypeEnum.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/PileTypeEnum.java new file mode 100644 index 0000000..11a1cef --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/PileTypeEnum.java @@ -0,0 +1,21 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +/** + * @author baigod + */ +public enum PileTypeEnum implements IEnum { + AC, // 交流充电桩 + DC, // 直流充电桩 + ; + + @Override + public String getValue() { + return name(); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/StationStatusEnum.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/StationStatusEnum.java new file mode 100644 index 0000000..665e522 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/StationStatusEnum.java @@ -0,0 +1,26 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +/** + * @author baigod + */ +public enum StationStatusEnum implements IEnum { + OPERATIONAL, // 正常运营 + PARTIAL_FAILURE, // 部分故障 + FULLY_LOADED, // 满载 + MAINTENANCE, // 维护中 + CLOSED, // 关闭 + WAITING_FOR_OPEN; // 待开放 + + @Override + public String getValue() { + return name(); + } + + +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/UserStatusEnum.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/UserStatusEnum.java new file mode 100644 index 0000000..6b1ac06 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/enums/UserStatusEnum.java @@ -0,0 +1,20 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +/** + * @author baigod + */ +public enum UserStatusEnum implements IEnum { + ENABLE, + DISABLE; + + @Override + public String getValue() { + return name(); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/typehandlers/JsonbTypeHandler.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/typehandlers/JsonbTypeHandler.java new file mode 100644 index 0000000..85faad5 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/typehandlers/JsonbTypeHandler.java @@ -0,0 +1,40 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.typehandlers; + +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedTypes; +import org.postgresql.util.PGobject; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; + +import java.lang.reflect.Field; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@Slf4j +@MappedTypes({JsonNode.class}) +public class JsonbTypeHandler extends JacksonTypeHandler { + + public JsonbTypeHandler(Class type) { + super(type); + } + + public JsonbTypeHandler(Class type, Field field) { + super(type, field); + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { + if (ps != null) { + PGobject jsonObject = new PGobject(); + jsonObject.setType("jsonb"); + jsonObject.setValue(JacksonUtil.toString(parameter)); + ps.setObject(i, jsonObject); + } + } +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/typehandlers/UUIDTypeHandler.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/typehandlers/UUIDTypeHandler.java new file mode 100644 index 0000000..df0a08a --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/config/ibatis/typehandlers/UUIDTypeHandler.java @@ -0,0 +1,40 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.config.ibatis.typehandlers; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.UUID; + +/** + * mysql UUID 类型转 varchar + */ +public class UUIDTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, UUID parameter, JdbcType jdbcType) throws SQLException { + ps.setObject(i, parameter); + } + + @Override + public UUID getNullableResult(ResultSet rs, String columnName) throws SQLException { + return rs.getObject(columnName, UUID.class); + } + + @Override + public UUID getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return rs.getObject(columnIndex, UUID.class); + } + + @Override + public UUID getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return cs.getObject(columnIndex, UUID.class); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Gun.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Gun.java new file mode 100644 index 0000000..d8c15b8 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Gun.java @@ -0,0 +1,61 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import sanbing.jcpp.app.dal.config.ibatis.enums.GunOptStatusEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.GunRunStatusEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum; +import sanbing.jcpp.infrastructure.cache.HasVersion; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + + +@Data +@TableName("jcpp_gun") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Gun implements Serializable, HasVersion { + + @TableId(type = IdType.INPUT) + private UUID id; + + private LocalDateTime createdTime; + + private JsonNode additionalInfo; + + private String gunNo; + + private String gunName; + + private String gunCode; + + private UUID stationId; + + private UUID pileId; + + private UUID ownerId; + + private OwnerTypeEnum ownerType; + + private GunRunStatusEnum runStatus; + + private LocalDateTime runStatusUpdatedTime; + + private GunOptStatusEnum optStatus; + + private Integer version; + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Order.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Order.java new file mode 100644 index 0000000..e86b1b1 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Order.java @@ -0,0 +1,69 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import sanbing.jcpp.app.dal.config.ibatis.enums.OrderStatusEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.OrderTypeEnum; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + + +@Data +@TableName("jcpp_order") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Order implements Serializable { + + @TableId(type = IdType.INPUT) + private UUID id; + + private String internalOrderNo; + + private String externalOrderNo; + + private String pileOrderNo; + + private LocalDateTime createdTime; + + private JsonNode additionalInfo; + + private LocalDateTime updatedTime; + + private LocalDateTime cancelledTime; + + private OrderStatusEnum status; + + private OrderTypeEnum type; + + private UUID creatorId; + + private UUID stationId; + + private UUID pileId; + + private UUID gunId; + + private String plateNo; + + private Long settlementAmount; + + private JsonNode settlementDetails; + + private BigDecimal electricityQuantity; + + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Pile.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Pile.java new file mode 100644 index 0000000..e70a7ec --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Pile.java @@ -0,0 +1,61 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.PileStatusEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.PileTypeEnum; +import sanbing.jcpp.infrastructure.cache.HasVersion; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@TableName(value = "jcpp_pile", autoResultMap = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Pile implements Serializable, HasVersion { + + @TableId(type = IdType.INPUT) + private UUID id; + + private LocalDateTime createdTime; + + private JsonNode additionalInfo; + + private String pileName; + + private String pileCode; + + private String protocol; + + private UUID stationId; + + private UUID ownerId; + + private OwnerTypeEnum ownerType; + + private String brand; + + private String model; + + private String manufacturer; + + private PileStatusEnum status; + + private PileTypeEnum type; + + private Integer version; +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Station.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Station.java new file mode 100644 index 0000000..e8b9c42 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Station.java @@ -0,0 +1,62 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import sanbing.jcpp.app.dal.config.ibatis.enums.OwnerTypeEnum; +import sanbing.jcpp.app.dal.config.ibatis.enums.StationStatusEnum; +import sanbing.jcpp.infrastructure.cache.HasVersion; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + + +@Data +@TableName("jcpp_station") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Station implements Serializable, HasVersion { + + @TableId(type = IdType.INPUT) + private UUID id; + + private LocalDateTime createdTime; + + private JsonNode additionalInfo; + + private String stationName; + + private String stationCode; + + private UUID ownerId; + + private Float longitude; + + private Float latitude; + + private OwnerTypeEnum ownerType; + + private String province; + + private String city; + + private String county; + + private String address; + + private StationStatusEnum status; + + private Integer version; + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/User.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/User.java new file mode 100644 index 0000000..7350bde --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/User.java @@ -0,0 +1,45 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import sanbing.jcpp.app.dal.config.ibatis.enums.UserStatusEnum; +import sanbing.jcpp.infrastructure.cache.HasVersion; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + + +@Data +@TableName("jcpp_user") +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class User implements Serializable, HasVersion { + + @TableId(type = IdType.INPUT) + private UUID id; + + private LocalDateTime createdTime; + + private JsonNode additionalInfo; + + private UserStatusEnum status; + + private String userName; + + private JsonNode userCredentials; + + private Integer version; + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/GunMapper.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/GunMapper.java new file mode 100644 index 0000000..b44c31f --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/GunMapper.java @@ -0,0 +1,14 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import sanbing.jcpp.app.dal.entity.Gun; + +/** + * @author baigod + */ +public interface GunMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/OrderMapper.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/OrderMapper.java new file mode 100644 index 0000000..ce68e25 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/OrderMapper.java @@ -0,0 +1,14 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import sanbing.jcpp.app.dal.entity.Order; + +/** + * @author baigod + */ +public interface OrderMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/PileMapper.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/PileMapper.java new file mode 100644 index 0000000..78e65ce --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/PileMapper.java @@ -0,0 +1,23 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; +import sanbing.jcpp.app.dal.entity.Pile; + +/** + * @author baigod + */ +public interface PileMapper extends BaseMapper { + + @Select("SELECT " + + " * " + + "FROM " + + " jcpp_pile " + + "WHERE " + + " pile_code = #{pileCode}") + Pile selectByCode(String pileCode); +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/StationMapper.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/StationMapper.java new file mode 100644 index 0000000..136010b --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/StationMapper.java @@ -0,0 +1,14 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import sanbing.jcpp.app.dal.entity.Station; + +/** + * @author baigod + */ +public interface StationMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/UserMapper.java b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/UserMapper.java new file mode 100644 index 0000000..e49d639 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/dal/mapper/UserMapper.java @@ -0,0 +1,14 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.dal.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import sanbing.jcpp.app.dal.entity.User; + +/** + * @author baigod + */ +public interface UserMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/data/PileSession.java b/jcpp-app/src/main/java/sanbing/jcpp/app/data/PileSession.java new file mode 100644 index 0000000..1dbb5cc --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/data/PileSession.java @@ -0,0 +1,57 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.data; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.UUID; + +/** + * @author baigod + */ +@Data +public class PileSession implements Serializable { + + private final UUID pileId; + + private final String pileCode; + + private final String protocolName; + + private UUID protocolSessionId; + + private String remoteAddress; + + private String nodeId; + + private String nodeWebapiIpPort; + + public PileSession(UUID pileId, String pileCode, String protocolName) { + this.pileId = pileId; + this.pileCode = pileCode; + this.protocolName = protocolName; + } + + @JsonCreator + public PileSession( + @JsonProperty("pileId") UUID pileId, + @JsonProperty("pileCode") String pileCode, + @JsonProperty("protocolName") String protocolName, + @JsonProperty("protocolSessionId") UUID protocolSessionId, + @JsonProperty("remoteAddress") String remoteAddress, + @JsonProperty("nodeId") String nodeId, + @JsonProperty("nodeWebapiIpPort") String nodeWebapiIpPort) { + this.pileId = pileId; + this.pileCode = pileCode; + this.protocolName = protocolName; + this.protocolSessionId = protocolSessionId; + this.remoteAddress = remoteAddress; + this.nodeId = nodeId; + this.nodeWebapiIpPort = nodeWebapiIpPort; + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/repository/AbstractCachedEntityRepository.java b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/AbstractCachedEntityRepository.java new file mode 100644 index 0000000..d192f65 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/AbstractCachedEntityRepository.java @@ -0,0 +1,23 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.repository; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.io.Serializable; + +public abstract class AbstractCachedEntityRepository extends AbstractEntityRepository { + + protected void publishEvictEvent(E event) { + if (TransactionSynchronizationManager.isActualTransactionActive()) { + eventPublisher.publishEvent(event); + } else { + handleEvictEvent(event); + } + } + + public abstract void handleEvictEvent(E event); + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/repository/AbstractEntityRepository.java b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/AbstractEntityRepository.java new file mode 100644 index 0000000..4bdf6bb --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/AbstractEntityRepository.java @@ -0,0 +1,17 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.repository; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; + +@Slf4j +public abstract class AbstractEntityRepository { + + @Resource + protected ApplicationEventPublisher eventPublisher; + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/repository/CachedVersionedEntityRepository.java b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/CachedVersionedEntityRepository.java new file mode 100644 index 0000000..157b928 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/CachedVersionedEntityRepository.java @@ -0,0 +1,19 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.repository; + +import jakarta.annotation.Resource; +import sanbing.jcpp.infrastructure.cache.HasVersion; +import sanbing.jcpp.infrastructure.cache.VersionedCache; +import sanbing.jcpp.infrastructure.cache.VersionedCacheKey; + +import java.io.Serializable; + +public abstract class CachedVersionedEntityRepository extends AbstractCachedEntityRepository { + + @Resource + protected VersionedCache cache; + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/repository/PileRepository.java b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/PileRepository.java new file mode 100644 index 0000000..183bb0a --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/PileRepository.java @@ -0,0 +1,15 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.repository; + +import sanbing.jcpp.app.dal.entity.Pile; + +/** + * @author baigod + */ +public interface PileRepository { + + Pile findPileByCode(String pileCode); +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/repository/PileRepositoryImpl.java b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/PileRepositoryImpl.java new file mode 100644 index 0000000..309d383 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/repository/PileRepositoryImpl.java @@ -0,0 +1,47 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.repository; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.event.TransactionalEventListener; +import sanbing.jcpp.app.dal.entity.Pile; +import sanbing.jcpp.app.dal.mapper.PileMapper; +import sanbing.jcpp.app.service.cache.pile.PileCacheEvictEvent; +import sanbing.jcpp.app.service.cache.pile.PileCacheKey; + +import java.util.ArrayList; +import java.util.List; + +import static sanbing.jcpp.infrastructure.util.validation.Validator.validateString; + +/** + * @author baigod + */ +@Repository +@Slf4j +public class PileRepositoryImpl extends CachedVersionedEntityRepository implements PileRepository { + + @Resource + PileMapper pileMapper; + + @TransactionalEventListener(classes = PileCacheEvictEvent.class) + @Override + public void handleEvictEvent(PileCacheEvictEvent event) { + // 如果修改或删除充电桩,需要在这里消费删除事件 + List toEvict = new ArrayList<>(3); + toEvict.add(new PileCacheKey(event.getPileId())); + toEvict.add(new PileCacheKey(event.getPileCode())); + cache.evict(toEvict); + } + + @Override + public Pile findPileByCode(String pileCode) { + validateString(pileCode, code -> "无效的桩编号" + pileCode); + return cache.get(new PileCacheKey(pileCode), + () -> pileMapper.selectByCode(pileCode)); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/DownlinkCallService.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/DownlinkCallService.java new file mode 100644 index 0000000..3dc6c78 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/DownlinkCallService.java @@ -0,0 +1,15 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service; + +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; + +/** + * @author baigod + */ +public interface DownlinkCallService { + + void sendDownlinkMessage(DownlinkRestMessage.Builder downlinkMessageBuilder, String pileCode); +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/PileProtocolService.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/PileProtocolService.java new file mode 100644 index 0000000..76c34ed --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/PileProtocolService.java @@ -0,0 +1,66 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service; + +import sanbing.jcpp.infrastructure.queue.Callback; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; + +/** + * @author baigod + */ +public interface PileProtocolService { + /** + * 桩登录 + */ + void pileLogin(UplinkQueueMessage uplinkQueueMessage, Callback callback); + + /** + * 充电桩心跳 + */ + void heartBeat(UplinkQueueMessage uplinkQueueMessage, Callback callback); + + /** + * 校验计费模型 + */ + void verifyPricing(UplinkQueueMessage uplinkQueueMessage, Callback callback); + + /** + * 查询计费策略 + */ + void queryPricing(UplinkQueueMessage uplinkQueueMessage, Callback callback); + + /** + * 上报电桩运行状态 + */ + void postGunRunStatus(UplinkQueueMessage uplinkQueueMessage, Callback callback); + + /** + * 上报充电进度 + */ + void postChargingProgress(UplinkQueueMessage uplinkQueueMessage, Callback callback); + + /** + * 费率下发反馈 + */ + void onSetPricingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback); + + /** + * 远程启动反馈 + * + * @param uplinkQueueMessage + * @param callback + */ + void onRemoteStartChargingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback); + + /** + * 远程停止反馈 + */ + void onRemoteStopChargingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback); + + /** + * 交易记录上报 + */ + void onTransactionRecord(UplinkQueueMessage uplinkQueueMessage, Callback callback); +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheEvictEvent.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheEvictEvent.java new file mode 100644 index 0000000..4948e03 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheEvictEvent.java @@ -0,0 +1,17 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.cache.pile; + +import lombok.Data; + +import java.util.UUID; + +@Data +public class PileCacheEvictEvent { + + private UUID pileId; + private String pileCode; + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheKey.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheKey.java new file mode 100644 index 0000000..94f7d56 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheKey.java @@ -0,0 +1,47 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.cache.pile; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import sanbing.jcpp.infrastructure.cache.VersionedCacheKey; + +import java.io.Serial; +import java.util.Optional; +import java.util.UUID; + +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +@Builder +public class PileCacheKey implements VersionedCacheKey { + + @Serial + private static final long serialVersionUID = 6366389552842340207L; + + private final UUID pileId; + private final String pileCode; + + public PileCacheKey(UUID pileId) { + this(pileId, null); + } + + public PileCacheKey(String pileCode) { + this(null, pileCode); + } + + @Override + public String toString() { + return Optional.ofNullable(pileId).map(UUID::toString).orElse(pileCode); + } + + @Override + public boolean isVersioned() { + return pileId != null; + } + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCaffeineCache.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCaffeineCache.java new file mode 100644 index 0000000..baa8690 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCaffeineCache.java @@ -0,0 +1,22 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.cache.pile; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import sanbing.jcpp.app.dal.entity.Pile; +import sanbing.jcpp.infrastructure.cache.CacheConstants; +import sanbing.jcpp.infrastructure.cache.VersionedCaffeineCache; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("PileCache") +public class PileCaffeineCache extends VersionedCaffeineCache { + + public PileCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.PILE_CACHE); + } + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileRedisCache.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileRedisCache.java new file mode 100644 index 0000000..9209bb7 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileRedisCache.java @@ -0,0 +1,33 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.cache.pile; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import sanbing.jcpp.app.dal.entity.Pile; +import sanbing.jcpp.infrastructure.cache.*; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("PileCache") +public class PileRedisCache extends VersionedRedisCache { + + public PileRedisCache(JCPPRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, LettuceConnectionFactory connectionFactory) { + super(CacheConstants.PILE_CACHE, cacheSpecsMap, connectionFactory, configuration, new JCPPRedisSerializer<>() { + + @Override + public byte[] serialize(Pile pile) throws SerializationException { + return JacksonUtil.writeValueAsBytes(pile); + } + + @Override + public Pile deserialize(PileCacheKey key, byte[] bytes) throws SerializationException { + return JacksonUtil.fromBytes(bytes, Pile.class); + } + }); + } +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionCacheKey.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionCacheKey.java new file mode 100644 index 0000000..e18f0e4 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionCacheKey.java @@ -0,0 +1,41 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.cache.session; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.io.Serializable; +import java.util.Optional; +import java.util.UUID; + +/** + * @author baigod + */ +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +@Builder +public class PileSessionCacheKey implements Serializable { + + private final UUID pileId; + private final String pileCode; + + public PileSessionCacheKey(UUID pileId) { + this(pileId, null); + } + + public PileSessionCacheKey(String pileCode) { + this(null, pileCode); + } + + @Override + public String toString() { + return Optional.ofNullable(pileId).map(UUID::toString).orElse(pileCode); + } + +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionCaffeineCache.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionCaffeineCache.java new file mode 100644 index 0000000..fec98a3 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionCaffeineCache.java @@ -0,0 +1,24 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.cache.session; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import sanbing.jcpp.app.data.PileSession; +import sanbing.jcpp.infrastructure.cache.CacheConstants; +import sanbing.jcpp.infrastructure.cache.CaffeineTransactionalCache; + +/** + * @author baigod + */ +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("PileSessionCache") +public class PileSessionCaffeineCache extends CaffeineTransactionalCache { + + public PileSessionCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.PILE_SESSION_CACHE); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionRedisCache.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionRedisCache.java new file mode 100644 index 0000000..77c193d --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/session/PileSessionRedisCache.java @@ -0,0 +1,36 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.cache.session; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import sanbing.jcpp.app.data.PileSession; +import sanbing.jcpp.infrastructure.cache.*; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; + +/** + * @author baigod + */ +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("PileSessionCache") +public class PileSessionRedisCache extends RedisTransactionalCache { + + public PileSessionRedisCache(JCPPRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, LettuceConnectionFactory connectionFactory) { + super(CacheConstants.PILE_SESSION_CACHE, cacheSpecsMap, connectionFactory, configuration, new JCPPRedisSerializer<>() { + + @Override + public byte[] serialize(PileSession pileSession) throws SerializationException { + return JacksonUtil.writeValueAsBytes(pileSession); + } + + @Override + public PileSession deserialize(PileSessionCacheKey key, byte[] bytes) throws SerializationException { + return JacksonUtil.fromBytes(bytes, PileSession.class); + } + }); + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/config/DownlinkRestTemplateConfiguration.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/config/DownlinkRestTemplateConfiguration.java new file mode 100644 index 0000000..af36e86 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/config/DownlinkRestTemplateConfiguration.java @@ -0,0 +1,32 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; + +/** + * @author baigod + */ +@Configuration +public class DownlinkRestTemplateConfiguration { + + @Bean("downlinkRestTemplate") + public RestTemplate downlinkRestTemplate() { + RestTemplate restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.of(3, ChronoUnit.SECONDS)) + .setReadTimeout(Duration.of(3, ChronoUnit.SECONDS)) + .build(); + restTemplate.setMessageConverters(Collections.singletonList(new ProtobufHttpMessageConverter())); + return restTemplate; + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultDownlinkCallService.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultDownlinkCallService.java new file mode 100644 index 0000000..90c1f76 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultDownlinkCallService.java @@ -0,0 +1,93 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.impl; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import sanbing.jcpp.app.data.PileSession; +import sanbing.jcpp.app.service.DownlinkCallService; +import sanbing.jcpp.app.service.cache.session.PileSessionCacheKey; +import sanbing.jcpp.infrastructure.cache.CacheValueWrapper; +import sanbing.jcpp.infrastructure.cache.TransactionalCache; +import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; +import sanbing.jcpp.protocol.adapter.DownlinkController; + +import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.*; + +/** + * @author baigod + */ +@Service +@Slf4j +public class DefaultDownlinkCallService implements DownlinkCallService { + + @Resource + RestTemplate downlinkRestTemplate; + + @Resource + ServiceInfoProvider serviceInfoProvider; + + @Resource + DownlinkController downlinkController; + + @Resource + TransactionalCache pileSessionCache; + + @Override + public void sendDownlinkMessage(DownlinkRestMessage.Builder downlinkMessageBuilder, String pileCode) { + if (serviceInfoProvider.isMonolith()) { + + downlinkController.onDownlink(downlinkMessageBuilder.build()) + .setResultHandler(result -> log.info("下行消息发送完成")); + + } else { + try { + CacheValueWrapper pileSessionCacheValueWrapper = pileSessionCache.get(new PileSessionCacheKey(pileCode)); + + if (pileSessionCacheValueWrapper == null) { + log.warn("充电桩会话不存在 {}", pileCode); + return; + } + + PileSession pileSession = pileSessionCacheValueWrapper.get(); + + invokeDownlinkRestApi(downlinkMessageBuilder.build(), pileSession.getNodeWebapiIpPort()); + + + } catch (RestClientException e) { + log.error("下行消息发送异常", e); + } + } + } + + private void invokeDownlinkRestApi(DownlinkRestMessage downlinkRestMessage, String nodeWebapiIpPort) { + HttpHeaders headers = new HttpHeaders(); + headers.add(JCPP_TRACER_ID, TracerContextUtil.getCurrentTracer().getTraceId()); + headers.add(JCPP_TRACER_ORIGIN, TracerContextUtil.getCurrentTracer().getOrigin()); + headers.add(JCPP_TRACER_TS, String.valueOf(TracerContextUtil.getCurrentTracer().getTracerTs())); + headers.setContentType(MediaType.parseMediaType("application/x-protobuf")); + + HttpEntity entity = new HttpEntity<>(downlinkRestMessage, headers); + + try { + ResponseEntity response = downlinkRestTemplate.postForEntity("http://" + nodeWebapiIpPort + "/api/onDownlink", + entity, ResponseEntity.class); + log.info("下行消息发送成功 {}", response); + } catch (RestClientException e) { + log.error("下行消息发送失败 {}", downlinkRestMessage, e); + throw new RuntimeException(e); + } + + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultPileProtocolService.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultPileProtocolService.java new file mode 100644 index 0000000..6699c10 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/impl/DefaultPileProtocolService.java @@ -0,0 +1,264 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.impl; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import sanbing.jcpp.app.dal.entity.Pile; +import sanbing.jcpp.app.data.PileSession; +import sanbing.jcpp.app.repository.PileRepository; +import sanbing.jcpp.app.service.DownlinkCallService; +import sanbing.jcpp.app.service.PileProtocolService; +import sanbing.jcpp.app.service.cache.session.PileSessionCacheKey; +import sanbing.jcpp.infrastructure.cache.TransactionalCache; +import sanbing.jcpp.infrastructure.proto.ProtoConverter; +import sanbing.jcpp.infrastructure.proto.model.PricingModel; +import sanbing.jcpp.infrastructure.proto.model.PricingModel.FlagPrice; +import sanbing.jcpp.infrastructure.proto.model.PricingModel.Period; +import sanbing.jcpp.infrastructure.queue.Callback; +import sanbing.jcpp.proto.gen.ProtocolProto.*; +import sanbing.jcpp.protocol.domain.DownlinkCmdEnum; + +import java.time.LocalTime; +import java.util.*; + +import static sanbing.jcpp.proto.gen.ProtocolProto.PricingModelFlag.*; +import static sanbing.jcpp.proto.gen.ProtocolProto.PricingModelRule.SPLIT_TIME; +import static sanbing.jcpp.proto.gen.ProtocolProto.PricingModelType.CHARGE; + +/** + * @author baigod + */ +@Service +@Slf4j +public class DefaultPileProtocolService implements PileProtocolService { + + @Resource + PileRepository pileRepository; + + @Resource + TransactionalCache pileSessionCache; + + @Resource + DownlinkCallService downlinkCallService; + + @Override + public void pileLogin(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到桩登录事件 {}", uplinkQueueMessage); + + LoginRequest loginRequest = uplinkQueueMessage.getLoginRequest(); + + Pile pile = pileRepository.findPileByCode(loginRequest.getPileCode()); + + String pileCode = loginRequest.getPileCode(); + + log.info("查询到充电桩信息 {}", pile); + + // 构造下行回复 + DownlinkRestMessage.Builder downlinkMessageBuilder = createDownlinkMessageBuilder(uplinkQueueMessage, loginRequest.getPileCode()); + downlinkMessageBuilder.setDownlinkCmd(DownlinkCmdEnum.LOGIN_ACK.name()); + + if (pile != null) { + // 保存到缓存 + cacheSession(uplinkQueueMessage, pile, + loginRequest.getRemoteAddress(), + loginRequest.getNodeId(), + loginRequest.getNodeWebapiIpPort()); + + downlinkMessageBuilder.setLoginResponse(LoginResponse.newBuilder() + .setSuccess(true) + .setPileCode(loginRequest.getPileCode()) + .build()); + } else { + downlinkMessageBuilder.setLoginResponse(LoginResponse.newBuilder() + .setSuccess(false) + .setPileCode(loginRequest.getPileCode()) + .build()); + } + + downlinkCallService.sendDownlinkMessage(downlinkMessageBuilder, pileCode); + + callback.onSuccess(); + } + + @Override + public void heartBeat(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到桩心跳事件 {}", uplinkQueueMessage); + + HeartBeatRequest heartBeatRequest = uplinkQueueMessage.getHeartBeatRequest(); + + Pile pile = pileRepository.findPileByCode(heartBeatRequest.getPileCode()); + + if (pile != null) { + // 重新保存到缓存 + cacheSession(uplinkQueueMessage, pile, + heartBeatRequest.getRemoteAddress(), + heartBeatRequest.getNodeId(), + heartBeatRequest.getNodeWebapiIpPort()); + } + } + + private void cacheSession(UplinkQueueMessage uplinkQueueMessage, Pile pile, String remoteAddress, String nodeId, String nodeWebapiIpPort) { + PileSession pileSession = new PileSession(pile.getId(), pile.getPileCode(), uplinkQueueMessage.getProtocolName()); + pileSession.setProtocolSessionId(new UUID(uplinkQueueMessage.getSessionIdMSB(), uplinkQueueMessage.getSessionIdLSB())); + pileSession.setRemoteAddress(remoteAddress); + pileSession.setNodeId(nodeId); + pileSession.setNodeWebapiIpPort(nodeWebapiIpPort); + pileSessionCache.put(new PileSessionCacheKey(pile.getId()), pileSession); + pileSessionCache.put(new PileSessionCacheKey(pile.getPileCode()), pileSession); + } + + @Override + public void verifyPricing(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到计费模型验证请求 {}", uplinkQueueMessage); + + VerifyPricingRequest verifyPricingRequest = uplinkQueueMessage.getVerifyPricingRequest(); + String pileCode = verifyPricingRequest.getPileCode(); + + long pricingId = verifyPricingRequest.getPricingId(); + // todo 默认校验成功,后续查库校验 + assert pricingId > 0; + + DownlinkRestMessage.Builder downlinkMessageBuilder = createDownlinkMessageBuilder(uplinkQueueMessage, pileCode); + downlinkMessageBuilder.setDownlinkCmd(DownlinkCmdEnum.VERIFY_PRICING_ACK.name()); + downlinkMessageBuilder.setVerifyPricingResponse(VerifyPricingResponse.newBuilder() + .setSuccess(true) + .setPricingId(pricingId) + .build()); + + downlinkCallService.sendDownlinkMessage(downlinkMessageBuilder, pileCode); + + callback.onSuccess(); + } + + @Override + public void queryPricing(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到充电桩计费模型请求 {}", uplinkQueueMessage); + + QueryPricingRequest queryPricingRequest = uplinkQueueMessage.getQueryPricingRequest(); + String pileCode = queryPricingRequest.getPileCode(); + + // TODO 先构造一个通用的计费模型,后续根据业务做库查询 + List periods = new ArrayList<>(); + + periods.add(createPeriod(1, LocalTime.parse("00:00"), LocalTime.parse("06:00"), TOP)); + periods.add(createPeriod(2, LocalTime.parse("06:00"), LocalTime.parse("12:00"), PEAK)); + periods.add(createPeriod(3, LocalTime.parse("12:00"), LocalTime.parse("18:00"), FLAT)); + periods.add(createPeriod(4, LocalTime.parse("18:00"), LocalTime.parse("00:00"), VALLEY)); + + Map flagPriceMap = new HashMap<>(); + flagPriceMap.put(TOP, new FlagPrice(75, 45)); + flagPriceMap.put(PEAK, new FlagPrice(75, 45)); + flagPriceMap.put(FLAT, new FlagPrice(75, 45)); + flagPriceMap.put(VALLEY, new FlagPrice(75, 45)); + + PricingModel model = new PricingModel(); + model.setId(UUID.randomUUID()); + model.setSequenceNumber(1); + model.setPileCode(pileCode); + model.setType(CHARGE); + model.setRule(SPLIT_TIME); + model.setStandardElec(75); + model.setStandardServ(45); + model.setFlagPriceList(flagPriceMap); + model.setPeriodsList(periods); + + // 构造下行计费 + DownlinkRestMessage.Builder downlinkMessageBuilder = createDownlinkMessageBuilder(uplinkQueueMessage, pileCode); + downlinkMessageBuilder.setDownlinkCmd(DownlinkCmdEnum.QUERY_PRICING_ACK.name()); + downlinkMessageBuilder.setQueryPricingResponse(QueryPricingResponse.newBuilder() + .setPileCode(pileCode) + .setPricingId(model.getSequenceNumber()) + .setPricingModel(ProtoConverter.toPricingModel(model)) + .build()); + + downlinkCallService.sendDownlinkMessage(downlinkMessageBuilder, pileCode); + + callback.onSuccess(); + } + + @Override + public void postGunRunStatus(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到充电桩上报的电桩状态 {}", uplinkQueueMessage); + + callback.onSuccess(); + } + + @Override + public void postChargingProgress(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到充电桩上报的充电进度 {}", uplinkQueueMessage); + + callback.onSuccess(); + } + + @Override + public void onSetPricingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到充电桩上费率下发反馈 {}", uplinkQueueMessage); + + callback.onSuccess(); + } + + @Override + public void onRemoteStartChargingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到充电桩启动结果反馈 {}", uplinkQueueMessage); + + callback.onSuccess(); + } + + @Override + public void onRemoteStopChargingResponse(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到充电桩停止结果反馈 {}", uplinkQueueMessage); + + callback.onSuccess(); + } + + @Override + public void onTransactionRecord(UplinkQueueMessage uplinkQueueMessage, Callback callback) { + log.info("接收到充电桩交易记录上报 {}", uplinkQueueMessage); + + // todo 毛都不敢先给个回复 + TransactionRecord transactionRecord = uplinkQueueMessage.getTransactionRecord(); + + String tradeNo = transactionRecord.getTradeNo(); + String pileCode = transactionRecord.getPileCode(); + + // 构造下行计费 + DownlinkRestMessage.Builder downlinkMessageBuilder = createDownlinkMessageBuilder(uplinkQueueMessage, pileCode); + downlinkMessageBuilder.setDownlinkCmd(DownlinkCmdEnum.TRANSACTION_RECORD.name()); + downlinkMessageBuilder.setTransactionRecordAck(TransactionRecordAck.newBuilder() + .setTradeNo(tradeNo) + .setSuccess(true) + .build()); + + downlinkCallService.sendDownlinkMessage(downlinkMessageBuilder, pileCode); + + callback.onSuccess(); + } + + private static Period createPeriod(int sn, LocalTime beginTime, LocalTime endTime, PricingModelFlag flag) { + Period period = new Period(); + period.setSn(sn); + period.setBegin(beginTime); + period.setEnd(endTime); + period.setFlag(flag); + return period; + } + + private DownlinkRestMessage.Builder createDownlinkMessageBuilder(UplinkQueueMessage uplinkQueueMessage, String pileCode) { + UUID messageId = UUID.randomUUID(); + DownlinkRestMessage.Builder builder = DownlinkRestMessage.newBuilder(); + builder.setMessageIdMSB(messageId.getLeastSignificantBits()); + builder.setMessageIdLSB(messageId.getLeastSignificantBits()); + builder.setPileCode(pileCode); + builder.setSessionIdMSB(uplinkQueueMessage.getSessionIdMSB()); + builder.setSessionIdLSB(uplinkQueueMessage.getSessionIdLSB()); + builder.setProtocolName(uplinkQueueMessage.getProtocolName()); + builder.setRequestIdMSB(uplinkQueueMessage.getMessageIdMSB()); + builder.setRequestIdLSB(uplinkQueueMessage.getMessageIdLSB()); + builder.setRequestData(uplinkQueueMessage.getRequestData()); + return builder; + } +} \ No newline at end of file diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AbstractConsumerService.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AbstractConsumerService.java new file mode 100644 index 0000000..cff3ebd --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AbstractConsumerService.java @@ -0,0 +1,66 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.queue; + +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider; +import sanbing.jcpp.infrastructure.queue.discovery.event.JCPPApplicationEventListener; +import sanbing.jcpp.infrastructure.queue.discovery.event.PartitionChangeEvent; +import sanbing.jcpp.infrastructure.util.annotation.AfterStartUp; +import sanbing.jcpp.infrastructure.util.async.JCPPExecutors; +import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractConsumerService extends JCPPApplicationEventListener { + + protected final PartitionProvider partitionProvider; + protected final ApplicationEventPublisher eventPublisher; + + protected ExecutorService consumersExecutor; + protected ExecutorService mgmtExecutor; + protected ScheduledExecutorService scheduler; + + public void init(String prefix) { + this.consumersExecutor = Executors.newCachedThreadPool(JCPPThreadFactory.forName(prefix + "-consumer")); + this.mgmtExecutor = JCPPExecutors.newWorkStealingPool(getMgmtThreadPoolSize(), prefix + "-mgmt"); + this.scheduler = Executors.newSingleThreadScheduledExecutor(JCPPThreadFactory.forName(prefix + "-consumer-scheduler")); + } + + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) + public void afterStartUp() { + startConsumers(); + } + + protected void startConsumers() { + } + + protected void stopConsumers() { + } + + protected abstract int getMgmtThreadPoolSize(); + + @PreDestroy + public void destroy() { + stopConsumers(); + if (consumersExecutor != null) { + consumersExecutor.shutdownNow(); + } + if (mgmtExecutor != null) { + mgmtExecutor.shutdownNow(); + } + if (scheduler != null) { + scheduler.shutdownNow(); + } + } + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AppConsumerStats.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AppConsumerStats.java new file mode 100644 index 0000000..3acab87 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AppConsumerStats.java @@ -0,0 +1,85 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.queue; + +import io.micrometer.core.instrument.Timer; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.stats.StatsCounter; +import sanbing.jcpp.infrastructure.stats.StatsFactory; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class AppConsumerStats { + public static final String TOTAL_MSGS = "totalMsgs"; + public static final String LOGIN_EVENTS = "loginEvents"; + public static final String HEARTBEAT_EVENTS = "heartBeatEvents"; + public static final String GUN_RUN_STATUS_EVENTS = "gunRunStatusEvents"; + public static final String CHARGING_PROGRESS_EVENTS = "chargingProgressEvents"; + public static final String TRANSACTION_RECORD_EVENTS = "transactionRecordEvents"; + + private final StatsCounter totalCounter; + private final StatsCounter loginCounter; + private final StatsCounter heartBeatCounter; + private final StatsCounter gunRunStatusCounter; + private final StatsCounter chargingProgressCounter; + private final StatsCounter transactionRecordCounter; + private final Timer appConsumerTimer; + + private final List counters = new ArrayList<>(); + + public AppConsumerStats(StatsFactory statsFactory) { + String statsKey = "appConsumer"; + + this.totalCounter = register(statsFactory.createStatsCounter(statsKey, TOTAL_MSGS)); + this.loginCounter = register(statsFactory.createStatsCounter(statsKey, LOGIN_EVENTS)); + this.heartBeatCounter = register(statsFactory.createStatsCounter(statsKey, HEARTBEAT_EVENTS)); + this.gunRunStatusCounter = register(statsFactory.createStatsCounter(statsKey, GUN_RUN_STATUS_EVENTS)); + this.chargingProgressCounter = register(statsFactory.createStatsCounter(statsKey, CHARGING_PROGRESS_EVENTS)); + this.transactionRecordCounter = register(statsFactory.createStatsCounter(statsKey, TRANSACTION_RECORD_EVENTS)); + this.appConsumerTimer = statsFactory.createTimer(statsKey); + } + + private StatsCounter register(StatsCounter counter) { + counters.add(counter); + return counter; + } + + public void log(UplinkQueueMessage msg) { + totalCounter.increment(); + if (msg.hasLoginRequest()) { + loginCounter.increment(); + } else if (msg.hasHeartBeatRequest()) { + heartBeatCounter.increment(); + } else if (msg.hasGunRunStatusProto()) { + gunRunStatusCounter.increment(); + } else if (msg.hasChargingProgressProto()) { + chargingProgressCounter.increment(); + } else if (msg.hasTransactionRecord()) { + transactionRecordCounter.increment(); + } + + appConsumerTimer.record(Duration.ofMillis(System.currentTimeMillis() - TracerContextUtil.getCurrentTracer().getTracerTs())); + } + + public void printStats() { + int total = totalCounter.get(); + if (total > 0) { + StringBuilder stats = new StringBuilder(); + counters.forEach(counter -> { + stats.append(counter.getName()).append(" = [").append(counter.get()).append("] "); + }); + log.info("App Queue Consumer Stats: {}", stats); + } + } + + public void reset() { + counters.forEach(StatsCounter::clear); + } +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AppQueueConsumerManager.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AppQueueConsumerManager.java new file mode 100644 index 0000000..62b7bff --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/AppQueueConsumerManager.java @@ -0,0 +1,296 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.queue; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.queue.QueueConsumer; +import sanbing.jcpp.infrastructure.queue.QueueMsg; +import sanbing.jcpp.infrastructure.queue.common.QueueConfig; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; +import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +@Slf4j +public class AppQueueConsumerManager { + + protected final String queueName; + @Getter + protected C config; + protected final MsgPackProcessor msgPackProcessor; + protected final BiFunction> consumerCreator; + protected final ExecutorService consumerExecutor; + protected final ScheduledExecutorService scheduler; + protected final ExecutorService taskExecutor; + + private final Queue tasks = new ConcurrentLinkedQueue<>(); + private final ReentrantLock lock = new ReentrantLock(); + + @Getter + private volatile Set partitions; + protected volatile ConsumerWrapper consumerWrapper; + protected volatile boolean stopped; + + @Builder + public AppQueueConsumerManager(String queueName, C config, + MsgPackProcessor msgPackProcessor, + BiFunction> consumerCreator, + ExecutorService consumerExecutor, + ScheduledExecutorService scheduler, + ExecutorService taskExecutor) { + this.queueName = queueName; + this.config = config; + this.msgPackProcessor = msgPackProcessor; + this.consumerCreator = consumerCreator; + this.consumerExecutor = consumerExecutor; + this.scheduler = scheduler; + this.taskExecutor = taskExecutor; + if (config != null) { + init(config); + } + } + + public void init(C config) { + this.config = config; + if (config.isConsumerPerPartition()) { + this.consumerWrapper = new ConsumerPerPartitionWrapper(); + } else { + this.consumerWrapper = new SingleConsumerWrapper(); + } + log.debug("[{}] Initialized consumer for queue: {}", queueName, config); + } + + public void update(C config) { + addTask(QueueConsumerManagerTask.configUpdate(config)); + } + + public void update(Set partitions) { + addTask(QueueConsumerManagerTask.partitionChange(partitions)); + } + + protected void addTask(QueueConsumerManagerTask todo) { + if (stopped) { + return; + } + tasks.add(todo); + log.info("[{}] Added task: {}", queueName, todo); + tryProcessTasks(); + } + + @SuppressWarnings("unchecked") + private void tryProcessTasks() { + taskExecutor.submit(() -> { + if (lock.tryLock()) { + try { + C newConfig = null; + Set newPartitions = null; + while (!stopped) { + QueueConsumerManagerTask task = tasks.poll(); + if (task == null) { + break; + } + log.info("[{}] Processing task: {}", queueName, task); + + if (task.getEvent() == QueueEvent.PARTITION_CHANGE) { + newPartitions = task.getPartitions(); + } else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) { + newConfig = (C) task.getConfig(); + } else { + processTask(task); + } + } + if (stopped) { + return; + } + if (newConfig != null) { + doUpdate(newConfig); + } + if (newPartitions != null) { + doUpdate(newPartitions); + } + } catch (Exception e) { + log.error("[{}] Failed to process tasks", queueName, e); + } finally { + lock.unlock(); + } + } else { + log.trace("[{}] Failed to acquire lock", queueName); + scheduler.schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); + } + }); + } + + protected void processTask(QueueConsumerManagerTask task) { + } + + private void doUpdate(C newConfig) { + log.info("[{}] Processing queue update: {}", queueName, newConfig); + var oldConfig = this.config; + this.config = newConfig; + if (log.isTraceEnabled()) { + log.trace("[{}] Old queue configuration: {}", queueName, oldConfig); + log.trace("[{}] New queue configuration: {}", queueName, newConfig); + } + + if (oldConfig == null) { + init(config); + } else if (newConfig.isConsumerPerPartition() != oldConfig.isConsumerPerPartition()) { + consumerWrapper.getConsumers().forEach(QueueConsumerTask::initiateStop); + consumerWrapper.getConsumers().forEach(QueueConsumerTask::awaitCompletion); + + init(config); + if (partitions != null) { + doUpdate(partitions); + } + } else { + log.trace("[{}] Silently applied new config, because consumer-per-partition not changed", queueName); + } + } + + private void doUpdate(Set partitions) { + this.partitions = partitions; + consumerWrapper.updatePartitions(partitions); + } + + private void launchConsumer(QueueConsumerTask consumerTask) { + log.info("[{}] Launching consumer", consumerTask.getKey()); + Future consumerLoop = consumerExecutor.submit(() -> { + JCPPThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString()); + try { + consumerLoop(consumerTask.getConsumer()); + } catch (Throwable e) { + log.error("Failure in consumer loop", e); + } + log.info("[{}] Consumer stopped", consumerTask.getKey()); + }); + consumerTask.setTask(consumerLoop); + } + + private void consumerLoop(QueueConsumer consumer) { + while (!stopped && !consumer.isStopped()) { + try { + List msgs = consumer.poll(config.getPollInterval()); + if (msgs.isEmpty()) { + continue; + } + processMsgs(msgs, consumer, config); + } catch (Exception e) { + if (!consumer.isStopped()) { + log.warn("Failed to process messages from queue", e); + try { + Thread.sleep(config.getPollInterval()); + } catch (InterruptedException e2) { + log.trace("Failed to wait until the server has capacity to handle new requests", e2); + } + } + } + } + if (consumer.isStopped()) { + consumer.unsubscribe(); + } + } + + protected void processMsgs(List msgs, QueueConsumer consumer, C config) throws Exception { + msgPackProcessor.process(msgs, consumer, config); + } + + public void stop() { + log.debug("[{}] Stopping consumers", queueName); + consumerWrapper.getConsumers().forEach(QueueConsumerTask::initiateStop); + stopped = true; + } + + public void awaitStop() { + log.debug("[{}] Waiting for consumers to stop", queueName); + consumerWrapper.getConsumers().forEach(QueueConsumerTask::awaitCompletion); + log.debug("[{}] Unsubscribed and stopped consumers", queueName); + } + + private static String partitionsToString(Collection partitions) { + return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]")); + } + + public interface MsgPackProcessor { + void process(List msgs, QueueConsumer consumer, C config) throws Exception; + } + + public interface ConsumerWrapper { + + void updatePartitions(Set partitions); + + Collection> getConsumers(); + + } + + class ConsumerPerPartitionWrapper implements ConsumerWrapper { + private final Map> consumers = new HashMap<>(); + + @Override + public void updatePartitions(Set partitions) { + Set addedPartitions = new HashSet<>(partitions); + addedPartitions.removeAll(consumers.keySet()); + + Set removedPartitions = new HashSet<>(consumers.keySet()); + removedPartitions.removeAll(partitions); + log.info("[{}] Added partitions: {}, removed partitions: {}", queueName, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); + + removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop()); + removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion()); + + addedPartitions.forEach((tpi) -> { + Integer partitionId = tpi.getPartition().orElse(-1); + String key = queueName + "-" + partitionId; + QueueConsumerTask consumer = new QueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId)); + consumers.put(tpi, consumer); + consumer.subscribe(Set.of(tpi)); + launchConsumer(consumer); + }); + } + + @Override + public Collection> getConsumers() { + return consumers.values(); + } + } + + class SingleConsumerWrapper implements ConsumerWrapper { + private QueueConsumerTask consumer; + + @Override + public void updatePartitions(Set partitions) { + log.info("[{}] New partitions: {}", queueName, partitionsToString(partitions)); + if (partitions.isEmpty()) { + if (consumer != null && consumer.isRunning()) { + consumer.initiateStop(); + consumer.awaitCompletion(); + } + consumer = null; + return; + } + + if (consumer == null) { + consumer = new QueueConsumerTask<>(queueName, () -> consumerCreator.apply(config, null)); // no partitionId passed + } + consumer.subscribe(partitions); + if (!consumer.isRunning()) { + launchConsumer(consumer); + } + } + + @Override + public Collection> getConsumers() { + if (consumer == null) { + return Collections.emptyList(); + } + return List.of(consumer); + } + } +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueConsumerManagerTask.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueConsumerManagerTask.java new file mode 100644 index 0000000..9a26bcf --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueConsumerManagerTask.java @@ -0,0 +1,37 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.queue; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import sanbing.jcpp.infrastructure.queue.common.QueueConfig; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; + +import java.util.Set; + +@Getter +@ToString +@AllArgsConstructor +public class QueueConsumerManagerTask { + + private final QueueEvent event; + private QueueConfig config; + private Set partitions; + private boolean drainQueue; + + public static QueueConsumerManagerTask delete(boolean drainQueue) { + return new QueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue); + } + + public static QueueConsumerManagerTask configUpdate(QueueConfig config) { + return new QueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, config, null, false); + } + + public static QueueConsumerManagerTask partitionChange(Set partitions) { + return new QueueConsumerManagerTask(QueueEvent.PARTITION_CHANGE, null, partitions, false); + } + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueConsumerTask.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueConsumerTask.java new file mode 100644 index 0000000..7e5cad7 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueConsumerTask.java @@ -0,0 +1,78 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.queue; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.queue.QueueConsumer; +import sanbing.jcpp.infrastructure.queue.QueueMsg; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; + +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +@Slf4j +public class QueueConsumerTask { + + @Getter + private final Object key; + private volatile QueueConsumer consumer; + private volatile Supplier> consumerSupplier; + + @Setter + private Future task; + + public QueueConsumerTask(Object key, Supplier> consumerSupplier) { + this.key = key; + this.consumer = null; + this.consumerSupplier = consumerSupplier; + } + + public QueueConsumer getConsumer() { + if (consumer == null) { + synchronized (this) { + if (consumer == null) { + Objects.requireNonNull(consumerSupplier, "consumerSupplier for key [" + key + "] is null"); + consumer = consumerSupplier.get(); + Objects.requireNonNull(consumer, "consumer for key [" + key + "] is null"); + consumerSupplier = null; + } + } + } + return consumer; + } + + public void subscribe(Set partitions) { + log.info("[{}] Subscribing to partitions: {}", key, partitions); + getConsumer().subscribe(partitions); + } + + public void initiateStop() { + log.debug("[{}] Initiating stop", key); + getConsumer().stop(); + } + + public void awaitCompletion() { + log.trace("[{}] Awaiting finish", key); + if (isRunning()) { + try { + task.get(30, TimeUnit.SECONDS); + log.trace("[{}] Awaited finish", key); + } catch (Exception e) { + log.warn("[{}] Failed to await for consumer to stop", key, e); + } + task = null; + } + } + + public boolean isRunning() { + return task != null; + } + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueEvent.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueEvent.java new file mode 100644 index 0000000..0f17161 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/QueueEvent.java @@ -0,0 +1,13 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.queue; + +import java.io.Serializable; + +public enum QueueEvent implements Serializable { + + PARTITION_CHANGE, CONFIG_UPDATE, DELETE + +} diff --git a/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/consumer/ProtocolUplinkConsumerService.java b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/consumer/ProtocolUplinkConsumerService.java new file mode 100644 index 0000000..e7ab6d0 --- /dev/null +++ b/jcpp-app/src/main/java/sanbing/jcpp/app/service/queue/consumer/ProtocolUplinkConsumerService.java @@ -0,0 +1,236 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.app.service.queue.consumer; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import sanbing.jcpp.app.service.PileProtocolService; +import sanbing.jcpp.app.service.queue.AbstractConsumerService; +import sanbing.jcpp.app.service.queue.AppConsumerStats; +import sanbing.jcpp.app.service.queue.AppQueueConsumerManager; +import sanbing.jcpp.infrastructure.queue.*; +import sanbing.jcpp.infrastructure.queue.common.QueueConfig; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; +import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider; +import sanbing.jcpp.infrastructure.queue.discovery.event.PartitionChangeEvent; +import sanbing.jcpp.infrastructure.queue.processing.IdMsgPair; +import sanbing.jcpp.infrastructure.queue.provider.AppQueueFactory; +import sanbing.jcpp.infrastructure.stats.StatsFactory; +import sanbing.jcpp.infrastructure.util.annotation.AppComponent; +import sanbing.jcpp.infrastructure.util.codec.ByteUtil; +import sanbing.jcpp.infrastructure.util.mdc.MDCUtils; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.infrastructure.util.trace.TracerRunnable; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_PREFIX; +import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_TS; +import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ID; +import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ORIGIN; + + +/** + * @author baigod + */ +@Service +@AppComponent +@Slf4j +public class ProtocolUplinkConsumerService extends AbstractConsumerService implements ApplicationListener { + + @Value("${queue.app.poll-interval}") + private int pollInterval; + @Value("${queue.app.pack-processing-timeout}") + private long packProcessingTimeout; + @Value("${queue.app.consumer-per-partition}") + private boolean consumerPerPartition; + @Value("${queue.app.stats.enabled}") + private boolean statsEnabled; + + private final PileProtocolService pileProtocolService; + + private final AppQueueFactory appQueueFactory; + + private AppQueueConsumerManager, AppQueueConfig> appConsumer; + + private final AppConsumerStats stats; + + public ProtocolUplinkConsumerService(PartitionProvider partitionProvider, + ApplicationEventPublisher eventPublisher, + PileProtocolService pileProtocolService, + AppQueueFactory appQueueFactory, + StatsFactory statsFactory) { + super(partitionProvider, eventPublisher); + this.pileProtocolService = pileProtocolService; + this.appQueueFactory = appQueueFactory; + this.stats = new AppConsumerStats(statsFactory); + } + + @PostConstruct + public void init() { + super.init("jcpp-app"); + + log.info("Initializing Protocol Uplink Messages Queue Subscriptions."); + + this.appConsumer = AppQueueConsumerManager., AppQueueConfig>builder() + .queueName("protocol uplink") + .config(AppQueueConfig.of(consumerPerPartition, pollInterval)) + .msgPackProcessor(this::processMsgs) + .consumerCreator((config, partitionId) -> appQueueFactory.createProtocolUplinkMsgConsumer()) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .build(); + } + + + @PreDestroy + public void destroy() { + super.destroy(); + } + + + @Override + protected void stopConsumers() { + super.stopConsumers(); + appConsumer.stop(); + appConsumer.awaitStop(); + } + + + @Scheduled(fixedDelayString = "${queue.app.stats.print-interval-ms}") + public void printStats() { + if (statsEnabled) { + stats.printStats(); + stats.reset(); + } + } + + private void processMsgs(List> msgs, QueueConsumer> consumer, AppQueueConfig config) throws Exception { + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).toList(); + ConcurrentMap> pendingMap = orderedMsgList.stream().collect( + Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + PackProcessingContext> ctx = new PackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + PendingMsgHolder pendingMsgHolder = new PendingMsgHolder(); + Future packSubmitFuture = consumersExecutor.submit(new TracerRunnable(() -> + orderedMsgList.forEach((element) -> { + UUID id = element.getUuid(); + ProtoQueueMsg msg = element.getMsg(); + tracer(msg); + log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); + Callback callback = new PackCallback<>(id, ctx); + try { + UplinkQueueMessage uplinkQueueMsg = msg.getValue(); + pendingMsgHolder.setUplinkQueueMessage(uplinkQueueMsg); + if (uplinkQueueMsg.hasLoginRequest()) { + pileProtocolService.pileLogin(uplinkQueueMsg, callback); + } else if (uplinkQueueMsg.hasHeartBeatRequest()) { + pileProtocolService.heartBeat(uplinkQueueMsg, callback); + } else if (uplinkQueueMsg.hasVerifyPricingRequest()) { + pileProtocolService.verifyPricing(uplinkQueueMsg, callback); + } else if (uplinkQueueMsg.hasQueryPricingRequest()) { + pileProtocolService.queryPricing(uplinkQueueMsg, callback); + } else if (uplinkQueueMsg.hasGunRunStatusProto()) { + pileProtocolService.postGunRunStatus(uplinkQueueMsg, callback); + } else if (uplinkQueueMsg.hasChargingProgressProto()) { + pileProtocolService.postChargingProgress(uplinkQueueMsg, callback); + } else if (uplinkQueueMsg.hasSetPricingResponse()) { + pileProtocolService.onSetPricingResponse(uplinkQueueMsg, callback); + } else if (uplinkQueueMsg.hasRemoteStartChargingResponse()) { + pileProtocolService.onRemoteStartChargingResponse(uplinkQueueMsg, callback); + } else if (uplinkQueueMsg.hasRemoteStopChargingResponse()) { + pileProtocolService.onRemoteStopChargingResponse(uplinkQueueMsg, callback); + } else if(uplinkQueueMsg.hasTransactionRecord()){ + pileProtocolService.onTransactionRecord(uplinkQueueMsg, callback); + }else { + callback.onSuccess(); + } + + if (statsEnabled) { + stats.log(uplinkQueueMsg); + } + } catch (Throwable e) { + log.warn("[{}] Failed to process message: {}", id, msg, e); + callback.onFailure(e); + } + })) + ); + if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) { + if (!packSubmitFuture.isDone()) { + packSubmitFuture.cancel(true); + UplinkQueueMessage lastSubmitMsg = pendingMsgHolder.getUplinkQueueMessage(); + log.warn("Timeout to process message: {}", lastSubmitMsg); + } + if (log.isDebugEnabled()) { + ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue())); + } + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue())); + } + consumer.commit(); + } + + private void tracer(ProtoQueueMsg msg) { + Optional.ofNullable(msg.getHeaders().get(MSG_MD_PREFIX + JCPP_TRACER_ID)) + .map(tracerId -> { + String origin = null; + byte[] tracerOrigin = msg.getHeaders().get(MSG_MD_PREFIX + JCPP_TRACER_ORIGIN); + if (tracerOrigin != null) { + origin = ByteUtil.bytesToString(tracerOrigin); + } + + long ts = System.currentTimeMillis(); + byte[] tracerTs = msg.getHeaders().get(MSG_MD_PREFIX + MSG_MD_TS); + if (tracerTs != null) { + ts = ByteUtil.bytesToLong(tracerTs); + } + + return TracerContextUtil.newTracer(ByteUtil.bytesToString(tracerId), origin, ts); + }) + .orElseGet(TracerContextUtil::newTracer); + + MDCUtils.recordTracer(); + } + + @Override + protected int getMgmtThreadPoolSize() { + return Math.max(Runtime.getRuntime().availableProcessors(), 4); + } + + @Override + protected void onJCPPApplicationEvent(PartitionChangeEvent event) { + Set appPartitions = event.getAppPartitions(); + log.info("Subscribing to partitions: {}", appPartitions); + appConsumer.update(appPartitions); + } + + @Data(staticConstructor = "of") + public static class AppQueueConfig implements QueueConfig { + private final boolean consumerPerPartition; + private final int pollInterval; + } + + @Setter + @Getter + private static class PendingMsgHolder { + private volatile UplinkQueueMessage uplinkQueueMessage; + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-cache/pom.xml b/jcpp-infrastructure-cache/pom.xml new file mode 100644 index 0000000..2019e50 --- /dev/null +++ b/jcpp-infrastructure-cache/pom.xml @@ -0,0 +1,51 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-infrastructure-cache + jar + JChargePointProtocol Infrastructure Cache Module + 基础缓存管理模块 + + + ${basedir}/.. + + + + + org.springframework.data + spring-data-redis + + + io.lettuce + lettuce-core + + + org.apache.commons + commons-pool2 + + + sanbing + jcpp-infrastructure-util + + + com.github.ben-manes.caffeine + caffeine + + + + diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheConstants.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheConstants.java new file mode 100644 index 0000000..7e632ae --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheConstants.java @@ -0,0 +1,13 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +public final class CacheConstants { + + public static final String PILE_CACHE = "piles"; + + public static final String PILE_SESSION_CACHE = "pileSessions"; + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecs.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecs.java new file mode 100644 index 0000000..f582448 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecs.java @@ -0,0 +1,13 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import lombok.Data; + +@Data +public class CacheSpecs { + private Integer timeToLiveInMinutes; + private Integer maxSize; +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecsMap.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecsMap.java new file mode 100644 index 0000000..e0af10d --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecsMap.java @@ -0,0 +1,22 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import lombok.Data; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +@Configuration +@ConfigurationProperties(prefix = "cache") +@Data +public class CacheSpecsMap { + + @Getter + private Map specs; + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheTransaction.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheTransaction.java new file mode 100644 index 0000000..a90ab57 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheTransaction.java @@ -0,0 +1,15 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +public interface CacheTransaction { + + void put(K key, V value); + + boolean commit(); + + void rollback(); + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheValueWrapper.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheValueWrapper.java new file mode 100644 index 0000000..f12f122 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheValueWrapper.java @@ -0,0 +1,11 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +public interface CacheValueWrapper { + + T get(); + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CaffeineCacheTransaction.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CaffeineCacheTransaction.java new file mode 100644 index 0000000..c58d79c --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CaffeineCacheTransaction.java @@ -0,0 +1,48 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Slf4j +@RequiredArgsConstructor +public class CaffeineCacheTransaction implements CacheTransaction { + @Getter + private final UUID id = UUID.randomUUID(); + private final CaffeineTransactionalCache cache; + @Getter + private final List keys; + @Getter + @Setter + private boolean failed; + + private final Map pendingPuts = new LinkedHashMap<>(); + + @Override + public void put(K key, V value) { + pendingPuts.put(key, value); + } + + @Override + public boolean commit() { + return cache.commit(id, pendingPuts); + } + + @Override + public void rollback() { + cache.rollback(id); + } + + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CaffeineTransactionalCache.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CaffeineTransactionalCache.java new file mode 100644 index 0000000..475d64a --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CaffeineTransactionalCache.java @@ -0,0 +1,180 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@RequiredArgsConstructor +public abstract class CaffeineTransactionalCache implements TransactionalCache { + + @Getter + protected final String cacheName; + protected final Cache cache; + protected final Lock lock = new ReentrantLock(); + private final Map> objectTransactions = new HashMap<>(); + private final Map> transactions = new HashMap<>(); + + public CaffeineTransactionalCache(CacheManager cacheManager, String cacheName) { + this.cacheName = cacheName; + this.cache = Optional.ofNullable(cacheManager.getCache(cacheName)) + .orElseThrow(() -> new IllegalArgumentException("Cache '" + cacheName + "' is not configured")); + } + + @Override + public CacheValueWrapper get(K key) { + return SimpleCacheValueWrapper.wrap(cache.get(key)); + } + + @Override + public void put(K key, V value) { + lock.lock(); + try { + failAllTransactionsByKey(key); + cache.put(key, value); + } finally { + lock.unlock(); + } + } + + @Override + public void putIfAbsent(K key, V value) { + lock.lock(); + try { + failAllTransactionsByKey(key); + doPutIfAbsent(key, value); + } finally { + lock.unlock(); + } + } + + @Override + public void evict(K key) { + lock.lock(); + try { + failAllTransactionsByKey(key); + doEvict(key); + } finally { + lock.unlock(); + } + } + + @Override + public void evict(Collection keys) { + lock.lock(); + try { + keys.forEach(key -> { + failAllTransactionsByKey(key); + doEvict(key); + }); + } finally { + lock.unlock(); + } + } + + @Override + public void evictOrPut(K key, V value) { + evict(key); + } + + @Override + public CacheTransaction newTransactionForKey(K key) { + return newTransaction(Collections.singletonList(key)); + } + + @Override + public CacheTransaction newTransactionForKeys(List keys) { + return newTransaction(keys); + } + + void doPutIfAbsent(K key, V value) { + cache.putIfAbsent(key, value); + } + + void doEvict(K key) { + cache.evict(key); + } + + CacheTransaction newTransaction(List keys) { + lock.lock(); + try { + var transaction = new CaffeineCacheTransaction<>(this, keys); + var transactionId = transaction.getId(); + for (K key : keys) { + objectTransactions.computeIfAbsent(key, k -> new HashSet<>()).add(transactionId); + } + transactions.put(transactionId, transaction); + return transaction; + } finally { + lock.unlock(); + } + } + + public boolean commit(UUID trId, Map pendingPuts) { + lock.lock(); + try { + var tr = transactions.get(trId); + var success = !tr.isFailed(); + if (success) { + for (K key : tr.getKeys()) { + Set otherTransactions = objectTransactions.get(key); + if (otherTransactions != null) { + for (UUID otherTrId : otherTransactions) { + if (trId == null || !trId.equals(otherTrId)) { + transactions.get(otherTrId).setFailed(true); + } + } + } + } + pendingPuts.forEach(this::doPutIfAbsent); + } + removeTransaction(trId); + return success; + } finally { + lock.unlock(); + } + } + + void rollback(UUID id) { + lock.lock(); + try { + removeTransaction(id); + } finally { + lock.unlock(); + } + } + + private void removeTransaction(UUID id) { + CaffeineCacheTransaction transaction = transactions.remove(id); + if (transaction != null) { + for (var key : transaction.getKeys()) { + Set transactions = objectTransactions.get(key); + if (transactions != null) { + transactions.remove(id); + if (transactions.isEmpty()) { + objectTransactions.remove(key); + } + } + } + } + } + + protected void failAllTransactionsByKey(K key) { + Set transactionsIds = objectTransactions.get(key); + if (transactionsIds != null) { + for (UUID otherTrId : transactionsIds) { + transactions.get(otherTrId).setFailed(true); + } + } + } + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/HasVersion.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/HasVersion.java new file mode 100644 index 0000000..b6dbf38 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/HasVersion.java @@ -0,0 +1,14 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +public interface HasVersion { + + Integer getVersion(); + + default void setVersion(Integer version) { + } + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPCaffeineCacheConfiguration.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPCaffeineCacheConfiguration.java new file mode 100644 index 0000000..e1f37b7 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPCaffeineCacheConfiguration.java @@ -0,0 +1,83 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.Ticker; +import com.github.benmanes.caffeine.cache.Weigher; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Configuration +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@EnableCaching +@Slf4j +public class JCPPCaffeineCacheConfiguration { + + private final CacheSpecsMap configuration; + + public JCPPCaffeineCacheConfiguration(CacheSpecsMap configuration) { + this.configuration = configuration; + } + + @Bean + public CacheManager cacheManager() { + log.info("Initializing cache: {} specs {}", Arrays.toString(RemovalCause.values()), configuration.getSpecs()); + SimpleCacheManager manager = new SimpleCacheManager(); + if (configuration.getSpecs() != null) { + List caches = + configuration.getSpecs().entrySet().stream() + .map(entry -> buildCache(entry.getKey(), + entry.getValue())) + .collect(Collectors.toList()); + manager.setCaches(caches); + } + + manager.initializeCaches(); + + return manager; + } + + private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) { + + final Caffeine caffeineBuilder + = Caffeine.newBuilder() + .weigher(collectionSafeWeigher()) + .maximumWeight(cacheSpec.getMaxSize()) + .ticker(ticker()); + if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) { + caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES); + } + return new CaffeineCache(name, caffeineBuilder.build()); + } + + @Bean + public Ticker ticker() { + return Ticker.systemTicker(); + } + + private Weigher collectionSafeWeigher() { + return (Weigher) (key, value) -> { + if (value instanceof Collection) { + return ((Collection) value).size(); + } + return 1; + }; + } + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisClusterConfiguration.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisClusterConfiguration.java new file mode 100644 index 0000000..217efe8 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisClusterConfiguration.java @@ -0,0 +1,54 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; + +@Configuration +@ConditionalOnExpression("'${cache.type:null}'=='redis' && '${redis.connection.type:null}'=='cluster'") +@Slf4j +public class JCPPJCPPRedisClusterConfiguration extends JCPPRedisCacheConfiguration { + + @Value("${redis.cluster.nodes:}") + private String clusterNodes; + + @Value("${redis.cluster.max-redirects:12}") + private Integer maxRedirects; + + @Value("${redis.cluster.useDefaultPoolConfig:true}") + private boolean useDefaultPoolConfig; + + @Value("${redis.password:}") + private String password; + + + @Override + public LettuceConnectionFactory loadFactory() { + log.info("Initializing Redis Cluster on {}", clusterNodes); + RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(); + clusterConfiguration.setClusterNodes(getNodes(clusterNodes)); + clusterConfiguration.setMaxRedirects(maxRedirects); + clusterConfiguration.setPassword(password); + return new LettuceConnectionFactory(clusterConfiguration, buildClientConfig()); + } + + private LettucePoolingClientConfiguration buildClientConfig() { + + var clientConfigurationBuilder = LettucePoolingClientConfiguration.builder(); + + if (!useDefaultPoolConfig) { + clientConfigurationBuilder + .poolConfig(buildPoolConfig()); + } + return clientConfigurationBuilder + .build(); + } +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisSentinelConfiguration.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisSentinelConfiguration.java new file mode 100644 index 0000000..a08e813 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisSentinelConfiguration.java @@ -0,0 +1,59 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; + +@Configuration +@ConditionalOnMissingBean(JCPPCaffeineCacheConfiguration.class) +@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "sentinel") +@Slf4j +public class JCPPJCPPRedisSentinelConfiguration extends JCPPRedisCacheConfiguration { + + @Value("${redis.sentinel.master:}") + private String master; + + @Value("${redis.sentinel.sentinels:}") + private String sentinels; + + @Value("${redis.sentinel.password:}") + private String sentinelPassword; + + @Value("${redis.sentinel.useDefaultPoolConfig:true}") + private boolean useDefaultPoolConfig; + + @Value("${redis.db:}") + private Integer database; + + @Value("${redis.password:}") + private String password; + + @Override + public LettuceConnectionFactory loadFactory() { + log.info("Initializing Redis Sentinel on {}, sentinels: {}", master, sentinels); + RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); + redisSentinelConfiguration.setMaster(master); + redisSentinelConfiguration.setSentinels(getNodes(sentinels)); + redisSentinelConfiguration.setSentinelPassword(sentinelPassword); + redisSentinelConfiguration.setPassword(password); + redisSentinelConfiguration.setDatabase(database); + return new LettuceConnectionFactory(redisSentinelConfiguration, buildClientConfig()); + } + + private LettucePoolingClientConfiguration buildClientConfig() { + var clientConfigurationBuilder = LettucePoolingClientConfiguration.builder(); + if (!useDefaultPoolConfig) { + clientConfigurationBuilder.poolConfig(buildPoolConfig()); + } + return clientConfigurationBuilder.build(); + } +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisStandaloneConfiguration.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisStandaloneConfiguration.java new file mode 100644 index 0000000..bdfd115 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPJCPPRedisStandaloneConfiguration.java @@ -0,0 +1,78 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; + +import java.time.Duration; + +@Configuration +@ConditionalOnMissingBean(JCPPCaffeineCacheConfiguration.class) +@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "standalone") +@Slf4j +public class JCPPJCPPRedisStandaloneConfiguration extends JCPPRedisCacheConfiguration { + + @Value("${redis.standalone.host:localhost}") + private String host; + + @Value("${redis.standalone.port:6379}") + private Integer port; + + @Value("${redis.standalone.clientName:standalone}") + private String clientName; + + @Value("${redis.standalone.commandTimeout:30000}") + private Long commandTimeout; + + @Value("${redis.standalone.shutdownTimeout:5000}") + private Long shutdownTimeout; + + @Value("${redis.standalone.useDefaultClientConfig:true}") + private boolean useDefaultClientConfig; + + @Value("${redis.standalone.usePoolConfig:false}") + private boolean usePoolConfig; + + @Value("${redis.db:0}") + private Integer db; + + @Value("${redis.password:}") + private String password; + + @Override + public LettuceConnectionFactory loadFactory() { + log.info("Initializing Redis Standalone on {}:{}", host, port); + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(); + standaloneConfiguration.setHostName(host); + standaloneConfiguration.setPort(port); + standaloneConfiguration.setDatabase(db); + standaloneConfiguration.setPassword(password); + return new LettuceConnectionFactory(standaloneConfiguration, buildClientConfig()); + } + + private LettucePoolingClientConfiguration buildClientConfig() { + + var clientConfigurationBuilder = LettucePoolingClientConfiguration.builder(); + + if (!useDefaultClientConfig) { + clientConfigurationBuilder + .clientName(clientName) + .commandTimeout(Duration.ofMillis(commandTimeout)) + .shutdownTimeout(Duration.ofMillis(shutdownTimeout)); + } + + if (usePoolConfig) { + clientConfigurationBuilder.poolConfig(buildPoolConfig()); + } + return clientConfigurationBuilder.build(); + } +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java new file mode 100644 index 0000000..926368d --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java @@ -0,0 +1,168 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import io.lettuce.core.api.StatefulRedisConnection; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.ReactiveRedisTemplate; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@Configuration +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Data +@Slf4j +public abstract class JCPPRedisCacheConfiguration { + + private static final String COMMA = ","; + private static final String COLON = ":"; + + @Value("${redis.evictTtlInMs:60000}") + private int evictTtlInMs; + + @Value("${redis.pool_config.maxTotal:128}") + private int maxTotal; + + @Value("${redis.pool_config.maxIdle:128}") + private int maxIdle; + + @Value("${redis.pool_config.minIdle:16}") + private int minIdle; + + @Value("${redis.pool_config.testOnBorrow:true}") + private boolean testOnBorrow; + + @Value("${redis.pool_config.testOnReturn:true}") + private boolean testOnReturn; + + @Value("${redis.pool_config.testWhileIdle:true}") + private boolean testWhileIdle; + + @Value("${redis.pool_config.minEvictableMs:60000}") + private long minEvictableMs; + + @Value("${redis.pool_config.evictionRunsMs:30000}") + private long evictionRunsMs; + + @Value("${redis.pool_config.maxWaitMills:60000}") + private long maxWaitMills; + + @Value("${redis.pool_config.numberTestsPerEvictionRun:3}") + private int numberTestsPerEvictionRun; + + @Value("${redis.pool_config.blockWhenExhausted:true}") + private boolean blockWhenExhausted; + + @Bean + public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory(LettuceConnectionFactory loadFactory) { + return loadFactory; + } + + @Bean + public RedisConnectionFactory redisConnectionFactory(LettuceConnectionFactory loadFactory) { + return loadFactory; + } + + @Bean + protected abstract LettuceConnectionFactory loadFactory(); + + @Bean + public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { + DefaultFormattingConversionService redisConversionService = new DefaultFormattingConversionService(); + RedisCacheConfiguration.registerDefaultConverters(redisConversionService); + registerDefaultConverters(redisConversionService); + RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().withConversionService(redisConversionService); + return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(configuration) + .transactionAware() + .build(); + } + + @Bean + public ReactiveRedisTemplate reactiveRedisTemplate(ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) { + RedisSerializationContext serializationContext = RedisSerializationContext + .newSerializationContext() + .key(new StringRedisSerializer()) + .value(new GenericJackson2JsonRedisSerializer()) + .hashKey(new StringRedisSerializer()) + .hashValue(new GenericJackson2JsonRedisSerializer()) + .build(); + + return new ReactiveRedisTemplate<>(reactiveRedisConnectionFactory, serializationContext); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + + private static void registerDefaultConverters(ConverterRegistry registry) { + Assert.notNull(registry, "ConverterRegistry must not be null!"); + registry.addConverter(UUID.class, String.class, UUID::toString); + } + + protected GenericObjectPoolConfig> buildPoolConfig() { + GenericObjectPoolConfig> poolConfig = new GenericObjectPoolConfig<>(); + poolConfig.setMaxTotal(maxTotal); + poolConfig.setMaxIdle(maxIdle); + poolConfig.setMinIdle(minIdle); + poolConfig.setTestOnBorrow(testOnBorrow); + poolConfig.setTestOnReturn(testOnReturn); + poolConfig.setTestWhileIdle(testWhileIdle); + poolConfig.setSoftMinEvictableIdleDuration(Duration.ofMillis(minEvictableMs)); + poolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(evictionRunsMs)); + poolConfig.setMaxWait(Duration.ofMillis(maxWaitMills)); + poolConfig.setNumTestsPerEvictionRun(numberTestsPerEvictionRun); + poolConfig.setBlockWhenExhausted(blockWhenExhausted); + return poolConfig; + } + + protected List getNodes(String nodes) { + List result; + if (!StringUtils.hasText(nodes)) { + result = Collections.emptyList(); + } else { + result = new ArrayList<>(); + for (String hostPort : nodes.split(COMMA)) { + String host = hostPort.split(COLON)[0]; + int port = Integer.parseInt(hostPort.split(COLON)[1]); + result.add(new RedisNode(host, port)); + } + } + return result; + } + + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisSerializer.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisSerializer.java new file mode 100644 index 0000000..fd1d3f6 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisSerializer.java @@ -0,0 +1,18 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.lang.Nullable; + +public interface JCPPRedisSerializer { + + @Nullable + byte[] serialize(@Nullable T t) throws SerializationException; + + @Nullable + T deserialize(K key, @Nullable byte[] bytes) throws SerializationException; + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/RedisCacheTransaction.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/RedisCacheTransaction.java new file mode 100644 index 0000000..64dbf60 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/RedisCacheTransaction.java @@ -0,0 +1,45 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.RedisConnection; + +import java.io.Serializable; +import java.util.Objects; + +@Slf4j +@RequiredArgsConstructor +public class RedisCacheTransaction implements CacheTransaction { + + private final RedisTransactionalCache cache; + private final RedisConnection connection; + + @Override + public void put(K key, V value) { + cache.put(key, value, connection); + } + + @Override + public boolean commit() { + try { + var execResult = connection.exec(); + return execResult.stream().anyMatch(Objects::nonNull); + } finally { + connection.close(); + } + } + + @Override + public void rollback() { + try { + connection.discard(); + } finally { + connection.close(); + } + } + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/RedisTransactionalCache.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/RedisTransactionalCache.java new file mode 100644 index 0000000..7ba1e8f --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/RedisTransactionalCache.java @@ -0,0 +1,246 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import io.lettuce.core.RedisAsyncCommandsImpl; +import io.lettuce.core.RedisClient; +import io.lettuce.core.cluster.RedisAdvancedClusterAsyncCommandsImpl; +import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.support.NullValue; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.connection.lettuce.LettuceConnection; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +@Slf4j +public abstract class RedisTransactionalCache implements TransactionalCache { + + static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); + + @Getter + private final String cacheName; + @Getter + private final LettuceConnectionFactory connectionFactory; + private final RedisSerializer keySerializer = StringRedisSerializer.UTF_8; + private final JCPPRedisSerializer valueSerializer; + protected final Expiration evictExpiration; + protected final Expiration cacheTtl; + protected final boolean cacheEnabled; + + public RedisTransactionalCache(String cacheName, + CacheSpecsMap cacheSpecsMap, + LettuceConnectionFactory connectionFactory, + JCPPRedisCacheConfiguration configuration, + JCPPRedisSerializer valueSerializer) { + this.cacheName = cacheName; + this.connectionFactory = connectionFactory; + this.valueSerializer = valueSerializer; + this.evictExpiration = Expiration.from(configuration.getEvictTtlInMs(), TimeUnit.MILLISECONDS); + this.cacheTtl = Optional.ofNullable(cacheSpecsMap) + .map(CacheSpecsMap::getSpecs) + .map(specs -> specs.get(cacheName)) + .map(CacheSpecs::getTimeToLiveInMinutes) + .filter(ttl -> !ttl.equals(0)) + .map(ttl -> Expiration.from(ttl, TimeUnit.MINUTES)) + .orElseGet(Expiration::persistent); + this.cacheEnabled = Optional.ofNullable(cacheSpecsMap) + .map(CacheSpecsMap::getSpecs) + .map(x -> x.get(cacheName)) + .map(CacheSpecs::getMaxSize) + .map(size -> size > 0) + .orElse(false); + } + + @Override + public CacheValueWrapper get(K key) { + if (!cacheEnabled) { + return null; + } + try (var connection = connectionFactory.getConnection()) { + byte[] rawValue = doGet(key, connection); + if (rawValue == null || rawValue.length == 0) { + return null; + } else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) { + return SimpleCacheValueWrapper.empty(); + } else { + V value = valueSerializer.deserialize(key, rawValue); + return SimpleCacheValueWrapper.wrap(value); + } + } + } + + protected byte[] doGet(K key, RedisConnection connection) { + return connection.stringCommands().get(getRawKey(key)); + } + + @Override + public void put(K key, V value) { + if (!cacheEnabled) { + return; + } + try (var connection = connectionFactory.getConnection()) { + put(key, value, connection); + } + } + + public void put(K key, V value, RedisConnection connection) { + put(connection, key, value, RedisStringCommands.SetOption.UPSERT); + } + + @Override + public void putIfAbsent(K key, V value) { + if (!cacheEnabled) { + return; + } + try (var connection = connectionFactory.getConnection()) { + put(connection, key, value, RedisStringCommands.SetOption.SET_IF_ABSENT); + } + } + + @Override + public void evict(K key) { + if (!cacheEnabled) { + return; + } + try (var connection = connectionFactory.getConnection()) { + connection.keyCommands().del(getRawKey(key)); + } + } + + @Override + public void evict(Collection keys) { + if (!cacheEnabled) { + return; + } + if (keys.isEmpty()) { + return; + } + try (var connection = connectionFactory.getConnection()) { + connection.keyCommands().del(keys.stream().map(this::getRawKey).toArray(byte[][]::new)); + } + } + + @Override + public void evictOrPut(K key, V value) { + if (!cacheEnabled) { + return; + } + try (var connection = connectionFactory.getConnection()) { + var rawKey = getRawKey(key); + var records = connection.keyCommands().del(rawKey); + if (records == null || records == 0) { + //We need to put the value in case of Redis, because evict will NOT cancel concurrent transaction used to "get" the missing value from cache. + connection.stringCommands().set(rawKey, getRawValue(value), evictExpiration, RedisStringCommands.SetOption.UPSERT); + } + } + } + + @Override + public CacheTransaction newTransactionForKey(K key) { + byte[][] rawKey = new byte[][]{getRawKey(key)}; + RedisConnection connection = watch(rawKey); + return new RedisCacheTransaction<>(this, connection); + } + + @Override + public CacheTransaction newTransactionForKeys(List keys) { + RedisConnection connection = watch(keys.stream().map(this::getRawKey).toArray(byte[][]::new)); + return new RedisCacheTransaction<>(this, connection); + } + + @Override + public R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { + if (!cacheEnabled) { + return dbCall.get(); + } + return TransactionalCache.super.getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue); + } + + @SuppressWarnings("unchecked") + protected RedisConnection getConnection(byte[] rawKey) { + if (!connectionFactory.isClusterAware()) { + return connectionFactory.getConnection(); + } + + RedisClusterNode redisClusterNode = connectionFactory.getClusterConnection().clusterGetNodeForKey(rawKey); + Object nativeConnection = connectionFactory.getConnection().getNativeConnection(); + RedisClusterAsyncCommands connection = ((RedisAdvancedClusterAsyncCommandsImpl) nativeConnection).getConnection(redisClusterNode.getId()); + LettuceConnection lettuceConnection = new LettuceConnection(((RedisAsyncCommandsImpl) connection).getStatefulConnection(), + connectionFactory.getTimeout(), + RedisClient.create()); + lettuceConnection.setConvertPipelineAndTxResults(connectionFactory.getConvertPipelineAndTxResults()); + return lettuceConnection; + } + + protected RedisConnection watch(byte[][] rawKeysList) { + RedisConnection connection = getConnection(rawKeysList[0]); + try { + connection.watch(rawKeysList); + connection.multi(); + } catch (Exception e) { + connection.close(); + throw e; + } + return connection; + } + + protected byte[] getRawKey(K key) { + String keyString = cacheName + key.toString(); + byte[] rawKey; + try { + rawKey = keySerializer.serialize(keyString); + } catch (Exception e) { + log.warn("Failed to serialize the cache key: {}", key, e); + throw new RuntimeException(e); + } + if (rawKey == null) { + log.warn("Failed to serialize the cache key: {}", key); + throw new IllegalArgumentException("Failed to serialize the cache key!"); + } + return rawKey; + } + + protected byte[] getRawValue(V value) { + if (value == null) { + return BINARY_NULL_VALUE; + } else { + try { + return valueSerializer.serialize(value); + } catch (Exception e) { + log.warn("Failed to serialize the cache value: {}", value, e); + throw new RuntimeException(e); + } + } + } + + public void put(RedisConnection connection, K key, V value, RedisStringCommands.SetOption setOption) { + if (!cacheEnabled) { + return; + } + byte[] rawKey = getRawKey(key); + put(connection, rawKey, value, setOption); + } + + public void put(RedisConnection connection, byte[] rawKey, V value, RedisStringCommands.SetOption setOption) { + byte[] rawValue = getRawValue(value); + connection.stringCommands().set(rawKey, rawValue, this.cacheTtl, setOption); + } + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/SimpleCacheValueWrapper.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/SimpleCacheValueWrapper.java new file mode 100644 index 0000000..477f747 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/SimpleCacheValueWrapper.java @@ -0,0 +1,35 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.springframework.cache.Cache; + +@ToString +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class SimpleCacheValueWrapper implements CacheValueWrapper { + + private final T value; + + @Override + public T get() { + return value; + } + + public static SimpleCacheValueWrapper empty() { + return new SimpleCacheValueWrapper<>(null); + } + + public static SimpleCacheValueWrapper wrap(T value) { + return new SimpleCacheValueWrapper<>(value); + } + + @SuppressWarnings("unchecked") + public static SimpleCacheValueWrapper wrap(Cache.ValueWrapper source) { + return source == null ? null : new SimpleCacheValueWrapper<>((T) source.get()); + } +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/TransactionalCache.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/TransactionalCache.java new file mode 100644 index 0000000..f3b27f0 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/TransactionalCache.java @@ -0,0 +1,86 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +public interface TransactionalCache { + + String getCacheName(); + + CacheValueWrapper get(K key); + + void put(K key, V value); + + void putIfAbsent(K key, V value); + + void evict(K key); + + void evict(Collection keys); + + void evictOrPut(K key, V value); + + CacheTransaction newTransactionForKey(K key); + + + CacheTransaction newTransactionForKeys(List keys); + + default V getOrFetchFromDB(K key, Supplier dbCall, boolean cacheNullValue, boolean putToCache) { + if (putToCache) { + return getAndPutInTransaction(key, dbCall, cacheNullValue); + } else { + CacheValueWrapper cacheValueWrapper = get(key); + if (cacheValueWrapper != null) { + return cacheValueWrapper.get(); + } + return dbCall.get(); + } + } + + default V getAndPutInTransaction(K key, Supplier dbCall, boolean cacheNullValue) { + return getAndPutInTransaction(key, dbCall, Function.identity(), Function.identity(), cacheNullValue); + } + + default R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { + CacheValueWrapper cacheValueWrapper = get(key); + if (cacheValueWrapper != null) { + V cacheValue = cacheValueWrapper.get(); + return cacheValue != null ? cacheValueToResult.apply(cacheValue) : null; + } + var cacheTransaction = newTransactionForKey(key); + try { + R dbValue = dbCall.get(); + if (dbValue != null || cacheNullValue) { + cacheTransaction.put(key, dbValueToCacheValue.apply(dbValue)); + cacheTransaction.commit(); + return dbValue; + } else { + cacheTransaction.rollback(); + return null; + } + } catch (Throwable e) { + cacheTransaction.rollback(); + throw e; + } + } + + default R getOrFetchFromDB(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue, boolean putToCache) { + if (putToCache) { + return getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue); + } else { + CacheValueWrapper cacheValueWrapper = get(key); + if (cacheValueWrapper != null) { + var cacheValue = cacheValueWrapper.get(); + return cacheValue == null ? null : cacheValueToResult.apply(cacheValue); + } + return dbCall.get(); + } + } + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCache.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCache.java new file mode 100644 index 0000000..2297d4d --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCache.java @@ -0,0 +1,51 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + + +import java.io.Serializable; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Supplier; + +public interface VersionedCache extends TransactionalCache { + + CacheValueWrapper get(K key); + + default V get(K key, Supplier supplier) { + return get(key, supplier, true); + } + + default V get(K key, Supplier supplier, boolean putToCache) { + return Optional.ofNullable(get(key)) + .map(CacheValueWrapper::get) + .orElseGet(() -> { + V value = supplier.get(); + if (putToCache) { + put(key, value); + } + return value; + }); + } + + void put(K key, V value); + + void evict(K key); + + void evict(Collection keys); + + void evict(K key, Integer version); + + default Integer getVersion(V value) { + if (value == null) { + return 0; + } else if (value.getVersion() != null) { + return value.getVersion(); + } else { + return null; + } + } + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCacheKey.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCacheKey.java new file mode 100644 index 0000000..2529c17 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCacheKey.java @@ -0,0 +1,15 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import java.io.Serializable; + +public interface VersionedCacheKey extends Serializable { + + default boolean isVersioned() { + return false; + } + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCaffeineCache.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCaffeineCache.java new file mode 100644 index 0000000..9e7ede6 --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCaffeineCache.java @@ -0,0 +1,86 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import sanbing.jcpp.infrastructure.util.JCPPPair; + +import java.io.Serializable; + +public abstract class VersionedCaffeineCache extends CaffeineTransactionalCache implements VersionedCache { + + public VersionedCaffeineCache(CacheManager cacheManager, String cacheName) { + super(cacheManager, cacheName); + } + + @Override + public CacheValueWrapper get(K key) { + JCPPPair versionValuePair = doGet(key); + if (versionValuePair != null) { + return SimpleCacheValueWrapper.wrap(versionValuePair.getSecond()); + } + return null; + } + + @Override + public void put(K key, V value) { + Integer version = getVersion(value); + if (version == null) { + return; + } + doPut(key, value, version); + } + + private void doPut(K key, V value, Integer version) { + lock.lock(); + try { + JCPPPair versionValuePair = doGet(key); + if (versionValuePair == null || version > versionValuePair.getFirst()) { + failAllTransactionsByKey(key); + cache.put(key, wrapValue(value, version)); + } + } finally { + lock.unlock(); + } + } + + private JCPPPair doGet(K key) { + Cache.ValueWrapper source = cache.get(key); + if (source != null && source.get() instanceof JCPPPair pair) { + return pair; + } + return null; + } + + @Override + public void evict(K key) { + lock.lock(); + try { + failAllTransactionsByKey(key); + cache.evict(key); + } finally { + lock.unlock(); + } + } + + @Override + public void evict(K key, Integer version) { + if (version == null) { + return; + } + doPut(key, null, version); + } + + @Override + void doPutIfAbsent(K key, V value) { + cache.putIfAbsent(key, wrapValue(value, getVersion(value))); + } + + private JCPPPair wrapValue(V value, Integer version) { + return JCPPPair.of(version, value); + } + +} diff --git a/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java new file mode 100644 index 0000000..bfc5a1e --- /dev/null +++ b/jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java @@ -0,0 +1,155 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.cache; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.NotImplementedException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.ReturnType; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.io.Serializable; +import java.util.Arrays; + +@Slf4j +public abstract class VersionedRedisCache extends RedisTransactionalCache implements VersionedCache { + + private static final int VERSION_SIZE = 8; + private static final int VALUE_END_OFFSET = -1; + + static final byte[] SET_VERSIONED_VALUE_LUA_SCRIPT = StringRedisSerializer.UTF_8.serialize(""" + local key = KEYS[1] + local newValue = ARGV[1] + local newVersion = tonumber(ARGV[2]) + local expiration = tonumber(ARGV[3]) + + local function setNewValue() + local newValueWithVersion = struct.pack(">I8", newVersion) .. newValue + redis.call('SET', key, newValueWithVersion, 'EX', expiration) + end + + -- Get the current version (first 8 bytes) of the current value + local currentVersionBytes = redis.call('GETRANGE', key, 0, 7) + + if currentVersionBytes and #currentVersionBytes == 8 then + local currentVersion = struct.unpack(">I8", currentVersionBytes) + if newVersion > currentVersion then + setNewValue() + end + else + -- If the current value is absent or the current version is not found, set the new value + setNewValue() + end + """); + static final byte[] SET_VERSIONED_VALUE_SHA = StringRedisSerializer.UTF_8.serialize("0453cb1814135b706b4198b09a09f43c9f67bbfe"); + + public VersionedRedisCache(String cacheName, CacheSpecsMap cacheSpecsMap, LettuceConnectionFactory connectionFactory, JCPPRedisCacheConfiguration configuration, JCPPRedisSerializer valueSerializer) { + super(cacheName, cacheSpecsMap, connectionFactory, configuration, valueSerializer); + } + + @PostConstruct + public void init() { + try (var connection = getConnection(SET_VERSIONED_VALUE_SHA)) { + log.debug("Loading LUA with expected SHA[{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), connection.getNativeConnection()); + String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT); + if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { + log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), sha, connection.getNativeConnection()); + } + } catch (Throwable t) { + log.error("Error on Redis versioned cache init", t); + } + } + + @Override + protected byte[] doGet(K key, RedisConnection connection) { + if (!key.isVersioned()) { + return super.doGet(key, connection); + } + byte[] rawKey = getRawKey(key); + return connection.stringCommands().getRange(rawKey, VERSION_SIZE, VALUE_END_OFFSET); + } + + @Override + public void put(K key, V value) { + if (!key.isVersioned()) { + super.put(key, value); + return; + } + Integer version = getVersion(value); + if (version == null) { + return; + } + doPut(key, value, version, cacheTtl); + } + + @Override + public void put(K key, V value, RedisConnection connection) { + if (!key.isVersioned()) { + super.put(key, value, connection); // because scripting commands are not supported in transaction mode + return; + } + Integer version = getVersion(value); + if (version == null) { + return; + } + byte[] rawKey = getRawKey(key); + doPut(rawKey, value, version, cacheTtl, connection); + } + + private void doPut(K key, V value, Integer version, Expiration expiration) { + if (!cacheEnabled) { + return; + } + log.trace("put [{}][{}][{}]", key, value, version); + final byte[] rawKey = getRawKey(key); + try (var connection = getConnection(rawKey)) { + doPut(rawKey, value, version, expiration, connection); + } + } + + private void doPut(byte[] rawKey, V value, Integer version, Expiration expiration, RedisConnection connection) { + byte[] rawValue = getRawValue(value); + byte[] rawVersion = StringRedisSerializer.UTF_8.serialize(String.valueOf(version)); + byte[] rawExpiration = StringRedisSerializer.UTF_8.serialize(String.valueOf(expiration.getExpirationTimeInSeconds())); + try { + connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion, rawExpiration); + } catch (InvalidDataAccessApiUsageException e) { + log.debug("loading LUA [{}]", connection.getNativeConnection()); + String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT); + if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { + log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}]", new String(SET_VERSIONED_VALUE_SHA), sha); + } + try { + connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion, rawExpiration); + } catch (InvalidDataAccessApiUsageException ignored) { + log.debug("Slowly executing eval instead of fast evalsha"); + connection.scriptingCommands().eval(SET_VERSIONED_VALUE_LUA_SCRIPT, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion, rawExpiration); + } + } + } + + @Override + public void evict(K key, Integer version) { + log.trace("evict [{}][{}]", key, version); + if (version != null) { + doPut(key, null, version, evictExpiration); + } + } + + @Override + public void putIfAbsent(K key, V value) { + throw new NotImplementedException("putIfAbsent is not supported by versioned cache"); + } + + @Override + public void evictOrPut(K key, V value) { + throw new NotImplementedException("evictOrPut is not supported by versioned cache"); + } + +} diff --git a/jcpp-infrastructure-proto/pom.xml b/jcpp-infrastructure-proto/pom.xml new file mode 100644 index 0000000..2583d91 --- /dev/null +++ b/jcpp-infrastructure-proto/pom.xml @@ -0,0 +1,50 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-infrastructure-proto + jar + JChargePointProtocol Infrastructure Proto Module + 基础Protobuf模块 + + + ${basedir}/.. + + + + + com.google.protobuf + protobuf-java + + + com.google.protobuf + protobuf-java-util + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + diff --git a/jcpp-infrastructure-proto/src/main/java/sanbing/jcpp/infrastructure/proto/ProtoConverter.java b/jcpp-infrastructure-proto/src/main/java/sanbing/jcpp/infrastructure/proto/ProtoConverter.java new file mode 100644 index 0000000..6d91913 --- /dev/null +++ b/jcpp-infrastructure-proto/src/main/java/sanbing/jcpp/infrastructure/proto/ProtoConverter.java @@ -0,0 +1,57 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.proto; + + +import sanbing.jcpp.infrastructure.proto.model.PricingModel; +import sanbing.jcpp.infrastructure.proto.model.PricingModel.FlagPrice; +import sanbing.jcpp.infrastructure.proto.model.PricingModel.Period; +import sanbing.jcpp.proto.gen.ProtocolProto.*; + +import java.util.Map; + +/** + * @author baigod + */ +public class ProtoConverter { + + public static PricingModelProto toPricingModel(PricingModel pricingModel) { + // 创建 PricingModelProto 实例 + PricingModelProto.Builder builder = PricingModelProto.newBuilder(); + + // 设置字段 + builder.setType(PricingModelType.valueOf(pricingModel.getType().name())); + builder.setRule(PricingModelRule.valueOf(pricingModel.getRule().name())); + builder.setStandardElec(pricingModel.getStandardElec()); + builder.setStandardServ(pricingModel.getStandardServ()); + + // 转换 flagPriceList + for (Map.Entry entry : pricingModel.getFlagPriceList().entrySet()) { + PricingModelFlag flag = entry.getKey(); + FlagPrice flagPrice = entry.getValue(); + + FlagPriceProto flagPriceProto = FlagPriceProto.newBuilder() + .setFlag(PricingModelFlag.valueOf(flag.name())) // 枚举转换 + .setElec(flagPrice.getElec()) + .setServ(flagPrice.getServ()) + .build(); + + builder.putFlagPrice(flag.ordinal(), flagPriceProto); // 按 ordinal 值作为 key 存入 + } + + // 转换 PeriodsList + for (Period period : pricingModel.getPeriodsList()) { + PeriodProto periodProto = PeriodProto.newBuilder() + .setSn(period.getSn()) + .setBegin(period.getBegin().toString()) // 假设 begin 是 LocalTime, 转换为字符串 + .setEnd(period.getEnd().toString()) // 假设 end 是 LocalTime, 转换为字符串 + .setFlag(PricingModelFlag.valueOf(period.getFlag().name())) + .build(); + builder.addPeriod(periodProto); + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-proto/src/main/java/sanbing/jcpp/infrastructure/proto/model/PricingModel.java b/jcpp-infrastructure-proto/src/main/java/sanbing/jcpp/infrastructure/proto/model/PricingModel.java new file mode 100644 index 0000000..c80631f --- /dev/null +++ b/jcpp-infrastructure-proto/src/main/java/sanbing/jcpp/infrastructure/proto/model/PricingModel.java @@ -0,0 +1,78 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.proto.model; + +import lombok.*; +import sanbing.jcpp.proto.gen.ProtocolProto.PricingModelFlag; +import sanbing.jcpp.proto.gen.ProtocolProto.PricingModelRule; +import sanbing.jcpp.proto.gen.ProtocolProto.PricingModelType; + +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Data +public class PricingModel { + + private UUID id; + + // 计数器,供充电桩协议使用 + private int sequenceNumber; + + private String pileCode; + + private PricingModelType type; + + private PricingModelRule rule; + + /** + * 标准电价(单位分) + */ + private int standardElec; + + /** + * 标准服务费(单位分) + */ + private int standardServ; + + /** + * 分时电价 + */ + private Map flagPriceList; + + /** + * 分时时段 + */ + private List periodsList; + + @Setter + @Getter + public static class Period { + private int sn; + + // 起始时间 + private LocalTime begin; + + // 结束时间 + private LocalTime end; + + // 尖峰平谷标识 + private PricingModelFlag flag; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class FlagPrice { + + // 分时电价,单位分 + private int elec; + + // 分时服务费,单位分 + private int serv; + } + +} \ No newline at end of file diff --git a/jcpp-infrastructure-proto/src/main/proto/cluster.proto b/jcpp-infrastructure-proto/src/main/proto/cluster.proto new file mode 100644 index 0000000..c2455fc --- /dev/null +++ b/jcpp-infrastructure-proto/src/main/proto/cluster.proto @@ -0,0 +1,25 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +syntax = "proto3"; + +package infrastructureProto; + +option java_package = "sanbing.jcpp.proto.gen"; +option java_outer_classname = "ClusterProto"; + +message ServiceInfo { + string serviceId = 1; + repeated string serviceTypes = 2; + SystemInfoProto systemInfo = 10; +} + +message SystemInfoProto { + int64 cpuUsage = 1; + int64 cpuCount = 2; + int64 memoryUsage = 3; + int64 totalMemory = 4; + int64 diskUsage = 5; + int64 totalDiscSpace = 6; +} diff --git a/jcpp-infrastructure-proto/src/main/proto/protocol.proto b/jcpp-infrastructure-proto/src/main/proto/protocol.proto new file mode 100644 index 0000000..bd3ee09 --- /dev/null +++ b/jcpp-infrastructure-proto/src/main/proto/protocol.proto @@ -0,0 +1,242 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +syntax = "proto3"; + +package infrastructureProto; + +option java_package = "sanbing.jcpp.proto.gen"; +option java_outer_classname = "ProtocolProto"; + +message UplinkQueueMessage { + int64 messageIdMSB = 1; + int64 messageIdLSB = 2; + int64 sessionIdMSB = 3; + int64 sessionIdLSB = 4; + string messageKey = 5; + string protocolName = 6; + bytes requestData = 10; + LoginRequest loginRequest = 21; + HeartBeatRequest heartBeatRequest = 22; + VerifyPricingRequest verifyPricingRequest = 23; + QueryPricingRequest queryPricingRequest = 24; + GunRunStatusProto gunRunStatusProto = 25; + ChargingProgressProto chargingProgressProto = 26; + SetPricingResponse setPricingResponse = 27; + RemoteStartChargingResponse remoteStartChargingResponse = 28; + RemoteStopChargingResponse remoteStopChargingResponse = 29; + TransactionRecord transactionRecord = 30; +} + +message DownlinkRestMessage { + int64 messageIdMSB = 1; + int64 messageIdLSB = 2; + int64 sessionIdMSB = 3; + int64 sessionIdLSB = 4; + string protocolName = 6; + string pileCode = 7; + optional int64 requestIdMSB = 8; + optional int64 requestIdLSB = 9; + optional bytes requestData = 10; + string downlinkCmd = 11; + LoginResponse loginResponse = 20; + VerifyPricingResponse verifyPricingResponse = 21; + QueryPricingResponse queryPricingResponse = 22; + SetPricingRequest setPricingRequest = 23; + RemoteStartChargingRequest remoteStartChargingRequest = 24; + RemoteStopChargingRequest remoteStopChargingRequest = 25; + TransactionRecordAck transactionRecordAck = 26; +} + +message LoginRequest { + string pileCode = 3; + string remoteAddress = 4; + string nodeId = 10; + string nodeWebapiIpPort = 11; + optional string additionalInfo = 20; +} + +message LoginResponse { + bool success = 1; + string pileCode = 2; +} + +message HeartBeatRequest { + string pileCode = 3; + string remoteAddress = 4; + string nodeId = 10; + string nodeWebapiIpPort = 11; + optional string additionalInfo = 20; +} + +message VerifyPricingRequest { + string pileCode = 4; + int64 pricingId = 30; + optional string pricingModel = 31; + optional string additionalInfo = 20; +} + +message VerifyPricingResponse { + bool success = 1; + int64 pricingId = 30; +} + +message QueryPricingRequest { + string pileCode = 4; + optional string additionalInfo = 20; +} + +message QueryPricingResponse { + string pileCode = 4; + int64 pricingId = 30; + PricingModelProto pricingModel = 1; +} + +message PricingModelProto { + PricingModelType type = 3; + PricingModelRule rule = 4; + int32 standardElec = 5; + int32 standardServ = 6; + map flagPrice = 8; + repeated PeriodProto period = 9; +} + +message PeriodProto { + int32 sn = 1; + string begin = 2; + string end = 3; + PricingModelFlag flag = 4; +} + +message FlagPriceProto { + PricingModelFlag flag = 1; + int32 elec = 2; + int32 serv = 3; +} + +enum PricingModelType { + CHARGE = 0; // 充电费率模型 + DISCHARGE = 1; // 放电费率模型 +} + +enum PricingModelRule { + STANDARD = 0; + SPLIT_TIME = 1; +} + +enum PricingModelFlag { + TOP = 0; // 尖峰 + PEAK = 1; // 峰 + FLAT = 2; // 平 + VALLEY = 3; // 谷 + DEEP = 4; // 深谷 +} + +enum GunRunStatus { + IDLE = 0; // 空闲 + INSERTED = 1; // 已插枪 + CHARGING = 2; // 充电中 + CHARGE_COMPLETE = 3; // 充电完成 + DISCHARGE_READY = 4; // 放电准备 + DISCHARGING = 5; // 放电中 + DISCHARGE_COMPLETE = 6; // 放电完成 + RESERVED = 7; // 预约 + FAULT = 8; // 故障 + UNKNOWN = 9; // 未知 +} + +message GunRunStatusProto { + int64 ts = 1; + string pileCode = 4; + string gunCode = 5; + GunRunStatus GunRunStatus = 41; + repeated string faultMessages = 6; + optional string additionalInfo = 20; +} + +message ChargingProgressProto { + int64 ts = 1; + string pileCode = 4; + string gunCode = 5; + string tradeNo = 6; + float outputVoltage = 7; + float outputCurrent = 8; + float soc = 9; + int32 totalChargingDurationMin = 10; + float totalChargingEnergyKWh = 11; + int64 totalChargingCostCent = 12; + optional string additionalInfo = 20; +} + +message SetPricingRequest { + string pileCode = 4; + int64 pricingId = 30; + PricingModelProto pricingModel = 1; +} + +message SetPricingResponse { + bool success = 1; + string pileCode = 4; + int64 pricingId = 30; +} + +message RemoteStartChargingRequest { + string pileCode = 4; + string gunCode = 5; + string tradeNo = 6; + int32 limitCent = 7; + optional string additionalInfo = 20; +} + +message RemoteStartChargingResponse { + int64 ts = 1; + string pileCode = 4; + string gunCode = 5; + string tradeNo = 6; + bool success = 7; + string failReason = 8; + optional string additionalInfo = 20; +} + +message RemoteStopChargingRequest { + string pileCode = 4; + string gunCode = 5; +} + +message RemoteStopChargingResponse { + int64 ts = 1; + string pileCode = 4; + string gunCode = 5; + bool success = 7; + string failReason = 8; + optional string additionalInfo = 20; +} + +message TransactionRecord { + string pileCode = 4; + string gunCode = 5; + string tradeNo = 6; + int64 startTs = 51; + int64 endTs = 52; + float topEnergyKWh = 53; + int64 topAmountCent = 54; + float peakEnergyKWh = 55; + int64 peakAmountCent = 56; + float flatEnergyKWh = 57; + int64 flatAmountCent = 58; + float valleyEnergyKWh = 59; + int64 valleyAmountCent = 60; + float deepEnergyKWh = 61; + int64 deepAmountCent = 62; + float totalEnergyKWh = 63; + int64 totalAmountCent = 64; + int64 tradeTs = 65; + string stopReason = 66; + optional string additionalInfo = 20; +} + +message TransactionRecordAck { + string tradeNo = 6; + bool success = 7; +} \ No newline at end of file diff --git a/jcpp-infrastructure-queue/pom.xml b/jcpp-infrastructure-queue/pom.xml new file mode 100644 index 0000000..b068db7 --- /dev/null +++ b/jcpp-infrastructure-queue/pom.xml @@ -0,0 +1,52 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-infrastructure-queue + jar + JChargePointProtocol Infrastructure Queue Module + 基础MQ管理模块 + + + ${basedir}/.. + + + + + org.apache.kafka + kafka-clients + + + sanbing + jcpp-infrastructure-util + + + sanbing + jcpp-infrastructure-proto + + + sanbing + jcpp-infrastructure-stats + + + org.apache.curator + curator-recipes + + + + + diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/AbstractQueueConsumerTemplate.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/AbstractQueueConsumerTemplate.java new file mode 100644 index 0000000..88914e8 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/AbstractQueueConsumerTemplate.java @@ -0,0 +1,190 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +import jakarta.annotation.Nonnull; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; + +@Slf4j +public abstract class AbstractQueueConsumerTemplate implements QueueConsumer{ + + public static final long ONE_MILLISECOND_IN_NANOS = TimeUnit.MILLISECONDS.toNanos(1); + private volatile boolean subscribed; + protected volatile boolean stopped = false; + protected volatile Set partitions; + protected final ReentrantLock consumerLock = new ReentrantLock(); //NonfairSync + final Queue> subscribeQueue = new ConcurrentLinkedQueue<>(); + + @Getter + private final String topic; + + public AbstractQueueConsumerTemplate(String topic) { + this.topic = topic; + } + + @Override + public void subscribe() { + log.debug("enqueue topic subscribe {} ", topic); + if (stopped) { + log.error("trying subscribe, but consumer stopped for topic {}", topic); + return; + } + subscribeQueue.add(Collections.singleton(new TopicPartitionInfo(topic, null, true))); + } + + @Override + public void subscribe(Set partitions) { + log.debug("enqueue topics subscribe {} ", partitions); + if (stopped) { + log.error("trying subscribe, but consumer stopped for topic {}", topic); + return; + } + subscribeQueue.add(partitions); + } + + @Override + public List poll(long durationInMillis) { + List records; + long startNanos = System.nanoTime(); + if (stopped) { + log.error("poll invoked but consumer stopped for topic " + topic, new RuntimeException("stacktrace")); + return emptyList(); + } + if (!subscribed && partitions == null && subscribeQueue.isEmpty()) { + return sleepAndReturnEmpty(startNanos, durationInMillis); + } + + if (consumerLock.isLocked()) { + log.error("poll. consumerLock is locked. will wait with no timeout. it looks like a race conditions or deadlock topic " + topic, new RuntimeException("stacktrace")); + } + + consumerLock.lock(); + try { + while (!subscribeQueue.isEmpty()) { + subscribed = false; + partitions = subscribeQueue.poll(); + } + if (!subscribed) { + List topicNames = getFullTopicNames(); + log.info("Subscribing to topics {}", topicNames); + doSubscribe(topicNames); + subscribed = true; + } + records = partitions.isEmpty() ? emptyList() : doPoll(durationInMillis); + } finally { + consumerLock.unlock(); + } + + if (records.isEmpty() && !isLongPollingSupported()) { + return sleepAndReturnEmpty(startNanos, durationInMillis); + } + + return decodeRecords(records); + } + + @Nonnull + List decodeRecords(@Nonnull List records) { + List result = new ArrayList<>(records.size()); + records.forEach(record -> { + try { + if (record != null) { + result.add(decode(record)); + } + } catch (IOException e) { + log.error("Failed decode record: [{}]", record); + throw new RuntimeException("Failed to decode record: ", e); + } + }); + return result; + } + + List sleepAndReturnEmpty(final long startNanos, final long durationInMillis) { + long durationNanos = TimeUnit.MILLISECONDS.toNanos(durationInMillis); + long spentNanos = System.nanoTime() - startNanos; + long nanosLeft = durationNanos - spentNanos; + if (nanosLeft >= ONE_MILLISECOND_IN_NANOS) { + try { + long sleepMs = TimeUnit.NANOSECONDS.toMillis(nanosLeft); + log.trace("Going to sleep after poll: topic {} for {}ms", topic, sleepMs); + Thread.sleep(sleepMs); + } catch (InterruptedException e) { + if (!stopped) { + log.error("Failed to wait", e); + } + } + } + return emptyList(); + } + + @Override + public void commit() { + if (consumerLock.isLocked()) { + log.error("commit. consumerLock is locked. will wait with no timeout. it looks like a race conditions or deadlock topic " + topic, new RuntimeException("stacktrace")); + } + consumerLock.lock(); + try { + doCommit(); + } finally { + consumerLock.unlock(); + } + } + + @Override + public void stop() { + stopped = true; + } + + @Override + public void unsubscribe() { + log.info("Unsubscribing and stopping consumer for topics {}", getFullTopicNames()); + stopped = true; + consumerLock.lock(); + try { + if (subscribed) { + doUnsubscribe(); + } + } finally { + consumerLock.unlock(); + } + } + + @Override + public boolean isStopped() { + return stopped; + } + + abstract protected List doPoll(long durationInMillis); + + abstract protected T decode(R record) throws IOException; + + abstract protected void doSubscribe(List topicNames); + + abstract protected void doCommit(); + + abstract protected void doUnsubscribe(); + + @Override + public List getFullTopicNames() { + if (partitions == null) { + return Collections.emptyList(); + } + return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); + } + + protected boolean isLongPollingSupported() { + return false; + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/Callback.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/Callback.java new file mode 100644 index 0000000..9e97701 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/Callback.java @@ -0,0 +1,26 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +public interface Callback { + + Callback EMPTY = new Callback() { + + @Override + public void onSuccess() { + + } + + @Override + public void onFailure(Throwable t) { + + } + }; + + void onSuccess(); + + void onFailure(Throwable t); + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/DefaultQueueMsg.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/DefaultQueueMsg.java new file mode 100644 index 0000000..9e27287 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/DefaultQueueMsg.java @@ -0,0 +1,23 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +import lombok.Data; + +@Data +public class DefaultQueueMsg implements QueueMsg { + private final String key; + private final byte[] data; + private final DefaultQueueMsgHeaders headers; + + public DefaultQueueMsg(QueueMsg msg) { + this.key = msg.getKey(); + this.data = msg.getData(); + DefaultQueueMsgHeaders headers = new DefaultQueueMsgHeaders(); + msg.getHeaders().getData().forEach(headers::put); + this.headers = headers; + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/DefaultQueueMsgHeaders.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/DefaultQueueMsgHeaders.java new file mode 100644 index 0000000..d93ac8c --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/DefaultQueueMsgHeaders.java @@ -0,0 +1,29 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + + +import java.util.HashMap; +import java.util.Map; + +public class DefaultQueueMsgHeaders implements QueueMsgHeaders { + + protected final Map data = new HashMap<>(); + + @Override + public byte[] put(String key, byte[] value) { + return data.put(key, value); + } + + @Override + public byte[] get(String key) { + return data.get(key); + } + + @Override + public Map getData() { + return data; + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/KafkaQueueMsg.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/KafkaQueueMsg.java new file mode 100644 index 0000000..81c59ca --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/KafkaQueueMsg.java @@ -0,0 +1,38 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +import org.apache.kafka.clients.consumer.ConsumerRecord; + +public class KafkaQueueMsg implements QueueMsg { + private final String key; + private final QueueMsgHeaders headers; + private final byte[] data; + + public KafkaQueueMsg(ConsumerRecord record) { + this.key = record.key(); + QueueMsgHeaders headers = new DefaultQueueMsgHeaders(); + record.headers().forEach(header -> { + headers.put(header.key(), header.value()); + }); + this.headers = headers; + this.data = record.value(); + } + + @Override + public String getKey() { + return key; + } + + @Override + public QueueMsgHeaders getHeaders() { + return headers; + } + + @Override + public byte[] getData() { + return data; + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/PackCallback.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/PackCallback.java new file mode 100644 index 0000000..f5bb075 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/PackCallback.java @@ -0,0 +1,32 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +import lombok.extern.slf4j.Slf4j; + +import java.util.UUID; + +@Slf4j +public class PackCallback implements Callback { + private final PackProcessingContext ctx; + private final UUID id; + + public PackCallback(UUID id, PackProcessingContext ctx) { + this.id = id; + this.ctx = ctx; + } + + @Override + public void onSuccess() { + log.trace("[{}] ON SUCCESS", id); + ctx.onSuccess(id); + } + + @Override + public void onFailure(Throwable t) { + log.trace("[{}] ON FAILURE", id, t); + ctx.onFailure(id, t); + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/PackProcessingContext.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/PackProcessingContext.java new file mode 100644 index 0000000..6ea6e10 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/PackProcessingContext.java @@ -0,0 +1,75 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.UUID; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +public class PackProcessingContext { + + private final AtomicInteger pendingCount; + private final CountDownLatch processingTimeoutLatch; + @Getter + private final ConcurrentMap ackMap; + @Getter + private final ConcurrentMap failedMap; + + public PackProcessingContext(CountDownLatch processingTimeoutLatch, + ConcurrentMap ackMap, + ConcurrentMap failedMap) { + this.processingTimeoutLatch = processingTimeoutLatch; + this.pendingCount = new AtomicInteger(ackMap.size()); + this.ackMap = ackMap; + this.failedMap = failedMap; + } + + public boolean await(long packProcessingTimeout, TimeUnit milliseconds) throws InterruptedException { + return processingTimeoutLatch.await(packProcessingTimeout, milliseconds); + } + + public void onSuccess(UUID id) { + boolean empty = false; + T msg = ackMap.remove(id); + if (msg != null) { + empty = pendingCount.decrementAndGet() == 0; + } + if (empty) { + processingTimeoutLatch.countDown(); + } else { + if (log.isTraceEnabled()) { + log.trace("Items left: {}", ackMap.size()); + for (T t : ackMap.values()) { + log.trace("left item: {}", t); + } + } + } + } + + public void onFailure(UUID id, Throwable t) { + boolean empty = false; + T msg = ackMap.remove(id); + if (msg != null) { + empty = pendingCount.decrementAndGet() == 0; + failedMap.put(id, msg); + if (log.isTraceEnabled()) { + log.trace("Items left: {}", ackMap.size()); + for (T v : ackMap.values()) { + log.trace("left item: {}", v); + } + } + } + if (empty) { + processingTimeoutLatch.countDown(); + } + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/ProtoQueueMsg.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/ProtoQueueMsg.java new file mode 100644 index 0000000..3667dfc --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/ProtoQueueMsg.java @@ -0,0 +1,40 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +import lombok.Data; + +@Data +public class ProtoQueueMsg implements QueueMsg { + + private final String key; + protected final T value; + private final QueueMsgHeaders headers; + + public ProtoQueueMsg(String key, T value) { + this(key, value, new DefaultQueueMsgHeaders()); + } + + public ProtoQueueMsg(String key, T value, QueueMsgHeaders headers) { + this.key = key; + this.value = value; + this.headers = headers; + } + + @Override + public String getKey() { + return key; + } + + @Override + public QueueMsgHeaders getHeaders() { + return headers; + } + + @Override + public byte[] getData() { + return value.toByteArray(); + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueAdmin.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueAdmin.java new file mode 100644 index 0000000..1e3688a --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueAdmin.java @@ -0,0 +1,17 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +public interface QueueAdmin { + + default void createTopicIfNotExists(String topic) { + createTopicIfNotExists(topic, null); + } + + void createTopicIfNotExists(String topic, String properties); + + void destroy(); + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueCallback.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueCallback.java new file mode 100644 index 0000000..f2dcc91 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueCallback.java @@ -0,0 +1,12 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +public interface QueueCallback { + + void onSuccess(QueueMsgMetadata metadata); + + void onFailure(Throwable t); +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueConsumer.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueConsumer.java new file mode 100644 index 0000000..7b68b34 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueConsumer.java @@ -0,0 +1,34 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + + + +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; + +import java.util.List; +import java.util.Set; + +public interface QueueConsumer { + + String getTopic(); + + void subscribe(); + + void subscribe(Set partitions); + + void stop(); + + void unsubscribe(); + + List poll(long durationInMillis); + + void commit(); + + boolean isStopped(); + + List getFullTopicNames(); + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsg.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsg.java new file mode 100644 index 0000000..828d756 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsg.java @@ -0,0 +1,14 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +public interface QueueMsg { + + String getKey(); + + QueueMsgHeaders getHeaders(); + + byte[] getData(); +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsgHeaders.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsgHeaders.java new file mode 100644 index 0000000..d3fe798 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsgHeaders.java @@ -0,0 +1,16 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +import java.util.Map; + +public interface QueueMsgHeaders { + + byte[] put(String key, byte[] value); + + byte[] get(String key); + + Map getData(); +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsgMetadata.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsgMetadata.java new file mode 100644 index 0000000..b137435 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueMsgMetadata.java @@ -0,0 +1,8 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + +public interface QueueMsgMetadata { +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueProducer.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueProducer.java new file mode 100644 index 0000000..aae4422 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/QueueProducer.java @@ -0,0 +1,19 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue; + + +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; + +public interface QueueProducer { + + void init(); + + String getTopic(); + + void send(TopicPartitionInfo tpi, T msg, QueueCallback callback); + + void stop(); +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/QueueConfig.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/QueueConfig.java new file mode 100644 index 0000000..a3667e8 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/QueueConfig.java @@ -0,0 +1,13 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.common; + +public interface QueueConfig { + + boolean isConsumerPerPartition(); + + int getPollInterval(); + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/QueueConstants.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/QueueConstants.java new file mode 100644 index 0000000..c2ac400 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/QueueConstants.java @@ -0,0 +1,17 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.common; + +/** + * @author baigod + */ +public final class QueueConstants { + + public static final String MSG_MD_PREFIX = "jcpp_"; + + public static final String MSG_MD_TS = "ts"; + + public static final String MSG_MD_PROTOCOL_SESSION_ID = "protocol_session_id"; +} \ No newline at end of file diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/TopicPartitionInfo.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/TopicPartitionInfo.java new file mode 100644 index 0000000..c777928 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/common/TopicPartitionInfo.java @@ -0,0 +1,59 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.common; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +import java.util.Objects; +import java.util.Optional; + +@ToString +public class TopicPartitionInfo { + + @Getter + private final String topic; + private final Integer partition; + @Getter + private final String fullTopicName; + @Getter + private final boolean myPartition; + + @Builder + public TopicPartitionInfo(String topic, Integer partition, boolean myPartition) { + this.topic = topic; + this.partition = partition; + this.myPartition = myPartition; + String tmp = topic; + if (partition != null) { + tmp += "." + partition; + } + this.fullTopicName = tmp; + } + + public TopicPartitionInfo newByTopic(String topic) { + return new TopicPartitionInfo(topic, this.partition, this.myPartition); + } + + public Optional getPartition() { + return Optional.ofNullable(partition); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TopicPartitionInfo that = (TopicPartitionInfo) o; + return topic.equals(that.topic) && + Objects.equals(partition, that.partition) && + fullTopicName.equals(that.fullTopicName); + } + + @Override + public int hashCode() { + return Objects.hash(fullTopicName); + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DefaultServiceInfoProvider.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DefaultServiceInfoProvider.java new file mode 100644 index 0000000..055610f --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DefaultServiceInfoProvider.java @@ -0,0 +1,109 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery; + +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import sanbing.jcpp.infrastructure.util.SystemUtil; +import sanbing.jcpp.proto.gen.ClusterProto.ServiceInfo; +import sanbing.jcpp.proto.gen.ClusterProto.SystemInfoProto; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * @author baigod + */ +@Component +@Slf4j +public class DefaultServiceInfoProvider implements ServiceInfoProvider { + + @Value("${service.id:#{null}}") + @Getter + private String serviceId; + + @Getter + @Value("${service.type:monolith}") + private String serviceType; + + private List serviceTypes; + + private ServiceInfo serviceInfo; + + @Getter + private String serviceWebapiEndpoint; + + @Value("${server.port}") + private String webapiPort; + + @PostConstruct + public void init() throws UnknownHostException { + + + if (!StringUtils.hasText(this.serviceId)) { + try { + this.serviceId = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + this.serviceId = RandomStringUtils.randomAlphabetic(10); + } + } + log.info("Current Service ID: {}", this.serviceId); + + serviceWebapiEndpoint = InetAddress.getLocalHost().getHostAddress() + ":" + webapiPort; + log.info("Current Service HostAddress: {}", this.serviceWebapiEndpoint); + if (serviceType.equalsIgnoreCase("monolith")) { + serviceTypes = List.of(ServiceType.values()); + } else { + serviceTypes = Collections.singletonList(ServiceType.of(serviceType)); + } + + generateNewServiceInfoWithCurrentSystemInfo(); + } + + + @Override + public boolean isMonolith() { + return "monolith".equals(getServiceType()); + } + + @Override + public ServiceInfo getServiceInfo() { + return serviceInfo; + } + + @Override + public ServiceInfo generateNewServiceInfoWithCurrentSystemInfo() { + ServiceInfo.Builder builder = ServiceInfo.newBuilder() + .setServiceId(serviceId) + .addAllServiceTypes(serviceTypes.stream().map(ServiceType::name).collect(Collectors.toList())) + .setSystemInfo(getCurrentSystemInfoProto()); + return serviceInfo = builder.build(); + } + + private SystemInfoProto getCurrentSystemInfoProto() { + SystemInfoProto.Builder builder = SystemInfoProto.newBuilder(); + + SystemUtil.getCpuUsage().ifPresent(builder::setCpuUsage); + SystemUtil.getMemoryUsage().ifPresent(builder::setMemoryUsage); + SystemUtil.getDiscSpaceUsage().ifPresent(builder::setDiskUsage); + + SystemUtil.getCpuCount().ifPresent(builder::setCpuCount); + SystemUtil.getTotalMemory().ifPresent(builder::setTotalMemory); + SystemUtil.getTotalDiscSpace().ifPresent(builder::setTotalDiscSpace); + + return builder.build(); + } + +} + diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DiscoveryProvider.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DiscoveryProvider.java new file mode 100644 index 0000000..89b0fb0 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DiscoveryProvider.java @@ -0,0 +1,15 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery; + +import sanbing.jcpp.proto.gen.ClusterProto.ServiceInfo; + +import java.util.List; + +public interface DiscoveryProvider { + + List getOtherServers(); + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DummyDiscoveryProvider.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DummyDiscoveryProvider.java new file mode 100644 index 0000000..36d2cd8 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/DummyDiscoveryProvider.java @@ -0,0 +1,41 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.stereotype.Service; +import sanbing.jcpp.infrastructure.util.annotation.AfterStartUp; +import sanbing.jcpp.proto.gen.ClusterProto.ServiceInfo; + +import java.util.Collections; +import java.util.List; + +@Service +@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "false", matchIfMissing = true) +@Slf4j +public class DummyDiscoveryProvider implements DiscoveryProvider { + + private final ServiceInfoProvider serviceInfoProvider; + private final PartitionProvider partitionProvider; + + public DummyDiscoveryProvider(ServiceInfoProvider serviceInfoProvider, PartitionProvider partitionProvider) { + this.serviceInfoProvider = serviceInfoProvider; + this.partitionProvider = partitionProvider; + } + + + @AfterStartUp(order = AfterStartUp.DISCOVERY_SERVICE) + public void onApplicationEvent(ApplicationReadyEvent event) { + partitionProvider.recalculatePartitions(serviceInfoProvider.getServiceInfo(), Collections.emptyList()); + } + + @Override + public List getOtherServers() { + return Collections.emptyList(); + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/HashPartitionProvider.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/HashPartitionProvider.java new file mode 100644 index 0000000..87db05b --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/HashPartitionProvider.java @@ -0,0 +1,199 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery; + +import com.google.common.hash.HashFunction; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; +import sanbing.jcpp.infrastructure.queue.discovery.event.PartitionChangeEvent; +import sanbing.jcpp.proto.gen.ClusterProto.ServiceInfo; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import static sanbing.jcpp.infrastructure.util.JCPPHashUtil.forName; +import static sanbing.jcpp.infrastructure.util.JCPPHashUtil.hash; + +/** + * @author baigod + */ +@Component +@Slf4j +@ConfigurationProperties("queue.partitions") +public class HashPartitionProvider implements PartitionProvider { + + @Value("${queue.app.topic}") + private String appTopic; + @Value("${queue.app.partitions:10}") + private Integer appPartitions; + @Value("${queue.partitions.hash_function_name:murmur3_128}") + private String hashFunctionName; + + private final ConcurrentMap partitionTopicsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap partitionSizesMap = new ConcurrentHashMap<>(); + + private HashFunction hashFunction; + + protected volatile ConcurrentMap> myPartitions = new ConcurrentHashMap<>(); + + @Resource + private ApplicationEventPublisher applicationEventPublisher; + + @PostConstruct + public void init() { + this.hashFunction = forName(hashFunctionName); + + QueueKey appKey = new QueueKey(ServiceType.APP); + partitionTopicsMap.put(appKey, appTopic); + partitionSizesMap.put(appKey, appPartitions); + } + + private TopicPartitionInfo resolve(QueueKey queueKey, int hash) { + Integer partitionSize = partitionSizesMap.get(queueKey); + if (partitionSize == null) { + throw new IllegalStateException("Partitions info for queue " + queueKey + " is missing"); + } + + int partition = Math.abs(hash % partitionSize); + + return buildTopicPartitionInfo(queueKey, partition); + } + + @Override + public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, UUID entityId) { + QueueKey queueKey = getQueueKey(serviceType, queueName); + return resolve(queueKey, hash(hashFunction, entityId)); + } + + @Override + public TopicPartitionInfo resolve(ServiceType serviceType, String queueName, String pileCode) { + QueueKey queueKey = getQueueKey(serviceType, queueName); + return resolve(queueKey, hash(hashFunction, pileCode)); + } + + private QueueKey getQueueKey(ServiceType serviceType, String queueName) { + QueueKey queueKey = new QueueKey(serviceType, queueName); + if (!partitionSizesMap.containsKey(queueKey)) { + queueKey = new QueueKey(serviceType); + } + return queueKey; + } + + @Override + public synchronized void recalculatePartitions(ServiceInfo currentService, List otherServices) { + log.info("Recalculating partitions"); + logServiceInfo(currentService); + otherServices.forEach(this::logServiceInfo); + + Map> queueServicesMap = new HashMap<>(); + addNode(currentService, queueServicesMap); + for (ServiceInfo other : otherServices) { + addNode(other, queueServicesMap); + } + queueServicesMap.values().forEach(list -> list.sort(Comparator.comparing(ServiceInfo::getServiceId))); + + final ConcurrentMap> newPartitions = new ConcurrentHashMap<>(); + partitionSizesMap.forEach((queueKey, size) -> { + for (int i = 0; i < size; i++) { + try { + List servers = queueServicesMap.get(queueKey); + ServiceInfo serviceInfo = servers == null || servers.isEmpty() ? null : servers.get(i % servers.size()); + log.info("Server responsible for {}[{}] - {}", queueKey, i, serviceInfo != null ? serviceInfo.getServiceId() : "none"); + if (currentService.equals(serviceInfo)) { + newPartitions.computeIfAbsent(queueKey, key -> new ArrayList<>()).add(i); + } + } catch (Exception e) { + log.warn("Failed to resolve server responsible for {}[{}]", queueKey, i, e); + } + } + }); + + final ConcurrentMap> oldPartitions = myPartitions; + myPartitions = newPartitions; + + log.info("Current Server responsible partitions: {}", myPartitions); + + Map> changedPartitionsMap = new HashMap<>(); + + Set removed = new HashSet<>(); + oldPartitions.forEach((queueKey, partitions) -> { + if (!newPartitions.containsKey(queueKey)) { + removed.add(queueKey); + } + }); + + removed.forEach(queueKey -> { + changedPartitionsMap.put(queueKey, Collections.emptySet()); + }); + + myPartitions.forEach((queueKey, partitions) -> { + if (!partitions.equals(oldPartitions.get(queueKey))) { + Set tpiList = partitions.stream() + .map(partition -> buildTopicPartitionInfo(queueKey, partition)) + .collect(Collectors.toSet()); + changedPartitionsMap.put(queueKey, tpiList); + } + }); + + if (!changedPartitionsMap.isEmpty()) { + Map>> partitionsByServiceType = new HashMap<>(); + changedPartitionsMap.forEach((queueKey, partitions) -> { + partitionsByServiceType.computeIfAbsent(queueKey.getType(), serviceType -> new HashMap<>()) + .put(queueKey, partitions); + }); + partitionsByServiceType.forEach(this::publishPartitionChangeEvent); + } + + } + + private void publishPartitionChangeEvent(ServiceType serviceType, Map> partitionsMap) { + log.info("Partitions changed: {}", System.lineSeparator() + partitionsMap.entrySet().stream() + .map(entry -> "[" + entry.getKey() + "] - [" + entry.getValue().stream() + .map(tpi -> tpi.getPartition().orElse(-1).toString()).sorted() + .collect(Collectors.joining(", ")) + "]") + .collect(Collectors.joining(System.lineSeparator()))); + PartitionChangeEvent event = new PartitionChangeEvent(this, serviceType, partitionsMap); + try { + applicationEventPublisher.publishEvent(event); + } catch (Exception e) { + log.error("Failed to publish partition change event {}", event, e); + } + } + + private void logServiceInfo(ServiceInfo server) { + log.info("Found server: {}", server.getServiceId()); + } + + private void addNode(ServiceInfo instance, Map> queueServiceList) { + for (String serviceTypeStr : instance.getServiceTypesList()) { + ServiceType serviceType = ServiceType.of(serviceTypeStr); + if (ServiceType.APP.equals(serviceType)) { + queueServiceList.computeIfAbsent(new QueueKey(serviceType), key -> new ArrayList<>()).add(instance); + } + } + } + + private TopicPartitionInfo buildTopicPartitionInfo(QueueKey queueKey, int partition) { + List partitions = myPartitions.get(queueKey); + return buildTopicPartitionInfo(queueKey, partition, partitions != null && partitions.contains(partition)); + } + + private TopicPartitionInfo buildTopicPartitionInfo(QueueKey queueKey, int partition, boolean myPartition) { + return TopicPartitionInfo.builder() + .topic(partitionTopicsMap.get(queueKey)) + .partition(partition) + .myPartition(myPartition) + .build(); + } + +} \ No newline at end of file diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/PartitionProvider.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/PartitionProvider.java new file mode 100644 index 0000000..908707d --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/PartitionProvider.java @@ -0,0 +1,22 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery; + + +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; +import sanbing.jcpp.proto.gen.ClusterProto.ServiceInfo; + +import java.util.List; +import java.util.UUID; + +public interface PartitionProvider { + + TopicPartitionInfo resolve(ServiceType serviceType,String queueName, UUID entityId); + + TopicPartitionInfo resolve(ServiceType serviceType,String queueName, String pileCode); + + void recalculatePartitions(ServiceInfo currentService, List otherServices); + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/QueueKey.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/QueueKey.java new file mode 100644 index 0000000..d847479 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/QueueKey.java @@ -0,0 +1,33 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery; + +import lombok.Data; +import lombok.With; + +@Data +public class QueueKey { + public static final String MAIN_QUEUE_NAME = "Main"; + + private final ServiceType type; + @With + private final String queueName; + + public QueueKey(ServiceType type, String queueName) { + this.type = type; + this.queueName = queueName; + } + + public QueueKey(ServiceType type) { + this.type = type; + this.queueName = MAIN_QUEUE_NAME; + } + + @Override + public String toString() { + return "QK(" + queueName + "," + type + ")"; + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ServiceInfoProvider.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ServiceInfoProvider.java new file mode 100644 index 0000000..294e2a6 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ServiceInfoProvider.java @@ -0,0 +1,26 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery; + + +import sanbing.jcpp.proto.gen.ClusterProto; + +/** + * @author baigod + */ +public interface ServiceInfoProvider { + String getServiceId(); + + String getServiceWebapiEndpoint(); + + String getServiceType(); + + boolean isMonolith(); + + ClusterProto.ServiceInfo getServiceInfo(); + + ClusterProto.ServiceInfo generateNewServiceInfoWithCurrentSystemInfo(); + +} \ No newline at end of file diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ServiceType.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ServiceType.java new file mode 100644 index 0000000..b4f6050 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ServiceType.java @@ -0,0 +1,23 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum ServiceType { + + APP("app"), + PROTOCOL("protocol"); + + private final String label; + + public static ServiceType of(String serviceType) { + return ServiceType.valueOf(serviceType.toUpperCase()); + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ZkDiscoveryProvider.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ZkDiscoveryProvider.java new file mode 100644 index 0000000..4ec563d --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/ZkDiscoveryProvider.java @@ -0,0 +1,328 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery; + +import com.google.protobuf.InvalidProtocolBufferException; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.framework.imps.CuratorFrameworkState; +import org.apache.curator.framework.recipes.cache.ChildData; +import org.apache.curator.framework.recipes.cache.CuratorCache; +import org.apache.curator.framework.recipes.cache.CuratorCacheListener; +import org.apache.curator.framework.state.ConnectionState; +import org.apache.curator.framework.state.ConnectionStateListener; +import org.apache.curator.retry.RetryForever; +import org.apache.curator.utils.CloseableUtils; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.KeeperException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import sanbing.jcpp.infrastructure.queue.discovery.event.OtherServiceShutdownEvent; +import sanbing.jcpp.infrastructure.util.annotation.AfterStartUp; +import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory; +import sanbing.jcpp.proto.gen.ClusterProto.ServiceInfo; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +@Service +@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "true") +@Slf4j +public class ZkDiscoveryProvider implements DiscoveryProvider, CuratorCacheListener { + + @Value("${zk.url}") + private String zkUrl; + @Value("${zk.retry-interval-ms}") + private Integer zkRetryInterval; + @Value("${zk.connection-timeout-ms}") + private Integer zkConnectionTimeout; + @Value("${zk.session-timeout-ms}") + private Integer zkSessionTimeout; + @Value("${zk.zk-dir}") + private String zkDir; + @Value("${zk.recalculate-delay:0}") + private Long recalculateDelay; + + protected final ConcurrentHashMap> delayedTasks; + + private final ApplicationEventPublisher applicationEventPublisher; + private final ServiceInfoProvider serviceInfoProvider; + private final PartitionProvider partitionProvider; + + private ScheduledExecutorService zkExecutorService; + private CuratorFramework client; + private CuratorCache cache; + private String nodePath; + private String zkNodesDir; + + private volatile boolean stopped = true; + + public ZkDiscoveryProvider(ApplicationEventPublisher applicationEventPublisher, + ServiceInfoProvider serviceInfoProvider, + PartitionProvider partitionProvider) { + this.applicationEventPublisher = applicationEventPublisher; + this.serviceInfoProvider = serviceInfoProvider; + this.partitionProvider = partitionProvider; + delayedTasks = new ConcurrentHashMap<>(); + } + + @PostConstruct + public void init() { + log.info("Discovery Provider Initializing..."); + Assert.hasLength(zkUrl, missingProperty("zk.url")); + Assert.notNull(zkRetryInterval, missingProperty("zk.retry-interval-ms")); + Assert.notNull(zkConnectionTimeout, missingProperty("zk.connection-timeout-ms")); + Assert.notNull(zkSessionTimeout, missingProperty("zk-session-timeout-ms")); + + zkExecutorService = Executors.newSingleThreadScheduledExecutor(JCPPThreadFactory.forName("zk-discovery")); + + zkNodesDir = zkDir + "/nodes"; + initZkClient(); + + log.info("Initialization completed, using ZK connect string: {}", zkUrl); + } + + @Override + public List getOtherServers() { + return cache.stream() + .filter(cd -> !cd.getPath().equals(nodePath) && !cd.getPath().equals(zkNodesDir)) + .map(cd -> { + try { + return ServiceInfo.parseFrom(cd.getData()); + } catch (NoSuchElementException | InvalidProtocolBufferException e) { + log.error("Failed to decode ZK node", e); + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + } + + @AfterStartUp(order = AfterStartUp.DISCOVERY_SERVICE) + public void onApplicationEvent(ApplicationReadyEvent event) { + if (stopped) { + log.debug("Ignoring application ready event. Service is stopped."); + return; + } else { + log.info("Received application ready event. Starting current ZK node."); + } + if (client.getState() != CuratorFrameworkState.STARTED) { + log.debug("Ignoring application ready event, ZK client is not started, ZK client state [{}]", client.getState()); + return; + } + log.info("Going to publish current server..."); + publishCurrentServer(); + log.info("Going to recalculate partitions..."); + recalculatePartitions(); + + zkExecutorService.scheduleAtFixedRate(this::publishCurrentServer, 1, 1, TimeUnit.MINUTES); + } + + @SneakyThrows + public synchronized void publishCurrentServer() { + ServiceInfo self = serviceInfoProvider.getServiceInfo(); + if (currentServerExists()) { + log.trace("[{}] Updating ZK node for current instance: {}", self.getServiceId(), nodePath); + client.setData().forPath(nodePath, serviceInfoProvider.generateNewServiceInfoWithCurrentSystemInfo().toByteArray()); + } else { + try { + log.info("[{}] Creating ZK node for current instance", self.getServiceId()); + nodePath = client.create() + .creatingParentsIfNeeded() + .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) + .forPath(zkNodesDir + "/node-", self.toByteArray()); + log.info("[{}] Created ZK node for current instance: {}", self.getServiceId(), nodePath); + client.getConnectionStateListenable().addListener(checkReconnect(self)); + } catch (Exception e) { + log.error("Failed to create ZK node", e); + throw new RuntimeException(e); + } + } + } + + private boolean currentServerExists() { + if (nodePath == null) { + return false; + } + try { + ServiceInfo self = serviceInfoProvider.getServiceInfo(); + ServiceInfo registeredServerInfo = ServiceInfo.parseFrom(client.getData().forPath(nodePath)); + if (self.equals(registeredServerInfo)) { + return true; + } + } catch (KeeperException.NoNodeException e) { + log.info("ZK node does not exist: {}", nodePath); + } catch (Exception e) { + log.error("Couldn't check if ZK node exists", e); + } + return false; + } + + private ConnectionStateListener checkReconnect(ServiceInfo self) { + return (client, newState) -> { + log.info("[{}] ZK state changed: {}", self.getServiceId(), newState); + if (newState == ConnectionState.LOST) { + zkExecutorService.submit(this::reconnect); + } + }; + } + + private volatile boolean reconnectInProgress = false; + + private synchronized void reconnect() { + if (!reconnectInProgress) { + reconnectInProgress = true; + try { + destroyZkClient(); + initZkClient(); + publishCurrentServer(); + } catch (Exception e) { + log.error("Failed to reconnect to ZK: {}", e.getMessage(), e); + } finally { + reconnectInProgress = false; + } + } + } + + private void initZkClient() { + try { + client = CuratorFrameworkFactory.newClient(zkUrl, zkSessionTimeout, zkConnectionTimeout, new RetryForever(zkRetryInterval)); + client.start(); + client.blockUntilConnected(); + cache = CuratorCache.builder(client, zkNodesDir).build(); + cache.listenable().addListener(this); + cache.start(); + stopped = false; + log.info("ZK client connected"); + } catch (Exception e) { + log.error("Failed to connect to ZK: {}", e.getMessage(), e); + CloseableUtils.closeQuietly(cache); + CloseableUtils.closeQuietly(client); + throw new RuntimeException(e); + } + } + + private void unpublishCurrentServer() { + try { + if (nodePath != null) { + client.delete().forPath(nodePath); + } + } catch (Exception e) { + log.error("Failed to delete ZK node {}", nodePath, e); + throw new RuntimeException(e); + } + } + + private void destroyZkClient() { + stopped = true; + try { + unpublishCurrentServer(); + } catch (Exception ignored) { + } + CloseableUtils.closeQuietly(cache); + CloseableUtils.closeQuietly(client); + log.info("ZK client disconnected"); + } + + @PreDestroy + public void destroy() { + destroyZkClient(); + zkExecutorService.shutdownNow(); + log.info("Stopped zk discovery service"); + } + + public static String missingProperty(String propertyName) { + return "The " + propertyName + " property need to be set!"; + } + + @Override + public void event(Type type, ChildData oldData, ChildData data) { + if (stopped) { + log.info("Ignoring {}. Service is stopped.", type); + return; + } + if (client.getState() != CuratorFrameworkState.STARTED) { + log.info("Ignoring {}, ZK client is not started, ZK client state [{}]", type, client.getState()); + return; + } + + switch (type) { + case NODE_CREATED -> { + if (data == null || data.getData() == null) { + log.info("Ignoring {} due to empty created data", type); + return; + } + String serviceId = getServiceId(type, data); + + ScheduledFuture task = delayedTasks.remove(serviceId); + if (task != null) { + if (task.cancel(false)) { + log.info("[{}] Recalculate partitions ignored. Service was restarted in time.", serviceId); + } else { + log.info("[{}] Going to recalculate partitions. Service was not restarted in time!", serviceId); + recalculatePartitions(); + } + } else { + log.info("[{}] Going to recalculate partitions due to adding new node.", + serviceId); + recalculatePartitions(); + } + } + case NODE_DELETED -> { + if (oldData == null || oldData.getData() == null) { + log.info("Ignoring {} due to empty delete data", type); + return; + } else if (nodePath != null && nodePath.equals(oldData.getPath())) { + log.info("ZK node for current instance is somehow deleted."); + publishCurrentServer(); + return; + } + String serviceId = getServiceId(type, oldData); + + zkExecutorService.submit(() -> applicationEventPublisher.publishEvent(new OtherServiceShutdownEvent(this, serviceId))); + ScheduledFuture future = zkExecutorService.schedule(() -> { + log.info("[{}] Going to recalculate partitions due to removed node", serviceId); + ScheduledFuture removedTask = delayedTasks.remove(serviceId); + if (removedTask != null) { + recalculatePartitions(); + } + }, recalculateDelay, TimeUnit.MILLISECONDS); + delayedTasks.put(serviceId, future); + } + default -> { + } + } + } + + private static String getServiceId(Type type, ChildData data) { + ServiceInfo instance; + try { + instance = ServiceInfo.parseFrom(data.getData()); + } catch (InvalidProtocolBufferException e) { + log.error("Failed to decode server instance for node {}", data.getPath(), e); + throw new RuntimeException(e); + } + + String serviceId = instance.getServiceId(); + + log.info("Processing [{}] event for [{}]", type, serviceId); + return serviceId; + } + + synchronized void recalculatePartitions() { + delayedTasks.values().forEach(future -> future.cancel(false)); + delayedTasks.clear(); + partitionProvider.recalculatePartitions(serviceInfoProvider.getServiceInfo(), getOtherServers()); + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/JCPPApplicationEvent.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/JCPPApplicationEvent.java new file mode 100644 index 0000000..1bcee8a --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/JCPPApplicationEvent.java @@ -0,0 +1,26 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery.event; + +import lombok.Getter; +import lombok.ToString; +import org.springframework.context.ApplicationEvent; + +import java.util.concurrent.atomic.AtomicInteger; + +@ToString +public class JCPPApplicationEvent extends ApplicationEvent { + + private static final AtomicInteger sequence = new AtomicInteger(); + + @Getter + private final int sequenceNumber; + + public JCPPApplicationEvent(Object source) { + super(source); + sequenceNumber = sequence.incrementAndGet(); + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/JCPPApplicationEventListener.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/JCPPApplicationEventListener.java new file mode 100644 index 0000000..8541521 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/JCPPApplicationEventListener.java @@ -0,0 +1,54 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery.event; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public abstract class JCPPApplicationEventListener implements ApplicationListener { + + private int lastProcessedSequenceNumber = Integer.MIN_VALUE; + private final Lock seqNumberLock = new ReentrantLock(); + + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Override + public void onApplicationEvent(T event) { + if (!filterApplicationEvent(event)) { + log.trace("Skipping event due to filter: {}", event); + return; + } + boolean validUpdate = false; + seqNumberLock.lock(); + try { + if (event.getSequenceNumber() > lastProcessedSequenceNumber) { + validUpdate = true; + lastProcessedSequenceNumber = event.getSequenceNumber(); + } + } finally { + seqNumberLock.unlock(); + } + if (validUpdate) { + try { + onJCPPApplicationEvent(event); + } catch (Exception e) { + log.error("Failed to handle partition change event: {}", event, e); + } + } else { + log.info("Application event ignored due to invalid sequence number ({} > {}). Event: {}", lastProcessedSequenceNumber, event.getSequenceNumber(), event); + } + } + + protected abstract void onJCPPApplicationEvent(T event); + + protected boolean filterApplicationEvent(T event) { + return true; + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/OtherServiceShutdownEvent.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/OtherServiceShutdownEvent.java new file mode 100644 index 0000000..b334e3c --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/OtherServiceShutdownEvent.java @@ -0,0 +1,18 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery.event; + +import lombok.Getter; + +public class OtherServiceShutdownEvent extends JCPPApplicationEvent { + + @Getter + private final String serviceId; + + public OtherServiceShutdownEvent(Object source, String serviceId) { + super(source); + this.serviceId = serviceId; + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/PartitionChangeEvent.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/PartitionChangeEvent.java new file mode 100644 index 0000000..7a4b37a --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/discovery/event/PartitionChangeEvent.java @@ -0,0 +1,45 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.discovery.event; + +import lombok.Getter; +import lombok.ToString; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; +import sanbing.jcpp.infrastructure.queue.discovery.QueueKey; +import sanbing.jcpp.infrastructure.queue.discovery.ServiceType; + +import java.io.Serial; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static sanbing.jcpp.infrastructure.queue.discovery.QueueKey.MAIN_QUEUE_NAME; + +@ToString(callSuper = true) +public class PartitionChangeEvent extends JCPPApplicationEvent { + + @Serial + private static final long serialVersionUID = -8731788167026510559L; + + @Getter + private final Map> partitionsMap; + + public PartitionChangeEvent(Object source, ServiceType serviceType, Map> partitionsMap) { + super(source); + this.partitionsMap = partitionsMap; + } + + public Set getAppPartitions() { + return getPartitionsByServiceTypeAndQueueName(ServiceType.APP, MAIN_QUEUE_NAME); + } + + private Set getPartitionsByServiceTypeAndQueueName(ServiceType serviceType, String queueName) { + return partitionsMap.entrySet() + .stream() + .filter(entry -> serviceType.equals(entry.getKey().getType()) && queueName.equals(entry.getKey().getQueueName())) + .flatMap(entry -> entry.getValue().stream()) + .collect(Collectors.toSet()); + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaAdmin.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaAdmin.java new file mode 100644 index 0000000..385634b --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaAdmin.java @@ -0,0 +1,94 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.kafka; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.CreateTopicsResult; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.common.errors.TopicExistsException; +import sanbing.jcpp.infrastructure.queue.QueueAdmin; +import sanbing.jcpp.infrastructure.util.property.PropertyUtils; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; + +@Slf4j +public class KafkaAdmin implements QueueAdmin { + + private final KafkaSettings settings; + private final Map topicConfigs; + private final int numPartitions; + private volatile Set topics; + + private final short replicationFactor; + + public KafkaAdmin(KafkaSettings settings, Map topicConfigs) { + this.settings = settings; + this.topicConfigs = topicConfigs; + + String numPartitionsStr = topicConfigs.get(KafkaTopicConfigs.NUM_PARTITIONS_SETTING); + if (numPartitionsStr != null) { + numPartitions = Integer.parseInt(numPartitionsStr); + topicConfigs.remove("partitions"); + } else { + numPartitions = 1; + } + replicationFactor = settings.getReplicationFactor(); + } + + @Override + public void createTopicIfNotExists(String topic, String properties) { + Set topics = getTopics(); + if (topics.contains(topic)) { + return; + } + try { + NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(PropertyUtils.getProps(topicConfigs, properties)); + createTopic(newTopic).values().get(topic).get(); + topics.add(topic); + } catch (ExecutionException ee) { + switch (ee.getCause()) { + case TopicExistsException ignored -> { + //do nothing + } + case null, default -> { + log.warn("[{}] Failed to create topic", topic, ee); + throw new RuntimeException(ee); + } + } + } catch (Exception e) { + log.warn("[{}] Failed to create topic", topic, e); + throw new RuntimeException(e); + } + } + + private Set getTopics() { + if (topics == null) { + synchronized (this) { + if (topics == null) { + topics = ConcurrentHashMap.newKeySet(); + try { + topics.addAll(settings.getAdminClient().listTopics().names().get()); + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to get all topics.", e); + } + } + } + } + return topics; + } + + public CreateTopicsResult createTopic(NewTopic topic) { + return settings.getAdminClient().createTopics(Collections.singletonList(topic)); + } + + @Override + public void destroy() { + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerStatisticConfig.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerStatisticConfig.java new file mode 100644 index 0000000..8b6aebc --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerStatisticConfig.java @@ -0,0 +1,29 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.kafka; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class KafkaConsumerStatisticConfig { + + @Value("${queue.kafka.consumer-stats.enabled:true}") + private Boolean enabled; + + @Value("${queue.kafka.consumer-stats.print-interval-ms:60000}") + private Long printIntervalMs; + + @Value("${queue.kafka.consumer-stats.kafka-response-timeout-ms:1000}") + private Long kafkaResponseTimeoutMs; +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerStatsService.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerStatsService.java new file mode 100644 index 0000000..a145ed9 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerStatsService.java @@ -0,0 +1,158 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.kafka; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +public class KafkaConsumerStatsService { + + private final Set monitoredGroups = ConcurrentHashMap.newKeySet(); + + private final KafkaSettings kafkaSettings; + private final KafkaConsumerStatisticConfig statsConfig; + + private Consumer consumer; + private ScheduledExecutorService statsPrintScheduler; + + @PostConstruct + public void init() { + if (!statsConfig.getEnabled()) { + return; + } + this.statsPrintScheduler = Executors.newSingleThreadScheduledExecutor(JCPPThreadFactory.forName("kafka-consumer-stats", Thread.MAX_PRIORITY)); + + Properties consumerProps = kafkaSettings.toConsumerProps(null); + consumerProps.put(ConsumerConfig.CLIENT_ID_CONFIG, "consumer-stats-loader-client"); + consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer-stats-loader-client-group"); + this.consumer = new KafkaConsumer<>(consumerProps); + + startLogScheduling(); + } + + private void startLogScheduling() { + Duration timeoutDuration = Duration.ofMillis(statsConfig.getKafkaResponseTimeoutMs()); + statsPrintScheduler.scheduleWithFixedDelay(() -> { + if (!isStatsPrintRequired()) { + return; + } + for (String groupId : monitoredGroups) { + try { + Map groupOffsets = kafkaSettings.getAdminClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata() + .get(statsConfig.getKafkaResponseTimeoutMs(), TimeUnit.MILLISECONDS); + Map endOffsets = consumer.endOffsets(groupOffsets.keySet(), timeoutDuration); + + List lagTopicsStats = getTopicsStatsWithLag(groupOffsets, endOffsets); + if (!lagTopicsStats.isEmpty()) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < lagTopicsStats.size(); i++) { + builder.append(lagTopicsStats.get(i).toString()); + if (i != lagTopicsStats.size() - 1) { + builder.append(", "); + } + } + log.info("[{}] Topic partitions with lag: [{}].", groupId, builder); + } + } catch (Exception e) { + log.warn("[{}] Failed to get consumer group stats. Reason - {}.", groupId, e.getMessage()); + log.trace("Detailed error: ", e); + } + } + + }, statsConfig.getPrintIntervalMs(), statsConfig.getPrintIntervalMs(), TimeUnit.MILLISECONDS); + } + + private boolean isStatsPrintRequired() { + return log.isInfoEnabled() ; + } + + private List getTopicsStatsWithLag(Map groupOffsets, Map endOffsets) { + List consumerGroupStats = new ArrayList<>(); + for (TopicPartition topicPartition : groupOffsets.keySet()) { + long endOffset = endOffsets.get(topicPartition); + long committedOffset = groupOffsets.get(topicPartition).offset(); + long lag = endOffset - committedOffset; + if (lag != 0) { + GroupTopicStats groupTopicStats = GroupTopicStats.builder() + .topic(topicPartition.topic()) + .partition(topicPartition.partition()) + .committedOffset(committedOffset) + .endOffset(endOffset) + .lag(lag) + .build(); + consumerGroupStats.add(groupTopicStats); + } + } + return consumerGroupStats; + } + + public void registerClientGroup(String groupId) { + if (statsConfig.getEnabled() && !StringUtils.isEmpty(groupId)) { + monitoredGroups.add(groupId); + } + } + + public void unregisterClientGroup(String groupId) { + if (statsConfig.getEnabled() && !StringUtils.isEmpty(groupId)) { + monitoredGroups.remove(groupId); + } + } + + @PreDestroy + public void destroy() { + if (statsPrintScheduler != null) { + statsPrintScheduler.shutdownNow(); + } + if (consumer != null) { + consumer.close(); + } + } + + + @Builder + @Data + private static class GroupTopicStats { + private String topic; + private int partition; + private long committedOffset; + private long endOffset; + private long lag; + + @Override + public String toString() { + return "[" + + "topic=[" + topic + ']' + + ", partition=[" + partition + "]" + + ", committedOffset=[" + committedOffset + "]" + + ", endOffset=[" + endOffset + "]" + + ", lag=[" + lag + "]" + + "]"; + } + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerTemplate.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerTemplate.java new file mode 100644 index 0000000..22818d1 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaConsumerTemplate.java @@ -0,0 +1,117 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.kafka; + +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.springframework.util.StopWatch; +import sanbing.jcpp.infrastructure.queue.AbstractQueueConsumerTemplate; +import sanbing.jcpp.infrastructure.queue.KafkaQueueMsg; +import sanbing.jcpp.infrastructure.queue.QueueAdmin; +import sanbing.jcpp.infrastructure.queue.QueueMsg; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +@Slf4j +public class KafkaConsumerTemplate extends AbstractQueueConsumerTemplate, T> { + + private final QueueAdmin admin; + private final KafkaConsumer consumer; + private final KafkaDecoder decoder; + + private final KafkaConsumerStatsService statsService; + private final String groupId; + + @Builder + private KafkaConsumerTemplate(KafkaSettings settings, KafkaDecoder decoder, + String clientId, String groupId, String topic, + QueueAdmin admin, KafkaConsumerStatsService statsService) { + super(topic); + Properties props = settings.toConsumerProps(topic); + props.put(ConsumerConfig.CLIENT_ID_CONFIG, clientId); + if (groupId != null) { + props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); + } + + this.statsService = statsService; + this.groupId = groupId; + + if (statsService != null) { + statsService.registerClientGroup(groupId); + } + + this.admin = admin; + this.consumer = new KafkaConsumer<>(props); + this.decoder = decoder; + } + + @Override + protected void doSubscribe(List topicNames) { + if (!topicNames.isEmpty()) { + topicNames.forEach(admin::createTopicIfNotExists); + consumer.subscribe(topicNames); + } else { + log.info("unsubscribe due to empty topic list"); + consumer.unsubscribe(); + } + } + + @Override + protected List> doPoll(long durationInMillis) { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + + log.trace("poll topic {} maxDuration {}", getTopic(), durationInMillis); + + ConsumerRecords records = consumer.poll(Duration.ofMillis(durationInMillis)); + + stopWatch.stop(); + log.trace("poll topic {} took {}ms", getTopic(), stopWatch.getTotalTimeMillis()); + + if (records.isEmpty()) { + return Collections.emptyList(); + } else { + List> recordList = new ArrayList<>(256); + records.forEach(recordList::add); + return recordList; + } + } + + @Override + public T decode(ConsumerRecord record) throws IOException { + return decoder.decode(new KafkaQueueMsg(record)); + } + + @Override + protected void doCommit() { + consumer.commitSync(); + } + + @Override + protected void doUnsubscribe() { + if (consumer != null) { + consumer.unsubscribe(); + consumer.close(); + } + if (statsService != null) { + statsService.unregisterClientGroup(groupId); + } + } + + @Override + public boolean isLongPollingSupported() { + return true; + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaDecoder.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaDecoder.java new file mode 100644 index 0000000..e4ad32a --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaDecoder.java @@ -0,0 +1,16 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.kafka; + + +import sanbing.jcpp.infrastructure.queue.QueueMsg; + +import java.io.IOException; + +public interface KafkaDecoder { + + T decode(QueueMsg msg) throws IOException; + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaProducerTemplate.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaProducerTemplate.java new file mode 100644 index 0000000..60fc4d0 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaProducerTemplate.java @@ -0,0 +1,133 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.kafka; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.header.Header; +import org.apache.kafka.common.header.internals.RecordHeader; +import sanbing.jcpp.infrastructure.queue.QueueAdmin; +import sanbing.jcpp.infrastructure.queue.QueueCallback; +import sanbing.jcpp.infrastructure.queue.QueueMsg; +import sanbing.jcpp.infrastructure.queue.QueueProducer; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@Slf4j +public class KafkaProducerTemplate implements QueueProducer { + + private final KafkaProducer producer; + + @Getter + private final String topic; + + @Getter + private final KafkaSettings settings; + + private final QueueAdmin admin; + + private final Set topics; + + @Getter + private final String clientId; + + @Builder + private KafkaProducerTemplate(KafkaSettings settings, String topic, String clientId, QueueAdmin admin) { + Properties props = settings.toProducerProps(topic); + + this.clientId = Objects.requireNonNull(clientId, "Kafka producer client.id is null"); + if (!StringUtils.isEmpty(clientId)) { + props.put(ProducerConfig.CLIENT_ID_CONFIG, clientId); + } + this.settings = settings; + + this.producer = new KafkaProducer<>(props); + this.topic = topic; + this.admin = admin; + topics = ConcurrentHashMap.newKeySet(); + } + + @Override + public void init() { + } + + void addAnalyticHeaders(List
headers) { + headers.add(new RecordHeader("_producerId", getClientId().getBytes(StandardCharsets.UTF_8))); + headers.add(new RecordHeader("_threadName", Thread.currentThread().getName().getBytes(StandardCharsets.UTF_8))); + if (log.isTraceEnabled()) { + try { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + int maxLevel = Math.min(stackTrace.length, 20); + for (int i = 2; i < maxLevel; i++) { // ignore two levels: getStackTrace and addAnalyticHeaders + headers.add(new RecordHeader("_stackTrace" + i, stackTrace[i].toString().getBytes(StandardCharsets.UTF_8))); + } + } catch (Throwable t) { + log.trace("Failed to add stacktrace headers in Kafka producer {}", getClientId(), t); + } + } + } + + @Override + public void send(TopicPartitionInfo tpi, T msg, QueueCallback callback) { + try { + createTopicIfNotExist(tpi); + String key = msg.getKey(); + byte[] data = msg.getData(); + ProducerRecord record; + List
headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); + if (log.isDebugEnabled()) { + addAnalyticHeaders(headers); + } + record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers); + producer.send(record, (metadata, exception) -> { + if (exception == null) { + if (callback != null) { + callback.onSuccess(new KafkaQueueMsgMetadata(metadata)); + } + } else { + if (callback != null) { + callback.onFailure(exception); + } else { + log.warn("Producer template failure: {}", exception.getMessage(), exception); + } + } + }); + } catch (Exception e) { + if (callback != null) { + callback.onFailure(e); + } else { + log.warn("Producer template failure (send method wrapper): {}", e.getMessage(), e); + } + throw e; + } + } + + private void createTopicIfNotExist(TopicPartitionInfo tpi) { + if (topics.contains(tpi)) { + return; + } + admin.createTopicIfNotExists(tpi.getFullTopicName()); + topics.add(tpi); + } + + @Override + public void stop() { + if (producer != null) { + producer.close(); + } + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaQueueMsgMetadata.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaQueueMsgMetadata.java new file mode 100644 index 0000000..02bd2c7 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaQueueMsgMetadata.java @@ -0,0 +1,17 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.kafka; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.apache.kafka.clients.producer.RecordMetadata; +import sanbing.jcpp.infrastructure.queue.QueueMsgMetadata; + +@Data +@AllArgsConstructor +public class KafkaQueueMsgMetadata implements QueueMsgMetadata { + + private RecordMetadata metadata; +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaSettings.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaSettings.java new file mode 100644 index 0000000..fc5f3cf --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaSettings.java @@ -0,0 +1,210 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.kafka; + +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.AdminClientConfig; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.serialization.ByteArrayDeserializer; +import org.apache.kafka.common.serialization.ByteArraySerializer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.util.property.JCPPProperty; +import sanbing.jcpp.infrastructure.util.property.PropertyUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +@Slf4j +@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +@ConfigurationProperties(prefix = "queue.kafka") +@Component +public class KafkaSettings { + + @Value("${queue.kafka.bootstrap-servers}") + private String servers; + + @Value("${queue.kafka.ssl.enabled:false}") + private boolean sslEnabled; + + @Value("${queue.kafka.ssl.truststore-location:}") + private String sslTruststoreLocation; + + @Value("${queue.kafka.ssl.truststore-password:}") + private String sslTruststorePassword; + + @Value("${queue.kafka.ssl.keystore-location:}") + private String sslKeystoreLocation; + + @Value("${queue.kafka.ssl.keystore-password:}") + private String sslKeystorePassword; + + @Value("${queue.kafka.ssl.key-password:}") + private String sslKeyPassword; + + @Value("${queue.kafka.acks:all}") + private String acks; + + @Value("${queue.kafka.retries:1}") + private int retries; + + @Value("${queue.kafka.compression-type:none}") + private String compressionType; + + @Value("${queue.kafka.batch-size:16384}") + private int batchSize; + + @Value("${queue.kafka.linger-ms:1}") + private long lingerMs; + + @Value("${queue.kafka.max-request-size:1048576}") + private int maxRequestSize; + + @Value("${queue.kafka.max-in-flight-requests-per-connection:5}") + private int maxInFlightRequestsPerConnection; + + @Value("${queue.kafka.buffer-memory:33554432}") + private long bufferMemory; + + @Value("${queue.kafka.replication-factor:1}") + @Getter + private short replicationFactor; + + @Value("${queue.kafka.max-poll-records:8192}") + private int maxPollRecords; + + @Value("${queue.kafka.max-poll-interval-ms:300000}") + private int maxPollIntervalMs; + + @Value("${queue.kafka.max-partition-fetch-bytes:16777216}") + private int maxPartitionFetchBytes; + + @Value("${queue.kafka.fetch-max-bytes:134217728}") + private int fetchMaxBytes; + + @Value("${queue.kafka.request-timeout-ms:30000}") + private int requestTimeoutMs; + + @Value("${queue.kafka.session-timeout-ms:10000}") + private int sessionTimeoutMs; + + @Value("${queue.kafka.auto-offset-reset:earliest}") + private String autoOffsetReset; + + @Value("${queue.kafka.other-inline:}") + private String otherInline; + + + @Setter + private Map> consumerPropertiesPerTopic = Collections.emptyMap(); + @Setter + private Map> producerPropertiesPerTopic = Collections.emptyMap(); + + private volatile AdminClient adminClient; + + public Properties toConsumerProps(String topic) { + Properties props = toProps(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers); + props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPollRecords); + props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, maxPartitionFetchBytes); + props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes); + props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); + + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); + + consumerPropertiesPerTopic + .getOrDefault(topic, Collections.emptyList()) + .forEach(kv -> props.put(kv.getKey(), kv.getValue())); + + return props; + } + + public Properties toProducerProps(String topic) { + Properties props = toProps(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers); + props.put(ProducerConfig.RETRIES_CONFIG, retries); + props.put(ProducerConfig.ACKS_CONFIG, acks); + props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize); + props.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs); + props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); + props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, compressionType); + props.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG, maxRequestSize); + props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, maxInFlightRequestsPerConnection); + + producerPropertiesPerTopic + .getOrDefault(topic, Collections.emptyList()) + .forEach(kv -> props.put(kv.getKey(), kv.getValue())); + + return props; + } + + Properties toProps() { + Properties props = new Properties(); + + props.put(CommonClientConfigs.REQUEST_TIMEOUT_MS_CONFIG, requestTimeoutMs); + props.put(CommonClientConfigs.SESSION_TIMEOUT_MS_CONFIG, sessionTimeoutMs); + + props.putAll(PropertyUtils.getProps(otherInline)); + + configureSSL(props); + + return props; + } + + void configureSSL(Properties props) { + if (sslEnabled) { + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocation); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, sslTruststorePassword); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, sslKeystoreLocation); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, sslKeystorePassword); + props.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, sslKeyPassword); + } + } + + public AdminClient getAdminClient() { + if (adminClient == null) { + synchronized (this) { + if (adminClient == null) { + adminClient = AdminClient.create(toAdminProps()); + } + } + } + return adminClient; + } + + protected Properties toAdminProps() { + Properties props = toProps(); + props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers); + props.put(AdminClientConfig.RETRIES_CONFIG, retries); + return props; + } + + @PreDestroy + private void destroy() { + if (adminClient != null) { + adminClient.close(); + } + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaTopicConfigs.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaTopicConfigs.java new file mode 100644 index 0000000..111e76e --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/kafka/KafkaTopicConfigs.java @@ -0,0 +1,32 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.kafka; + +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.util.property.PropertyUtils; + +import java.util.Map; + +@Component +@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +public class KafkaTopicConfigs { + public static final String NUM_PARTITIONS_SETTING = "partitions"; + + @Value("${queue.kafka.topic-properties.app:}") + private String appProperties; + + @Getter + private Map appConfigs; + + @PostConstruct + private void init() { + this.appConfigs = PropertyUtils.getProps(appProperties); + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/DefaultInMemoryStorage.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/DefaultInMemoryStorage.java new file mode 100644 index 0000000..f61f905 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/DefaultInMemoryStorage.java @@ -0,0 +1,66 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.memory; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.queue.QueueMsg; + +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; + +@Component +@Slf4j +public final class DefaultInMemoryStorage implements InMemoryStorage { + private final ConcurrentHashMap> storage = new ConcurrentHashMap<>(); + + @Override + public void printStats() { + if (log.isDebugEnabled()) { + storage.forEach((topic, queue) -> { + if (!queue.isEmpty()) { + log.debug("[{}] Queue Size [{}]", topic, queue.size()); + } + }); + } + } + + @Override + public int getLagTotal() { + return storage.values().stream().map(BlockingQueue::size).reduce(0, Integer::sum); + } + + @Override + public int getLag(String topic) { + return Optional.ofNullable(storage.get(topic)).map(Collection::size).orElse(0); + } + + @Override + public boolean put(String topic, QueueMsg msg) { + return storage.computeIfAbsent(topic, (t) -> new LinkedBlockingQueue<>()).add(msg); + } + + @Override + public List get(String topic) throws InterruptedException { + final BlockingQueue queue = storage.get(topic); + if (queue != null) { + final QueueMsg firstMsg = queue.poll(); + if (firstMsg != null) { + final int queueSize = queue.size(); + if (queueSize > 0) { + final List entities = new ArrayList<>(Math.min(queueSize, 999) + 1); + entities.add(firstMsg); + queue.drainTo(entities, 999); + return entities; + } + return Collections.singletonList(firstMsg); + } + } + return Collections.emptyList(); + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryQueueConsumer.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryQueueConsumer.java new file mode 100644 index 0000000..181c12f --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryQueueConsumer.java @@ -0,0 +1,106 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.memory; + +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.queue.QueueConsumer; +import sanbing.jcpp.infrastructure.queue.QueueMsg; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class InMemoryQueueConsumer implements QueueConsumer { + private final InMemoryStorage storage; + private volatile Set partitions; + private volatile boolean stopped; + private volatile boolean subscribed; + + public InMemoryQueueConsumer(InMemoryStorage storage, String topic) { + this.storage = storage; + this.topic = topic; + stopped = false; + } + + private final String topic; + + @Override + public String getTopic() { + return topic; + } + + @Override + public void subscribe() { + partitions = Collections.singleton(new TopicPartitionInfo(topic, null, true)); + subscribed = true; + } + + @Override + public void subscribe(Set partitions) { + this.partitions = partitions; + subscribed = true; + } + + @Override + public void stop() { + stopped = true; + } + + @Override + public void unsubscribe() { + stopped = true; + subscribed = false; + } + + @Override + public List poll(long durationInMillis) { + if (subscribed) { + @SuppressWarnings("unchecked") + List messages = partitions + .stream() + .map(tpi -> { + try { + return storage.get(tpi.getFullTopicName()); + } catch (InterruptedException e) { + if (!stopped) { + log.error("Queue was interrupted.", e); + } + return Collections.emptyList(); + } + }) + .flatMap(List::stream) + .map(msg -> (T) msg).collect(Collectors.toList()); + if (!messages.isEmpty()) { + return messages; + } + try { + Thread.sleep(durationInMillis); + } catch (InterruptedException e) { + if (!stopped) { + log.error("Failed to sleep.", e); + } + } + } + return Collections.emptyList(); + } + + @Override + public void commit() { + } + + @Override + public boolean isStopped() { + return stopped; + } + + @Override + public List getFullTopicNames() { + return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryQueueProducer.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryQueueProducer.java new file mode 100644 index 0000000..594b26d --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryQueueProducer.java @@ -0,0 +1,48 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.memory; + +import lombok.Data; +import sanbing.jcpp.infrastructure.queue.QueueCallback; +import sanbing.jcpp.infrastructure.queue.QueueMsg; +import sanbing.jcpp.infrastructure.queue.QueueProducer; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; + +@Data +public class InMemoryQueueProducer implements QueueProducer { + private final InMemoryStorage storage; + + private final String topic; + + public InMemoryQueueProducer(InMemoryStorage storage, String topic) { + this.storage = storage; + this.topic = topic; + } + + @Override + public void init() { + + } + + @Override + public void send(TopicPartitionInfo tpi, T msg, QueueCallback callback) { + boolean result = storage.put(tpi.getFullTopicName(), msg); + if (result) { + if (callback != null) { + callback.onSuccess(null); + } + } else { + if (callback != null) { + callback.onFailure(new RuntimeException("Failure add msg to InMemoryQueue")); + } + } + } + + @Override + public void stop() { + + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryStorage.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryStorage.java new file mode 100644 index 0000000..cc6b60a --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/memory/InMemoryStorage.java @@ -0,0 +1,24 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.memory; + + +import sanbing.jcpp.infrastructure.queue.QueueMsg; + +import java.util.List; + +public interface InMemoryStorage { + + void printStats(); + + int getLagTotal(); + + int getLag(String topic); + + boolean put(String topic, QueueMsg msg); + + List get(String topic) throws InterruptedException; + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/processing/IdMsgPair.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/processing/IdMsgPair.java new file mode 100644 index 0000000..8a6eec5 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/processing/IdMsgPair.java @@ -0,0 +1,22 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.processing; + +import lombok.Getter; +import sanbing.jcpp.infrastructure.queue.ProtoQueueMsg; + +import java.util.UUID; + +public class IdMsgPair { + @Getter + final UUID uuid; + @Getter + final ProtoQueueMsg msg; + + public IdMsgPair(UUID uuid, ProtoQueueMsg msg) { + this.uuid = uuid; + this.msg = msg; + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/AppQueueFactory.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/AppQueueFactory.java new file mode 100644 index 0000000..ce80fe5 --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/AppQueueFactory.java @@ -0,0 +1,18 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.provider; + + +import sanbing.jcpp.infrastructure.queue.ProtoQueueMsg; +import sanbing.jcpp.infrastructure.queue.QueueConsumer; +import sanbing.jcpp.infrastructure.queue.QueueProducer; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; + +public interface AppQueueFactory { + + QueueConsumer> createProtocolUplinkMsgConsumer(); + + QueueProducer> createProtocolUplinkMsgProducer(String topic); +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/InMemoryAppQueueFactory.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/InMemoryAppQueueFactory.java new file mode 100644 index 0000000..82cb1ad --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/InMemoryAppQueueFactory.java @@ -0,0 +1,48 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.provider; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.queue.ProtoQueueMsg; +import sanbing.jcpp.infrastructure.queue.QueueConsumer; +import sanbing.jcpp.infrastructure.queue.QueueProducer; +import sanbing.jcpp.infrastructure.queue.memory.InMemoryQueueConsumer; +import sanbing.jcpp.infrastructure.queue.memory.InMemoryQueueProducer; +import sanbing.jcpp.infrastructure.queue.memory.InMemoryStorage; +import sanbing.jcpp.infrastructure.queue.settings.QueueAppSettings; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; + +@Slf4j +@Component +@ConditionalOnExpression("'${queue.type:null}'=='memory' && '${service.type:null}'=='monolith'") +public class InMemoryAppQueueFactory implements AppQueueFactory { + + private final InMemoryStorage storage; + private final QueueAppSettings appSettings; + + public InMemoryAppQueueFactory(InMemoryStorage storage, QueueAppSettings appSettings) { + this.storage = storage; + this.appSettings = appSettings; + } + + @Override + public QueueConsumer> createProtocolUplinkMsgConsumer() { + return new InMemoryQueueConsumer<>(storage, appSettings.getTopic()); + } + + @Override + public QueueProducer> createProtocolUplinkMsgProducer(String topic) { + return new InMemoryQueueProducer<>(storage, topic); + } + + @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") + private void printInMemoryStats() { + storage.printStats(); + } + +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/KafkaAppQueueFactory.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/KafkaAppQueueFactory.java new file mode 100644 index 0000000..155e15b --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/provider/KafkaAppQueueFactory.java @@ -0,0 +1,83 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.provider; + +import com.google.protobuf.util.JsonFormat; +import jakarta.annotation.PreDestroy; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.queue.ProtoQueueMsg; +import sanbing.jcpp.infrastructure.queue.QueueAdmin; +import sanbing.jcpp.infrastructure.queue.QueueConsumer; +import sanbing.jcpp.infrastructure.queue.QueueProducer; +import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider; +import sanbing.jcpp.infrastructure.queue.kafka.*; +import sanbing.jcpp.infrastructure.queue.settings.QueueAppSettings; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; + +@Component +@ConditionalOnExpression("'${queue.type:null}'=='kafka'") +public class KafkaAppQueueFactory implements AppQueueFactory { + + private final KafkaSettings kafkaSettings; + private final QueueAppSettings appSettings; + private final KafkaConsumerStatsService consumerStatsService; + private final ServiceInfoProvider serviceInfoProvider; + + private final QueueAdmin appAdmin; + + public KafkaAppQueueFactory(KafkaSettings kafkaSettings, + ServiceInfoProvider serviceInfoProvider, + QueueAppSettings appSettings, + KafkaConsumerStatsService consumerStatsService, + KafkaTopicConfigs kafkaTopicConfigs) { + this.kafkaSettings = kafkaSettings; + this.serviceInfoProvider = serviceInfoProvider; + this.appSettings = appSettings; + this.consumerStatsService = consumerStatsService; + + this.appAdmin = new KafkaAdmin(kafkaSettings, kafkaTopicConfigs.getAppConfigs()); + } + + + @Override + public QueueConsumer> createProtocolUplinkMsgConsumer() { + KafkaConsumerTemplate.KafkaConsumerTemplateBuilder> consumerBuilder = KafkaConsumerTemplate.builder(); + consumerBuilder.settings(kafkaSettings); + consumerBuilder.topic(appSettings.getTopic()); + consumerBuilder.clientId("protocol-uplink-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.groupId("protocol-uplink-consumer"); + if (appSettings.getDecoder() == QueueAppSettings.DecoderType.protobuf) { + consumerBuilder.decoder(msg -> new ProtoQueueMsg<>(msg.getKey(), UplinkQueueMessage.parseFrom(msg.getData()), msg.getHeaders())); + } else { + consumerBuilder.decoder(msg -> { + UplinkQueueMessage.Builder builder = UplinkQueueMessage.newBuilder(); + JsonFormat.parser().merge(new String(msg.getData()), builder); + return new ProtoQueueMsg<>(msg.getKey(), builder.build(), msg.getHeaders()); + }); + } + consumerBuilder.admin(appAdmin); + consumerBuilder.statsService(consumerStatsService); + return consumerBuilder.build(); + } + + @Override + public QueueProducer> createProtocolUplinkMsgProducer(String topic) { + KafkaProducerTemplate.KafkaProducerTemplateBuilder> requestBuilder = KafkaProducerTemplate.builder(); + requestBuilder.settings(kafkaSettings); + requestBuilder.clientId("protocol-to-app-" + serviceInfoProvider.getServiceId()); + requestBuilder.topic(topic); + requestBuilder.admin(appAdmin); + return requestBuilder.build(); + } + + + @PreDestroy + private void destroy() { + if (appAdmin != null) { + appAdmin.destroy(); + } + } +} diff --git a/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/settings/QueueAppSettings.java b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/settings/QueueAppSettings.java new file mode 100644 index 0000000..74415fb --- /dev/null +++ b/jcpp-infrastructure-queue/src/main/java/sanbing/jcpp/infrastructure/queue/settings/QueueAppSettings.java @@ -0,0 +1,30 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.queue.settings; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +@Lazy +@Data +@Component +public class QueueAppSettings { + + @Value("${queue.app.topic}") + private String topic; + + @Value("${queue.app.partitions}") + private int partitions; + + @Value("${queue.app.decoder:protobuf}") + private DecoderType decoder; + + public enum DecoderType { + protobuf, + json + } +} diff --git a/jcpp-infrastructure-stats/pom.xml b/jcpp-infrastructure-stats/pom.xml new file mode 100644 index 0000000..bf27d95 --- /dev/null +++ b/jcpp-infrastructure-stats/pom.xml @@ -0,0 +1,57 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-infrastructure-stats + jar + JChargePointProtocol Infrastructure Stats Module + 基础监控模块 + + + ${basedir}/.. + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + org.apache.commons + commons-lang3 + + + + diff --git a/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultCounter.java b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultCounter.java new file mode 100644 index 0000000..83f7b36 --- /dev/null +++ b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultCounter.java @@ -0,0 +1,37 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.stats; + +import io.micrometer.core.instrument.Counter; + +import java.util.concurrent.atomic.AtomicInteger; + +public class DefaultCounter { + private final AtomicInteger aiCounter; + private final Counter micrometerCounter; + + public DefaultCounter(AtomicInteger aiCounter, Counter micrometerCounter) { + this.aiCounter = aiCounter; + this.micrometerCounter = micrometerCounter; + } + + public void increment() { + aiCounter.incrementAndGet(); + micrometerCounter.increment(); + } + + public void clear() { + aiCounter.set(0); + } + + public int get() { + return aiCounter.get(); + } + + public void add(int delta){ + aiCounter.addAndGet(delta); + micrometerCounter.increment(delta); + } +} diff --git a/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultMessagesStats.java b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultMessagesStats.java new file mode 100644 index 0000000..d0ea727 --- /dev/null +++ b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultMessagesStats.java @@ -0,0 +1,54 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.stats; + +public class DefaultMessagesStats implements MessagesStats { + private final StatsCounter totalCounter; + private final StatsCounter successfulCounter; + private final StatsCounter failedCounter; + + public DefaultMessagesStats(StatsCounter totalCounter, StatsCounter successfulCounter, StatsCounter failedCounter) { + this.totalCounter = totalCounter; + this.successfulCounter = successfulCounter; + this.failedCounter = failedCounter; + } + + @Override + public void incrementTotal(int amount) { + totalCounter.add(amount); + } + + @Override + public void incrementSuccessful(int amount) { + successfulCounter.add(amount); + } + + @Override + public void incrementFailed(int amount) { + failedCounter.add(amount); + } + + @Override + public int getTotal() { + return totalCounter.get(); + } + + @Override + public int getSuccessful() { + return successfulCounter.get(); + } + + @Override + public int getFailed() { + return failedCounter.get(); + } + + @Override + public void reset() { + totalCounter.clear(); + successfulCounter.clear(); + failedCounter.clear(); + } +} diff --git a/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultStatsFactory.java b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultStatsFactory.java new file mode 100644 index 0000000..8126f85 --- /dev/null +++ b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/DefaultStatsFactory.java @@ -0,0 +1,124 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.stats; + +import io.micrometer.common.util.StringUtils; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.concurrent.atomic.AtomicInteger; + +@Service +public class DefaultStatsFactory implements StatsFactory { + private static final String TOTAL_MSGS = "totalMsgs"; + private static final String SUCCESSFUL_MSGS = "successfulMsgs"; + private static final String FAILED_MSGS = "failedMsgs"; + + private static final String STATS_NAME_TAG = "statsName"; + + private static final Counter STUB_COUNTER = new StubCounter(); + + @Resource + private MeterRegistry meterRegistry; + + @Value("${metrics.enabled:true}") + private Boolean metricsEnabled; + + @Value("${metrics.timer.percentiles:1.0}") + private String timerPercentilesStr; + + private double[] timerPercentiles; + + @PostConstruct + public void init() { + if (StringUtils.isNotEmpty(timerPercentilesStr)) { + String[] split = timerPercentilesStr.split(","); + timerPercentiles = new double[split.length]; + for (int i = 0; i < split.length; i++) { + timerPercentiles[i] = Double.parseDouble(split[i]); + } + } + } + + + @Override + public StatsCounter createStatsCounter(String key, String statsName, String... otherTags) { + String[] tags = getTags(statsName, otherTags); + return new StatsCounter( + new AtomicInteger(0), + metricsEnabled ? meterRegistry.counter(key, tags) : STUB_COUNTER, + statsName + ); + } + + @Override + public DefaultCounter createDefaultCounter(String key, String... tags) { + return new DefaultCounter( + new AtomicInteger(0), + metricsEnabled ? + meterRegistry.counter(key, tags) + : STUB_COUNTER + ); + } + + @Override + public MessagesStats createMessagesStats(String key, String... tags) { + StatsCounter totalCounter = createStatsCounter(key, TOTAL_MSGS, tags); + StatsCounter successfulCounter = createStatsCounter(key, SUCCESSFUL_MSGS, tags); + StatsCounter failedCounter = createStatsCounter(key, FAILED_MSGS, tags); + return new DefaultMessagesStats(totalCounter, successfulCounter, failedCounter); + } + + @Override + public Timer createTimer(String key, String... tags) { + Timer.Builder timerBuilder = Timer.builder(key) + .tags(tags) + .publishPercentiles(); + if (timerPercentiles != null && timerPercentiles.length > 0) { + timerBuilder.publishPercentiles(timerPercentiles); + } + return timerBuilder.register(meterRegistry); + } + + @Override + public T createGauge(String key, T number, String... tags) { + return meterRegistry.gauge(key, Tags.of(tags), number); + } + + private static String[] getTags(String statsName, String[] otherTags) { + String[] tags = new String[]{STATS_NAME_TAG, statsName}; + if (otherTags.length > 0) { + if (otherTags.length % 2 != 0) { + throw new IllegalArgumentException("Invalid tags array size"); + } + tags = ArrayUtils.addAll(tags, otherTags); + } + return tags; + } + + + private static class StubCounter implements Counter { + @Override + public void increment(double amount) { + } + + @Override + public double count() { + return 0; + } + + @Override + public Id getId() { + return null; + } + } +} diff --git a/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/MessagesStats.java b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/MessagesStats.java new file mode 100644 index 0000000..83d9632 --- /dev/null +++ b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/MessagesStats.java @@ -0,0 +1,33 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.stats; + +public interface MessagesStats { + default void incrementTotal() { + incrementTotal(1); + } + + void incrementTotal(int amount); + + default void incrementSuccessful() { + incrementSuccessful(1); + } + + void incrementSuccessful(int amount); + + default void incrementFailed() { + incrementFailed(1); + } + + void incrementFailed(int amount); + + int getTotal(); + + int getSuccessful(); + + int getFailed(); + + void reset(); +} diff --git a/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsCounter.java b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsCounter.java new file mode 100644 index 0000000..2e32e84 --- /dev/null +++ b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsCounter.java @@ -0,0 +1,22 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.stats; + +import io.micrometer.core.instrument.Counter; + +import java.util.concurrent.atomic.AtomicInteger; + +public class StatsCounter extends DefaultCounter { + private final String name; + + public StatsCounter(AtomicInteger aiCounter, Counter micrometerCounter, String name) { + super(aiCounter, micrometerCounter); + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsFactory.java b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsFactory.java new file mode 100644 index 0000000..970c9bf --- /dev/null +++ b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsFactory.java @@ -0,0 +1,59 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.stats; + +import io.micrometer.core.instrument.Timer; + +public interface StatsFactory { + + /** + * 创建状态计数器,默认带一个statsName的Tag,并可以自定义扩展其他Tag + * + * @param key 指标名 + * @param statsName statsName的标签值 + * @param otherTags 其他Tag键值对,参数个数需要是偶数 + * @return + */ + StatsCounter createStatsCounter(String key, String statsName, String... otherTags); + + /** + * 创建计数器,可自定义Tag + * + * @param key 指标名 + * @param tags 自定义Tag键值对,参数个数需要是偶数 + * @return + */ + DefaultCounter createDefaultCounter(String key, String... tags); + + /** + * 创建消息计数器,消息计数器默认包含三种状态(总数、成功数、失败数) + * + * @param key 指标名 + * @param tags 自定义Tag键值对,参数个数需要是偶数 + * @return + */ + MessagesStats createMessagesStats(String key, String... tags); + + /** + * 创建计时器 + * + * @param key 指标名 + * @param tags 自定义Tag键值对,参数个数需要是偶数 + * @return + */ + Timer createTimer(String key, String... tags); + + /** + * 创建计量器,用于记录某个值的当前状态,可以是瞬时数值 + * + * @param key 指标名 + * @param number 初始值 + * @param tags 自定义Tag键值对,参数个数需要是偶数 + * @return + * @param + */ + T createGauge(String key, T number, String... tags); + +} diff --git a/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsTimer.java b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsTimer.java new file mode 100644 index 0000000..c5de588 --- /dev/null +++ b/jcpp-infrastructure-stats/src/main/java/sanbing/jcpp/infrastructure/stats/StatsTimer.java @@ -0,0 +1,44 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.stats; + +import io.micrometer.core.instrument.Timer; +import lombok.Getter; + +import java.util.concurrent.TimeUnit; + +public class StatsTimer { + + @Getter + private final String name; + private final Timer timer; + + private int count; + private long totalTime; + + public StatsTimer(String name, Timer micrometerTimer) { + this.name = name; + this.timer = micrometerTimer; + } + + public void record(long timeMs) { + count++; + totalTime += timeMs; + timer.record(timeMs, TimeUnit.MILLISECONDS); + } + + public double getAvg() { + if (count == 0) { + return 0.0; + } + return (double) totalTime / count; + } + + public void reset() { + count = 0; + totalTime = 0; + } + +} diff --git a/jcpp-infrastructure-util/pom.xml b/jcpp-infrastructure-util/pom.xml new file mode 100644 index 0000000..cbfbe6c --- /dev/null +++ b/jcpp-infrastructure-util/pom.xml @@ -0,0 +1,81 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-infrastructure-util + jar + JChargePointProtocol Infrastructure Util Module + 基础工具模块 + + + ${basedir}/.. + + + + + org.springframework.boot + spring-boot-starter-json + + + org.springframework.boot + spring-boot-starter-log4j2 + + + com.lmax + disruptor + + + com.google.guava + guava + ${guava.version} + + + org.apache.commons + commons-lang3 + + + cn.hutool + hutool-core + + + + jakarta.validation + jakarta.validation-api + + + org.hibernate.validator + hibernate-validator + + + org.glassfish + jakarta.el + + + jakarta.annotation + jakarta.annotation-api + + + com.github.oshi + oshi-core + + + io.netty + netty-buffer + + + + diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/JCPPHashUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/JCPPHashUtil.java new file mode 100644 index 0000000..f296ec8 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/JCPPHashUtil.java @@ -0,0 +1,34 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util; + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * @author baigod + */ +public class JCPPHashUtil { + public static HashFunction forName(String name) { + return switch (name) { + case "murmur3_32" -> Hashing.murmur3_32_fixed(); + case "murmur3_128" -> Hashing.murmur3_128(); + case "sha256" -> Hashing.sha256(); + default -> throw new IllegalArgumentException("Can't find hash function with name " + name); + }; + } + + public static int hash(HashFunction hashFunction, String key) { + return hashFunction.hashString(key, StandardCharsets.UTF_8).asInt(); + } + + public static int hash(HashFunction hashFunction, UUID key) { + return hashFunction.hashString(key.toString(), StandardCharsets.UTF_8).asInt(); + } + +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/JCPPPair.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/JCPPPair.java new file mode 100644 index 0000000..32c2622 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/JCPPPair.java @@ -0,0 +1,19 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class JCPPPair { + private S first; + private T second; + + public static JCPPPair of(S first, T second) { + return new JCPPPair<>(first, second); + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/SystemUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/SystemUtil.java new file mode 100644 index 0000000..571e8f5 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/SystemUtil.java @@ -0,0 +1,95 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util; + +import lombok.extern.slf4j.Slf4j; +import oshi.SystemInfo; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.file.FileStore; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Optional; + +@Slf4j +public class SystemUtil { + + private static final HardwareAbstractionLayer HARDWARE; + + static { + HARDWARE = new SystemInfo().getHardware(); + } + + public static Optional getMemoryUsage() { + try { + GlobalMemory memory = HARDWARE.getMemory(); + long total = memory.getTotal(); + long available = memory.getAvailable(); + return Optional.of(toPercent(total - available, total)); + } catch (Throwable e) { + log.debug("Failed to get memory usage!!!", e); + } + return Optional.empty(); + } + + public static Optional getTotalMemory() { + try { + return Optional.of(HARDWARE.getMemory().getTotal()); + } catch (Throwable e) { + log.debug("Failed to get total memory!!!", e); + } + return Optional.empty(); + } + + public static Optional getCpuUsage() { + try { + return Optional.of((int) (HARDWARE.getProcessor().getSystemCpuLoad(1000) * 100.0)); + } catch (Throwable e) { + log.debug("Failed to get cpu usage!!!", e); + } + return Optional.empty(); + } + + public static Optional getCpuCount() { + try { + return Optional.of(HARDWARE.getProcessor().getLogicalProcessorCount()); + } catch (Throwable e) { + log.debug("Failed to get total cpu count!!!", e); + } + return Optional.empty(); + } + + public static Optional getDiscSpaceUsage() { + try { + FileStore store = Files.getFileStore(Paths.get("/")); + long total = store.getTotalSpace(); + long available = store.getUsableSpace(); + return Optional.of(toPercent(total - available, total)); + } catch (Throwable e) { + log.debug("Failed to get free disc space!!!", e); + } + return Optional.empty(); + } + + public static Optional getTotalDiscSpace() { + try { + FileStore store = Files.getFileStore(Paths.get("/")); + return Optional.of(store.getTotalSpace()); + } catch (Throwable e) { + log.debug("Failed to get total disc space!!!", e); + } + return Optional.empty(); + } + + private static int toPercent(long used, long total) { + BigDecimal u = new BigDecimal(used); + BigDecimal t = new BigDecimal(total); + BigDecimal i = new BigDecimal(100); + return u.multiply(i).divide(t, RoundingMode.HALF_UP).intValue(); + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/AfterStartUp.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/AfterStartUp.java new file mode 100644 index 0000000..afa3bef --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/AfterStartUp.java @@ -0,0 +1,28 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.annotation; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.Order; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@EventListener(ApplicationReadyEvent.class) +@Order +public @interface AfterStartUp { + + int DISCOVERY_SERVICE = 1; + int REGULAR_SERVICE = 2; + + @AliasFor(annotation = Order.class, attribute = "value") + int order(); +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/AppComponent.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/AppComponent.java new file mode 100644 index 0000000..c6f6fae --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/AppComponent.java @@ -0,0 +1,25 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.annotation; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author baigod + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ConditionalOnExpression("'${service.type:null}'=='monolith' || '${service.type:null}'=='app'") +@Component +public @interface AppComponent { + + +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/ProtocolComponent.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/ProtocolComponent.java new file mode 100644 index 0000000..1b56966 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/annotation/ProtocolComponent.java @@ -0,0 +1,49 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.annotation; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.util.annotation.ProtocolComponent.ProtocolCondition; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author baigod + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Conditional(ProtocolCondition.class) +@Component +public @interface ProtocolComponent { + + @AliasFor(annotation = Component.class) + String value() default ""; + + class ProtocolCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + if (!metadata.isAnnotated(ProtocolComponent.class.getName())) { + return true; + } + + String serviceType = context.getEnvironment().getProperty("service.type", "null"); + + String protocolName = (String) metadata.getAnnotationAttributes(ProtocolComponent.class.getName()).get("value"); + + String enabled = context.getEnvironment().getProperty("service.protocols." + protocolName + ".enabled", "false"); + + return ("monolith".equals(serviceType) || "protocol".equals(serviceType)) && "true".equals(enabled); + } + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPAsynchron.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPAsynchron.java new file mode 100644 index 0000000..04a37ce --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPAsynchron.java @@ -0,0 +1,54 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.async; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +public class JCPPAsynchron { + + public static void withCallback(ListenableFuture future, Consumer onSuccess, + Consumer onFailure) { + withCallback(future, onSuccess, onFailure, null); + } + + public static void withCallback(ListenableFuture future, Consumer onSuccess, + Consumer onFailure, Executor executor) { + FutureCallback callback = new FutureCallback<>() { + @Override + public void onSuccess(T result) { + try { + onSuccess.accept(result); + } catch (Throwable th) { + onFailure(th); + } + } + + @Override + public void onFailure(Throwable t) { + onFailure.accept(t); + } + }; + Futures.addCallback(future, callback, Objects.requireNonNullElseGet(executor, MoreExecutors::directExecutor)); + } + + public static ListenableFuture submit(Callable task, Consumer onSuccess, Consumer onFailure, Executor executor) { + return submit(task, onSuccess, onFailure, executor, null); + } + + public static ListenableFuture submit(Callable task, Consumer onSuccess, Consumer onFailure, Executor executor, Executor callbackExecutor) { + ListenableFuture future = Futures.submit(task, executor); + withCallback(future, onSuccess, onFailure, callbackExecutor); + return future; + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPExecutors.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPExecutors.java new file mode 100644 index 0000000..7a92486 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPExecutors.java @@ -0,0 +1,27 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.async; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; + +public class JCPPExecutors { + + public static ExecutorService newWorkStealingPool(int parallelism, String namePrefix) { + return new ForkJoinPool(parallelism, + new JCPPForkJoinWorkerThreadFactory(namePrefix), + null, true); + } + + public static ExecutorService newWorkStealingPool(int parallelism, Class clazz) { + return newWorkStealingPool(parallelism, clazz.getSimpleName()); + } + + public static ExecutorService newVirtualThreadPool(String namePrefix) { + return Executors.newThreadPerTaskExecutor(new JCPPVirtualThreadFactory(namePrefix)); + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPForkJoinWorkerThreadFactory.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPForkJoinWorkerThreadFactory.java new file mode 100644 index 0000000..c2758c7 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPForkJoinWorkerThreadFactory.java @@ -0,0 +1,30 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.async; + +import lombok.NonNull; +import lombok.ToString; + +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; +import java.util.concurrent.atomic.AtomicLong; + +@ToString +public class JCPPForkJoinWorkerThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory { + private final String namePrefix; + private final AtomicLong threadNumber = new AtomicLong(1); + + public JCPPForkJoinWorkerThreadFactory(@NonNull String namePrefix) { + this.namePrefix = namePrefix; + } + + @Override + public final ForkJoinWorkerThread newThread(ForkJoinPool pool) { + ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + thread.setContextClassLoader(this.getClass().getClassLoader()); + thread.setName(namePrefix + "-" + thread.getPoolIndex() + "-" + threadNumber.getAndIncrement()); + return thread; + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPThreadFactory.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPThreadFactory.java new file mode 100644 index 0000000..d6b02c3 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPThreadFactory.java @@ -0,0 +1,46 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.async; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import java.util.concurrent.ThreadFactory; + +public class JCPPThreadFactory { + public static final String THREAD_TOPIC_SEPARATOR = " | "; + + public static ThreadFactory forName(String name) { + return new ThreadFactoryBuilder() + .setNameFormat(name) + .setDaemon(true) + .setPriority(Thread.NORM_PRIORITY) + .build(); + } + + public static ThreadFactory forName(String name, int priority) { + return new ThreadFactoryBuilder() + .setNameFormat(name) + .setDaemon(true) + .setPriority(priority) + .build(); + } + public static void updateCurrentThreadName(String threadSuffix) { + String name = Thread.currentThread().getName(); + int spliteratorIndex = name.indexOf(THREAD_TOPIC_SEPARATOR); + if (spliteratorIndex > 0) { + name = name.substring(0, spliteratorIndex); + } + name = name + THREAD_TOPIC_SEPARATOR + threadSuffix; + Thread.currentThread().setName(name); + } + + public static void addThreadNamePrefix(String prefix) { + String name = Thread.currentThread().getName(); + name = prefix + "-" + name; + Thread.currentThread().setName(name); + } + + +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPVirtualThreadFactory.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPVirtualThreadFactory.java new file mode 100644 index 0000000..2c88144 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/async/JCPPVirtualThreadFactory.java @@ -0,0 +1,24 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.async; + +import sanbing.jcpp.infrastructure.util.trace.TracerRunnable; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +public class JCPPVirtualThreadFactory implements ThreadFactory { + private final String namePrefix; + private final AtomicLong threadNumber = new AtomicLong(1); + + public JCPPVirtualThreadFactory(String namePrefix) { + this.namePrefix = namePrefix; + } + + @Override + public Thread newThread(Runnable r) { + return Thread.ofVirtual().name(namePrefix + "-" + threadNumber.getAndIncrement()).unstarted(new TracerRunnable(r)); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/BCDUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/BCDUtil.java new file mode 100644 index 0000000..0fde9eb --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/BCDUtil.java @@ -0,0 +1,172 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.codec; + + +public class BCDUtil { + private static final String HEX = "0123456789ABCDEF"; + + /** + * 十进制 转 BCD字节数组 + * + * @param num long (8字节) + * @return byte[] + */ + public static byte[] longToBcdBytes(long num) { + int digits = 0; + long temp = num; + while (temp != 0) { + digits++; + temp /= 10; + } + int byteLen = digits % 2 == 0 ? digits / 2 : (digits + 1) / 2; + byte[] bcd = new byte[byteLen]; + for (int i = 0; i < digits; i++) { + byte tmp = (byte) (num % 10); + if (i % 2 == 0) { + bcd[i / 2] = tmp; + } else { + bcd[i / 2] |= (byte) (tmp << 4); + } + num /= 10; + } + for (int i = 0; i < byteLen / 2; i++) { + byte tmp = bcd[i]; + bcd[i] = bcd[byteLen - i - 1]; + bcd[byteLen - i - 1] = tmp; + } + return bcd; + } + + /** + * BCD字节数组 转 十进制 + * + * @param bcd byte[] + * @return long + */ + public static long bcdBytesToLong(byte[] bcd) { + return Long.parseLong(BCDUtil.toString(bcd)); + } + + /** + * bcd字节数组 转 数字字符串 + * + * @param bcd byte[] + * @return String + */ + public static String toString(byte[] bcd) { + StringBuilder sb = new StringBuilder(); + for (byte b : bcd) { + sb.append(toString(b)); + } + return sb.toString(); + } + + /** + * 单个字节BCD 转 数字字符串 + * + * @param bcd byte + * @return String + */ + public static String toString(byte bcd) { + StringBuilder sb = new StringBuilder(); + byte high = (byte) (bcd & 0xf0); + high >>>= (byte) 4; + high = (byte) (high & 0x0f); + byte low = (byte) (bcd & 0x0f); + + sb.append(high); + sb.append(low); + return sb.toString(); + } + + /** + * 数字字符串 转 BCD字节数组 + * + * @param str 数字字符串 + * @return BCD字节数组 + */ + public static byte[] numStrToBcdBytes(String str) { + //若为奇数,补0为偶 + if ((str.length() & 0x1) == 1) { + str = "0" + str; + } + byte[] ret = new byte[str.length() / 2]; + byte[] bs = str.getBytes(); + for (int i = 0; i < ret.length; i++) { + byte high = ascII2Bcd(bs[2 * i]); + byte low = ascII2Bcd(bs[2 * i + 1]); + ret[i] = (byte) ((high << 4) | low); + } + return ret; + } + + public static byte ascII2Bcd(byte asc) { + if ((asc >= '0') && (asc <= '9')) + return (byte) (asc - '0'); + else if ((asc >= 'A') && (asc <= 'F')) + return (byte) (asc - 'A' + 10); + else if ((asc >= 'a') && (asc <= 'f')) + return (byte) (asc - 'a' + 10); + else + return (byte) (asc - 48); + } + + /** + * BCD 转 数字 + * + * @param bcd byte + * @return int + */ + public static int bcdByteToInt(byte bcd) { + return ((bcd & 0xF0) >>> 4) * 10 + (bcd & 0x0F); + } + + + /** + * char to byte + * + * @param c char + * @return byte + */ + private static byte charToByte(char c) { + return (byte) HEX.indexOf(c); + } + + /** + * Hex 转 BCD字节数组 + * + * @param hex String + * @return BCD字节数组 + */ + public static byte[] toBytes(String hex) { + int len = (hex.length() / 2); + byte[] result = new byte[len]; + char[] cr = hex.toCharArray(); + for (int i = 0; i < len; i++) { + int pos = i * 2; + result[i] = (byte) (charToByte(cr[pos]) << 4 | charToByte(cr[pos + 1])); + } + return result; + } + + /** + * BCD字节数组 转 Hex + * + * @param bcd BCD字节数组 + * @return Hex + */ + public static String bcdBytesToHex(byte[] bcd) { + StringBuilder sb = new StringBuilder(); + for (byte b : bcd) { + int highNibble = (b >> 4) & 0x0F; + int lowNibble = b & 0x0F; + sb.append(Integer.toHexString(highNibble)); + sb.append(Integer.toHexString(lowNibble)); + } + return sb.toString().toUpperCase(); + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/ByteUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/ByteUtil.java new file mode 100644 index 0000000..3575809 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/ByteUtil.java @@ -0,0 +1,81 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.codec; + +import cn.hutool.core.io.checksum.crc16.CRC16Modbus; +import io.netty.buffer.ByteBuf; +import sanbing.jcpp.infrastructure.util.JCPPPair; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * @author baigod + */ +public class ByteUtil { + + public static byte[] uuidToBytes(UUID uuid) { + ByteBuffer buf = ByteBuffer.allocate(16); + buf.putLong(uuid.getMostSignificantBits()); + buf.putLong(uuid.getLeastSignificantBits()); + return buf.array(); + } + + public static UUID bytesToUuid(byte[] bytes) { + ByteBuffer bb = ByteBuffer.wrap(bytes); + long firstLong = bb.getLong(); + long secondLong = bb.getLong(); + return new UUID(firstLong, secondLong); + } + + public static byte[] stringToBytes(String string) { + return string.getBytes(StandardCharsets.UTF_8); + } + + public static String bytesToString(byte[] data) { + return new String(data, StandardCharsets.UTF_8); + } + + public static byte[] longToBytes(long x) { + ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES); + longBuffer.putLong(0, x); + return longBuffer.array(); + } + + public static long bytesToLong(byte[] bytes) { + return ByteBuffer.wrap(bytes).getLong(); + } + + /** + * 计算校验和 + */ + public static int crcSum(byte[] data) { + CRC16Modbus crc16Modbus = new CRC16Modbus(); + crc16Modbus.update(data); + return (int) crc16Modbus.getValue(); + } + + /** + * 验证校验和 + */ + public static JCPPPair checkCrcSum(byte[] data, int checkSum) { + int expectedCs = crcSum(data); + return JCPPPair.of(expectedCs == checkSum, expectedCs); + } + + /** + * ByteBuf转byte数组 + * + * @param byteBuf + * @return + */ + public static byte[] toBytes(ByteBuf byteBuf) { + int msgLength = byteBuf.readableBytes(); + byte[] bytes = new byte[msgLength]; + byteBuf.readBytes(bytes); + return bytes; + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/CP56Time2aUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/CP56Time2aUtil.java new file mode 100644 index 0000000..83c02fc --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/codec/CP56Time2aUtil.java @@ -0,0 +1,65 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.codec; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +public class CP56Time2aUtil { + /** + * 解码 CP56Time2a 字节数组为 Instant 对象 + * + * @param bytes 字节数组 + * @return Instant 对象 + */ + public static Instant decode(byte[] bytes) { + // 将字节数组解释为各个时间部分 + int milliseconds = ((bytes[0] & 0xFF) + ((bytes[1] & 0xFF) << 8)); // 处理字节的无符号值 + int minutes = bytes[2] & 0x3F; + int hours = bytes[3] & 0x1F; + int days = bytes[4] & 0x1F; + int months = bytes[5] & 0x0F; + int years = bytes[6] & 0x7F; + + // 将 CP56Time2a 转换为 LocalDateTime + LocalDateTime dateTime = LocalDateTime.of( + years + 2000, + months, + days, + hours, + minutes, + milliseconds / 1000 // 秒数 + ); + + // 返回对应的 Instant 对象 + return dateTime.atZone(ZoneId.systemDefault()).toInstant(); + } + + /** + * 编码 Instant 对象为 CP56Time2a 字节数组 + * + * @param instant Instant 对象 + * @return 字节数组 + */ + public static byte[] encode(Instant instant) { + // 将 Instant 转换到 LocalDateTime + LocalDateTime aTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + + byte[] result = new byte[7]; + int milliseconds = aTime.getSecond() * 1000; // 获取毫秒部分 + + // 填充字节数组 + result[0] = (byte) (milliseconds % 256); + result[1] = (byte) (milliseconds / 256); + result[2] = (byte) aTime.getMinute(); + result[3] = (byte) aTime.getHour(); + result[4] = (byte) aTime.getDayOfMonth(); + result[5] = (byte) aTime.getMonthValue(); // 1-12 + result[6] = (byte) (aTime.getYear() % 100); // 00-99 + + return result; + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ConstraintValidator.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ConstraintValidator.java new file mode 100644 index 0000000..8d40b7f --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ConstraintValidator.java @@ -0,0 +1,97 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.config; + +import com.google.common.collect.Iterators; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.metadata.ConstraintDescriptor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; +import org.hibernate.validator.cfg.ConstraintMapping; +import org.hibernate.validator.internal.cfg.context.DefaultConstraintMapping; +import org.hibernate.validator.internal.engine.ConfigurationImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import sanbing.jcpp.infrastructure.util.exception.DataValidationException; +import sanbing.jcpp.infrastructure.util.validation.Length; +import sanbing.jcpp.infrastructure.util.validation.StringLengthValidator; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Configuration +public class ConstraintValidator { + + private static Validator fieldsValidator; + + static { + initializeValidators(); + } + + public static void validateFields(Object data) { + validateFields(data, "Validation error: "); + } + + public static void validateFields(Object data, String errorPrefix) { + Set> constraintsViolations = fieldsValidator.validate(data); + if (!constraintsViolations.isEmpty()) { + throw new DataValidationException(errorPrefix + getErrorMessage(constraintsViolations)); + } + } + + public static String getErrorMessage(Collection> constraintsViolations) { + return constraintsViolations.stream() + .map(ConstraintValidator::getErrorMessage) + .distinct().sorted().collect(Collectors.joining(", ")); + } + + public static String getErrorMessage(ConstraintViolation constraintViolation) { + ConstraintDescriptor constraintDescriptor = constraintViolation.getConstraintDescriptor(); + String property = (String) constraintDescriptor.getAttributes().get("fieldName"); + if (StringUtils.isEmpty(property) && !(constraintDescriptor.getAnnotation() instanceof AssertTrue)) { + property = Iterators.getLast(constraintViolation.getPropertyPath().iterator()).toString(); + } + + String error = ""; + if (StringUtils.isNotEmpty(property)) { + error += property + " "; + } + error += constraintViolation.getMessage(); + return error; + } + + private static void initializeValidators() { + HibernateValidatorConfiguration validatorConfiguration = Validation.byProvider(HibernateValidator.class).configure(); + + ConstraintMapping constraintMapping = getCustomConstraintMapping(); + validatorConfiguration.addMapping(constraintMapping); + + fieldsValidator = validatorConfiguration.buildValidatorFactory().getValidator(); + } + + @Bean + public LocalValidatorFactoryBean validatorFactoryBean() { + LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); + localValidatorFactoryBean.setConfigurationInitializer(configuration -> { + ((ConfigurationImpl) configuration).addMapping(getCustomConstraintMapping()); + }); + return localValidatorFactoryBean; + } + + private static ConstraintMapping getCustomConstraintMapping() { + ConstraintMapping constraintMapping = new DefaultConstraintMapping(null); + constraintMapping.constraintDefinition(Length.class).validatedBy(StringLengthValidator.class); + return constraintMapping; + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ShardingThreadPool.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ShardingThreadPool.java new file mode 100644 index 0000000..4d5338b --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ShardingThreadPool.java @@ -0,0 +1,82 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.config; + +import com.google.common.hash.HashFunction; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory; +import sanbing.jcpp.infrastructure.util.trace.TracerRunnable; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; + +import static sanbing.jcpp.infrastructure.util.JCPPHashUtil.forName; +import static sanbing.jcpp.infrastructure.util.JCPPHashUtil.hash; + +/** + * @author baigod + */ +@Component +@Slf4j +public class ShardingThreadPool { + @Value("${thread-pool.sharding.hash_function_name:murmur3_128}") + private String hashFunctionName; + + @Value("${thread-pool.sharding.parallelism:128}") + private int parallelism; + + private HashFunction hashFunction; + + private final Map EXECUTOR_SERVICE_MAP = new ConcurrentHashMap<>(128); + + @PostConstruct + public void init() { + this.hashFunction = forName(hashFunctionName); + } + + @PreDestroy + public void destroy() { + for (ExecutorService executorService : EXECUTOR_SERVICE_MAP.values()) { + executorService.shutdownNow(); + log.info("Sharding Thread [{}] Shutdown completed.", executorService); + } + } + + @Scheduled(fixedDelayString = "${thread-pool.sharding.stats-print-interval-ms:10000}") + public void printStats() { + EXECUTOR_SERVICE_MAP.forEach((k, v) -> { + + ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) v; + + log.info("分区 {}/{} 的线程池中剩余 {} 条待执行任务,当前正在执行的线程数 {}, 已完成任务 {} / {}", + k, + EXECUTOR_SERVICE_MAP.size(), + threadPoolExecutor.getQueue().size(), + threadPoolExecutor.getActiveCount(), + threadPoolExecutor.getCompletedTaskCount(), + threadPoolExecutor.getTaskCount()); + }); + } + + /** + * 提交分片任务 + */ + public void execute(UUID hashKey, TracerRunnable runnable) { + int partition = hash(hashFunction, hashKey); + + EXECUTOR_SERVICE_MAP.computeIfAbsent(partition % parallelism, + p -> Executors.newFixedThreadPool(1, JCPPThreadFactory.forName("sharding-threads-" + p))) + .execute(runnable); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ThreadPoolConfiguration.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ThreadPoolConfiguration.java new file mode 100644 index 0000000..40daa49 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/config/ThreadPoolConfiguration.java @@ -0,0 +1,43 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.config; + +import jakarta.annotation.PreDestroy; +import org.springframework.context.annotation.Configuration; +import sanbing.jcpp.infrastructure.util.async.JCPPExecutors; +import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * @author baigod + */ +@Configuration +public class ThreadPoolConfiguration { + + public static final ExecutorService JCPP_COMMON_THREAD_POOL = JCPPExecutors.newVirtualThreadPool("jcpp-common-virtual"); + + public static final ScheduledExecutorService PROTOCOL_SESSION_SCHEDULED = Executors.newSingleThreadScheduledExecutor(JCPPThreadFactory.forName("protocol-session-schedule")); + + @PreDestroy + public void destroy() { + PROTOCOL_SESSION_SCHEDULED.shutdownNow(); + + JCPP_COMMON_THREAD_POOL.shutdown(); + + try { + if (!JCPP_COMMON_THREAD_POOL.awaitTermination(5, TimeUnit.SECONDS)) { + JCPP_COMMON_THREAD_POOL.shutdownNow(); + } + } catch (InterruptedException e) { + JCPP_COMMON_THREAD_POOL.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/DataValidationException.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/DataValidationException.java new file mode 100644 index 0000000..4fbcdb1 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/DataValidationException.java @@ -0,0 +1,16 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.exception; + +public class DataValidationException extends RuntimeException { + + public DataValidationException(String message) { + super(message); + } + + public DataValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/DownlinkException.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/DownlinkException.java new file mode 100644 index 0000000..02342ad --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/DownlinkException.java @@ -0,0 +1,19 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.exception; + +/** + * @author baigod + */ +public class DownlinkException extends RuntimeException { + + public DownlinkException(String message) { + super(message); + } + + public DownlinkException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/IncorrectParameterException.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/IncorrectParameterException.java new file mode 100644 index 0000000..21af4a2 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/exception/IncorrectParameterException.java @@ -0,0 +1,17 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.exception; + + +public class IncorrectParameterException extends RuntimeException { + + public IncorrectParameterException(String message) { + super(message); + } + + public IncorrectParameterException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/BigNumberSerializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/BigNumberSerializer.java new file mode 100644 index 0000000..3271327 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/BigNumberSerializer.java @@ -0,0 +1,35 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.ser.std.NumberSerializer; + +import java.io.IOException; + +@JacksonStdImpl +public class BigNumberSerializer extends NumberSerializer { + + private static final long JS_NUM_MAX = 9007199254740992L; + private static final long JS_NUM_MIN = -9007199254740992L; + public static final BigNumberSerializer instance = new BigNumberSerializer(Number.class); + + public BigNumberSerializer(Class rawType) { + super(rawType); + } + + @Override + public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException { + long longValue = value.longValue(); + if (longValue >= JS_NUM_MIN && longValue <= JS_NUM_MAX) { + super.serialize(value, gen, provider); + } else { + gen.writeString(value.toString()); + } + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DataTypeModule.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DataTypeModule.java new file mode 100644 index 0000000..3bd9fb3 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DataTypeModule.java @@ -0,0 +1,56 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; + + +/** + * 类型转换 + * + * @author baigod + */ +public class DataTypeModule extends SimpleModule { + public static final DataTypeModule INSTANCE = new DataTypeModule(); + + private DataTypeModule() { + super(DataTypeModule.class.getName()); + + // number + this.addSerializer(Long.class, BigNumberSerializer.instance); + this.addSerializer(Long.TYPE, BigNumberSerializer.instance); + this.addSerializer(BigInteger.class, BigNumberSerializer.instance); + this.addSerializer(BigDecimal.class, BigNumberSerializer.instance); + + // time + this.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE); + this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + this.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE); + this.addSerializer(Instant.class, InstantSerializer.INSTANCE); + this.addSerializer(Date.class, DateSerializer.INSTANCE); + this.addSerializer(java.sql.Date.class, SqlDateSerializer.INSTANCE); + this.addSerializer(Timestamp.class, TimestampSerializer.INSTANCE); + + this.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE); + this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + this.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE); + this.addDeserializer(Instant.class, InstantDeserializer.INSTANCE); + this.addDeserializer(Date.class, DateDeserializer.INSTANCE); + this.addDeserializer(java.sql.Date.class, SqlDateDeserializer.INSTANCE); + this.addDeserializer(Timestamp.class, TimestampDeserializer.INSTANCE); + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DateDeserializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DateDeserializer.java new file mode 100644 index 0000000..5608a28 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DateDeserializer.java @@ -0,0 +1,38 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +/** + * 时间反序列化 + * + * @author baigod + */ +public class DateDeserializer extends JsonDeserializer { + public static final DateDeserializer INSTANCE = new DateDeserializer(); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter DATE_TIME_FORMATTER_MS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + private DateDeserializer() { + } + + @Override + public Date deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + String dateString = p.getText(); + + return dateString.length() > 19 + ? Date.from(LocalDateTime.parse(dateString, DATE_TIME_FORMATTER_MS).atZone(ZoneOffset.systemDefault()).toInstant()) + : Date.from(LocalDateTime.parse(dateString, DATE_TIME_FORMATTER).atZone(ZoneOffset.systemDefault()).toInstant()); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DateSerializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DateSerializer.java new file mode 100644 index 0000000..961d5e5 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/DateSerializer.java @@ -0,0 +1,32 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.apache.commons.lang3.time.FastDateFormat; + +import java.io.IOException; +import java.util.Date; + +/** + * 时间序列化 + * + * @author baigod + */ +public class DateSerializer extends StdSerializer { + public static final DateSerializer INSTANCE = new DateSerializer(); + private static final FastDateFormat FAST_DATE_FORMAT_MS = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss.SSS"); + + private DateSerializer() { + super(Date.class); + } + + @Override + public void serialize(Date value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(FAST_DATE_FORMAT_MS.format(value)); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/InstantDeserializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/InstantDeserializer.java new file mode 100644 index 0000000..8b3ad52 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/InstantDeserializer.java @@ -0,0 +1,41 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import lombok.SneakyThrows; +import org.apache.commons.lang3.time.FastDateFormat; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +/** + * Instant 反序列化 + * + * @author baigod + */ +public class InstantDeserializer extends com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer { + public static final InstantDeserializer INSTANCE = new InstantDeserializer(); + + private final FastDateFormat FAST_DATE_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss"); + private final FastDateFormat FAST_DATE_FORMAT_MS = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss.SSS"); + + private InstantDeserializer() { + super(InstantDeserializer.INSTANT, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); + } + + @SneakyThrows + @Override + public Instant deserialize(JsonParser parser, DeserializationContext context) { + String timestamp = parser.getText(); + + return timestamp.length() > 19 + ? FAST_DATE_FORMAT_MS.parse(timestamp).toInstant() + : FAST_DATE_FORMAT.parse(timestamp).toInstant(); + + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/InstantSerializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/InstantSerializer.java new file mode 100644 index 0000000..11ceb2a --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/InstantSerializer.java @@ -0,0 +1,21 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import java.time.format.DateTimeFormatter; + +/** + * Instant 序列化 + * + * @author baigod + */ +public class InstantSerializer extends com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer { + public static final InstantSerializer INSTANCE = new InstantSerializer(); + + private InstantSerializer() { + super(com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer.INSTANCE, true,false, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/JacksonUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/JacksonUtil.java new file mode 100644 index 0000000..e523eab --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/JacksonUtil.java @@ -0,0 +1,215 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser.Feature; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonWriteFeature; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.util.Arrays; +import java.util.TimeZone; + +/** + * @author baigod + */ +public class JacksonUtil { + + public static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() + .configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) + .configure(Feature.ALLOW_SINGLE_QUOTES, true) + .configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true) + .configure(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS, false) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .defaultTimeZone(TimeZone.getTimeZone("GMT+8")) + .build() + .registerModules(DataTypeModule.INSTANCE); + + public static final ObjectMapper PRETTY_SORTED_JSON_MAPPER = JsonMapper.builder() + .enable(SerializationFeature.INDENT_OUTPUT) + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) + .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) + .configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true) + .serializationInclusion(Include.NON_NULL) + .defaultTimeZone(TimeZone.getTimeZone("GMT+8")) + .build() + .registerModules(DataTypeModule.INSTANCE); + + + public static T convertValue(Object fromValue, Class toValueType) { + try { + return fromValue != null ? OBJECT_MAPPER.convertValue(fromValue, toValueType) : null; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("The given object value: " + + fromValue + " cannot be converted to " + toValueType, e); + } + } + + public static T convertValue(Object fromValue, TypeReference toValueTypeRef) { + try { + return fromValue != null ? OBJECT_MAPPER.convertValue(fromValue, toValueTypeRef) : null; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("The given object value: " + + fromValue + " cannot be converted to " + toValueTypeRef, e); + } + } + + public static T fromString(String string, Class clazz) { + try { + return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null; + } catch (IOException e) { + throw new IllegalArgumentException("The given string value: " + + string + " cannot be transformed to Json object", e); + } + } + + public static T fromString(String string, TypeReference valueTypeRef) { + try { + return string != null ? OBJECT_MAPPER.readValue(string, valueTypeRef) : null; + } catch (IOException e) { + throw new IllegalArgumentException("The given string value: " + + string + " cannot be transformed to Json object", e); + } + } + + public static T fromBytes(byte[] bytes, Class clazz) { + try { + return bytes != null ? OBJECT_MAPPER.readValue(bytes, clazz) : null; + } catch (IOException e) { + throw new IllegalArgumentException("The given string value: " + + Arrays.toString(bytes) + " cannot be transformed to Json object", e); + } + } + + public static JsonNode fromBytes(byte[] bytes) { + try { + return OBJECT_MAPPER.readTree(bytes); + } catch (IOException e) { + throw new IllegalArgumentException("The given byte[] value: " + + Arrays.toString(bytes) + " cannot be transformed to Json object", e); + } + } + + public static String toString(Object value) { + try { + return value != null ? OBJECT_MAPPER.writeValueAsString(value) : null; + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("The given Json object value: " + + value + " cannot be transformed to a String", e); + } + } + + public static String toPrettyString(Object o) { + try { + return PRETTY_SORTED_JSON_MAPPER.writeValueAsString(o); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public static T treeToValue(JsonNode node, Class clazz) { + try { + return OBJECT_MAPPER.treeToValue(node, clazz); + } catch (IOException e) { + throw new IllegalArgumentException("Can't convert value: " + node.toString(), e); + } + } + + public static JsonNode toJsonNode(String value) { + return toJsonNode(value, OBJECT_MAPPER); + } + + public static JsonNode toJsonNode(String value, ObjectMapper mapper) { + if (value == null || value.isEmpty()) { + return null; + } + try { + return mapper.readTree(value); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + public static ObjectNode newObjectNode() { + return newObjectNode(OBJECT_MAPPER); + } + + public static ObjectNode newObjectNode(ObjectMapper mapper) { + return mapper.createObjectNode(); + } + + public static ArrayNode newArrayNode() { + return newArrayNode(OBJECT_MAPPER); + } + + public static ArrayNode newArrayNode(ObjectMapper mapper) { + return mapper.createArrayNode(); + } + + public static T clone(T value) { + @SuppressWarnings("unchecked") + Class valueClass = (Class) value.getClass(); + return fromString(toString(value), valueClass); + } + + public static JsonNode valueToTree(T value) { + return OBJECT_MAPPER.valueToTree(value); + } + + public static byte[] writeValueAsBytes(T value) { + try { + return OBJECT_MAPPER.writeValueAsBytes(value); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("The given Json object value: " + + value + " cannot be transformed to a String", e); + } + } + + public static JsonNode getSafely(JsonNode node, String... path) { + if (node == null) { + return null; + } + for (String p : path) { + if (!node.has(p)) { + return null; + } else { + node = node.get(p); + } + } + return node; + } + + /** + * 合并两个ObjectNode. + * 如果存在相同的字段,优先保留第二个ObjectNode中的值。 + * + * @param node1 the first ObjectNode + * @param node2 the second ObjectNode + * @return 合并后的结果 + */ + public static ObjectNode merge(ObjectNode node1, ObjectNode node2) { + ObjectNode mergedNode = OBJECT_MAPPER.createObjectNode(); + + // 把第一个节点的所有字段添加到mergedNode中 + node1.fields().forEachRemaining(entry -> { + mergedNode.set(entry.getKey(), entry.getValue()); + }); + + // 把第二个节点的所有字段添加到mergedNode中,覆盖相同字段 + node2.fields().forEachRemaining(entry -> { + mergedNode.set(entry.getKey(), entry.getValue()); + }); + + return mergedNode; + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalDateTimeDeserializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalDateTimeDeserializer.java new file mode 100644 index 0000000..95f5d03 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalDateTimeDeserializer.java @@ -0,0 +1,39 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * LocalDateTime类型反序列化 + * 需要用到的字段上加 @JsonDeserialize(using = LocalDateTimeDeserializer.class) + */ +public class LocalDateTimeDeserializer extends StdDeserializer { + public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer(); + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter DATE_TIME_FORMATTER_MS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + private LocalDateTimeDeserializer() { + super(LocalDateTime.class); + } + + @Override + public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext context) + throws IOException { + + String dateString = jsonParser.getText(); + return dateString.length() > 19 + ? LocalDateTime.parse(dateString, DATE_TIME_FORMATTER_MS) + : LocalDateTime.parse(dateString, DATE_TIME_FORMATTER); + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalDateTimeSerializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalDateTimeSerializer.java new file mode 100644 index 0000000..5c7d790 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalDateTimeSerializer.java @@ -0,0 +1,32 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 时间类型序列化工具 + * + * @author baigod + */ +public class LocalDateTimeSerializer extends JsonSerializer { + public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer(); + + private static final DateTimeFormatter DATE_TIME_FORMATTER_MS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + private LocalDateTimeSerializer() { + } + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.format(DATE_TIME_FORMATTER_MS)); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalTimeDeserializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalTimeDeserializer.java new file mode 100644 index 0000000..c4e8f1e --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalTimeDeserializer.java @@ -0,0 +1,42 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * LocalDateTime类型反序列化 + * 需要用到的字段上加 @JsonDeserialize(using = EnergyLocalTimeDeserializer.class) + */ +public class LocalTimeDeserializer extends StdDeserializer { + public static final LocalTimeDeserializer INSTANCE = new LocalTimeDeserializer(); + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); + private static final DateTimeFormatter DATE_TIME_FORMATTER_MS = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + + + private LocalTimeDeserializer() { + super(LocalDateTime.class); + } + + @Override + public LocalTime deserialize(JsonParser jsonParser, DeserializationContext context) + throws IOException { + + String dateString = jsonParser.getText(); + return dateString.length() > 8 + ? LocalTime.parse(dateString, DATE_TIME_FORMATTER_MS) + : LocalTime.parse(dateString, DATE_TIME_FORMATTER); + + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalTimeSerializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalTimeSerializer.java new file mode 100644 index 0000000..3fde5cc --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LocalTimeSerializer.java @@ -0,0 +1,32 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * 时间类型序列化工具 + * + * @author baigod + */ +public class LocalTimeSerializer extends JsonSerializer { + public static final LocalTimeSerializer INSTANCE = new LocalTimeSerializer(); + + private LocalTimeSerializer() { + } + + private static final DateTimeFormatter DATE_TIME_FORMATTER_MS = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + + @Override + public void serialize(LocalTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.format(DATE_TIME_FORMATTER_MS)); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LongTimestampDeserializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LongTimestampDeserializer.java new file mode 100644 index 0000000..a60a19c --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/LongTimestampDeserializer.java @@ -0,0 +1,33 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 13位时间戳反序列化器 + * @author baigod + */ +public class LongTimestampDeserializer extends JsonDeserializer { + + @Override + public Long deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + + // 判定是否是long类型 + if ("LONG".equals(jsonParser.getNumberType().name())) { + return jsonParser.getLongValue(); + } + LocalDateTime localDateTime = LocalDateTime.parse(jsonParser.getValueAsString().replace(" ", "T").replace("Z", "")); + ZoneId systemDefaultZone = ZoneId.systemDefault(); + return localDateTime.atZone(systemDefaultZone).toInstant().toEpochMilli(); + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/SqlDateDeserializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/SqlDateDeserializer.java new file mode 100644 index 0000000..5b891cf --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/SqlDateDeserializer.java @@ -0,0 +1,39 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.sql.Date; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +/** + * sqlDate 反序列化 + * + * @author baigod + */ +public class SqlDateDeserializer extends JsonDeserializer { + + public static final SqlDateDeserializer INSTANCE = new SqlDateDeserializer(); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter DATE_TIME_FORMATTER_MS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + private SqlDateDeserializer() { + } + + @Override + public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String dateString = p.getText(); + + return dateString.length() > 19 + ? new Date(LocalDateTime.parse(dateString, DATE_TIME_FORMATTER_MS).atZone(ZoneOffset.systemDefault()).toInstant().toEpochMilli()) + : new Date(LocalDateTime.parse(dateString, DATE_TIME_FORMATTER).atZone(ZoneOffset.systemDefault()).toInstant().toEpochMilli()); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/SqlDateSerializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/SqlDateSerializer.java new file mode 100644 index 0000000..0749d53 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/SqlDateSerializer.java @@ -0,0 +1,33 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.apache.commons.lang3.time.FastDateFormat; + +import java.io.IOException; +import java.sql.Date; + +/** + * sqlDate序列化 + * + * @author baigod + */ +public class SqlDateSerializer extends StdSerializer { + public static final SqlDateSerializer INSTANCE = new SqlDateSerializer(); + + private static final FastDateFormat FAST_DATE_FORMAT_MS = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss.SSS"); + + private SqlDateSerializer() { + super(Date.class); + } + + @Override + public void serialize(Date value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(FAST_DATE_FORMAT_MS.format(value)); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/TimestampDeserializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/TimestampDeserializer.java new file mode 100644 index 0000000..4ce6b8a --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/TimestampDeserializer.java @@ -0,0 +1,38 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +/** + * timestamp 反序列化 + * @author baigod + */ +public class TimestampDeserializer extends JsonDeserializer { + + public static final TimestampDeserializer INSTANCE = new TimestampDeserializer(); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter DATE_TIME_FORMATTER_MS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + private TimestampDeserializer() { + } + + @Override + public Timestamp deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + String dateString = p.getText(); + + return dateString.length() > 19 + ? Timestamp.from(LocalDateTime.parse(dateString, DATE_TIME_FORMATTER_MS).atZone(ZoneOffset.systemDefault()).toInstant()) + : Timestamp.from(LocalDateTime.parse(dateString, DATE_TIME_FORMATTER).atZone(ZoneOffset.systemDefault()).toInstant()); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/TimestampSerializer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/TimestampSerializer.java new file mode 100644 index 0000000..fcf9fcb --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/jackson/TimestampSerializer.java @@ -0,0 +1,33 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.apache.commons.lang3.time.FastDateFormat; + +import java.io.IOException; +import java.sql.Timestamp; + +/** + * timestamp 序列化 + * + * @author baigod + */ +public class TimestampSerializer extends StdSerializer { + public static final TimestampSerializer INSTANCE = new TimestampSerializer(); + + private static final FastDateFormat FAST_DATE_FORMAT_MS = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss.SSS"); + + private TimestampSerializer() { + super(Timestamp.class); + } + + @Override + public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(FAST_DATE_FORMAT_MS.format(value)); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/mdc/MDCUtils.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/mdc/MDCUtils.java new file mode 100644 index 0000000..7f22912 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/mdc/MDCUtils.java @@ -0,0 +1,43 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.mdc; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.MDC; +import sanbing.jcpp.infrastructure.util.trace.Tracer; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; + + +public class MDCUtils { + + private static final String TRACE_ID = "TRACE_ID"; + + public static String putIfAbsentTracer() { + String traceId = MDC.get(TRACE_ID); + + if (StringUtils.isEmpty(traceId)) { + return recordTracer(); + } + + return traceId; + } + + public static String recordTracer() { + Tracer tracer = TracerContextUtil.getCurrentTracer(); + + if (!StringUtils.isEmpty(tracer.getTraceId())) { + MDC.put(TRACE_ID, tracer.getTraceId()); + } else { + MDC.remove(TRACE_ID); + } + + return tracer.getTraceId(); + + } + + public static void cleanTracer() { + MDC.remove(TRACE_ID); + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/property/JCPPProperty.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/property/JCPPProperty.java new file mode 100644 index 0000000..a3c0903 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/property/JCPPProperty.java @@ -0,0 +1,14 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.property; + +import lombok.Data; + +@Data +public class JCPPProperty { + + private String key; + private String value; +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/property/PropertyUtils.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/property/PropertyUtils.java new file mode 100644 index 0000000..bf3ed82 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/property/PropertyUtils.java @@ -0,0 +1,44 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.property; + + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public class PropertyUtils { + + public static Map getProps(String properties) { + Map configs = new HashMap<>(); + if (StringUtils.isNotEmpty(properties)) { + for (String property : properties.split(";")) { + if (StringUtils.isNotEmpty(property)) { + int delimiterPosition = property.indexOf(":"); + String key = property.substring(0, delimiterPosition); + String value = property.substring(delimiterPosition + 1); + configs.put(key, value); + } + } + } + return configs; + } + + public static Map getProps(Map defaultProperties, String propertiesStr) { + return getProps(defaultProperties, propertiesStr, PropertyUtils::getProps); + } + + public static Map getProps(Map defaultProperties, String propertiesStr, Function> parser) { + Map properties = defaultProperties; + if (StringUtils.isNotBlank(propertiesStr)) { + properties = new HashMap<>(properties); + properties.putAll(parser.apply(propertiesStr)); + } + return properties; + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TraceIdGenerator.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TraceIdGenerator.java new file mode 100644 index 0000000..6baf94a --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TraceIdGenerator.java @@ -0,0 +1,69 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.trace; + +import java.net.InetAddress; +import java.util.concurrent.atomic.AtomicInteger; + +public class TraceIdGenerator { + + //127.0.0.1 + private static String IP_16 = "7F000001"; + + private static final int MAX_COUNT_INDEX = 9000; + + private static final AtomicInteger COUNT = new AtomicInteger(1000); + + static { + try { + String ipAddress = InetAddress.getLocalHost().getHostAddress(); + if (ipAddress != null) { + IP_16 = getIP_16(ipAddress); + } + } catch (Throwable ignored) { + } + } + + public static String generate() { + return getTraceId(IP_16, System.currentTimeMillis(), getNextId()); + } + + private static String getIP_16(String ip) { + String[] ips = ip.split("\\."); + StringBuilder sb = new StringBuilder(); + for (String column : ips) { + String hex = Integer.toHexString(Integer.parseInt(column)).toUpperCase(); + if (hex.length() == 1) { + sb.append('0').append(hex); + } else { + sb.append(hex); + } + + } + return sb.toString(); + } + + private static String getTraceId(String ip, long timestamp, String nextId) { + return ip + timestamp + nextId; + } + + private static String getNextId() { + + int count = COUNT.incrementAndGet(); + + if (count > 9000) { + synchronized (TraceIdGenerator.class) { + if (COUNT.get() > MAX_COUNT_INDEX) { + COUNT.set(1000); + } + } + + return String.valueOf(COUNT.incrementAndGet()); + } else { + return String.valueOf(count); + } + + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/Tracer.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/Tracer.java new file mode 100644 index 0000000..e5a3105 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/Tracer.java @@ -0,0 +1,38 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.trace; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class Tracer implements Serializable { + + private String traceId; + + private String origin; + + private final long tracerTs; + + public Tracer(String traceId, String origin) { + this.traceId = traceId; + this.origin = origin; + this.tracerTs = System.currentTimeMillis(); + } + + public Tracer(String traceId, String origin, long tracerTs) { + this.traceId = traceId; + this.origin = origin; + this.tracerTs = tracerTs; + } + + public Tracer(String traceId, long tracerTs) { + this.traceId = traceId; + this.origin = "JCPP"; + this.tracerTs = tracerTs; + + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerCallable.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerCallable.java new file mode 100644 index 0000000..9978493 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerCallable.java @@ -0,0 +1,41 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.trace; + + +import sanbing.jcpp.infrastructure.util.mdc.MDCUtils; + +import java.util.concurrent.Callable; + +public class TracerCallable implements Callable { + + private Tracer tracer; + private final Callable callable; + + public TracerCallable(Callable callable) { + this.tracer = TracerContextUtil.getCurrentTracer(); + this.callable = callable; + } + + @Override + public T call() throws Exception { + try { + if (this.tracer != null) { + TracerContextUtil.newTracer(tracer.getTraceId(), tracer.getOrigin(), tracer.getTracerTs()); + + MDCUtils.recordTracer(); + } + + return this.callable.call(); + } finally { + TracerContextUtil.cleanTracer(); + + MDCUtils.cleanTracer(); + + this.tracer = null; + } + } + +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerContextUtil.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerContextUtil.java new file mode 100644 index 0000000..5921c7c --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerContextUtil.java @@ -0,0 +1,71 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.trace; + +import org.apache.commons.lang3.StringUtils; + +/** + * Tracer上下文工具类 + */ +public class TracerContextUtil { + + public static final String JCPP_TRACER_ID = "jcpp_tracer_id"; + public static final String JCPP_TRACER_ORIGIN = "jcpp_tracer_origin"; + public static final String JCPP_TRACER_TS = "jcpp_tracer_ts"; + + private static final ThreadLocal TRACE_ID_CONTAINER = new ThreadLocal<>(); + + public static Tracer newTracer(String traceId, String origin) { + Tracer tracer; + + if (StringUtils.isEmpty(traceId)) { + tracer = new Tracer(TraceIdGenerator.generate(), origin); + } else { + tracer = new Tracer(traceId, origin); + } + + TRACE_ID_CONTAINER.set(tracer); + + return tracer; + } + + public static Tracer newTracer(String traceId, String origin, long ts) { + final Tracer tracer; + + if (StringUtils.isEmpty(traceId)) { + tracer = new Tracer(TraceIdGenerator.generate(), origin, ts); + } else { + tracer = new Tracer(traceId, origin, ts); + } + + + TRACE_ID_CONTAINER.set(tracer); + + return tracer; + } + + public static Tracer newTracer(String origin) { + return newTracer(TraceIdGenerator.generate(), origin); + } + + public static Tracer newTracer() { + return newTracer(TraceIdGenerator.generate(), null); + } + + public static Tracer getCurrentTracer() { + Tracer tracer = TRACE_ID_CONTAINER.get(); + + if (tracer == null) { + return newTracer(); + } + + return tracer; + } + + public static void cleanTracer() { + TRACE_ID_CONTAINER.remove(); + } + +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerRunnable.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerRunnable.java new file mode 100644 index 0000000..779daa4 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/trace/TracerRunnable.java @@ -0,0 +1,38 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.trace; + + +import sanbing.jcpp.infrastructure.util.mdc.MDCUtils; + +public class TracerRunnable implements Runnable { + + private Tracer tracer; + private final Runnable runnable; + + public TracerRunnable(Runnable runnable) { + this.tracer = TracerContextUtil.getCurrentTracer(); + this.runnable = runnable; + } + + @Override + public void run() { + try { + if (this.tracer != null) { + TracerContextUtil.newTracer(tracer.getTraceId(), tracer.getOrigin(), tracer.getTracerTs()); + + MDCUtils.recordTracer(); + } + + this.runnable.run(); + } finally { + TracerContextUtil.cleanTracer(); + + MDCUtils.cleanTracer(); + + this.tracer = null; + } + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/Length.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/Length.java new file mode 100644 index 0000000..ae32266 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/Length.java @@ -0,0 +1,28 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +@Constraint(validatedBy = {}) +public @interface Length { + String message() default "length must be equal or less than {max}"; + + String fieldName() default ""; + + int max() default 255; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/StringLengthValidator.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/StringLengthValidator.java new file mode 100644 index 0000000..98948d6 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/StringLengthValidator.java @@ -0,0 +1,35 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +@Slf4j +public class StringLengthValidator implements ConstraintValidator { + private int max; + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + String stringValue; + if (value instanceof CharSequence || value instanceof JsonNode) { + stringValue = value.toString(); + } else { + return true; + } + if (StringUtils.isEmpty(stringValue)) { + return true; + } + return stringValue.length() <= max; + } + + @Override + public void initialize(Length constraintAnnotation) { + this.max = constraintAnnotation.max(); + } +} diff --git a/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/Validator.java b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/Validator.java new file mode 100644 index 0000000..a13aab0 --- /dev/null +++ b/jcpp-infrastructure-util/src/main/java/sanbing/jcpp/infrastructure/util/validation/Validator.java @@ -0,0 +1,55 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.validation; + +import sanbing.jcpp.infrastructure.util.exception.IncorrectParameterException; + +import java.util.UUID; +import java.util.function.Function; + +public class Validator { + + + public static void validateString(String val, String errorMessage) { + if (val == null || val.isEmpty()) { + throw new IncorrectParameterException(errorMessage); + } + } + + + public static void validateString(String val, Function errorMessageFunction) { + if (val == null || val.isEmpty()) { + throw new IncorrectParameterException(errorMessageFunction.apply(val)); + } + } + + public static void validatePositiveNumber(long val, String errorMessage) { + if (val <= 0) { + throw new IncorrectParameterException(errorMessage); + } + } + + @Deprecated + public static void validateId(UUID id, String errorMessage) { + if (id == null) { + throw new IncorrectParameterException(errorMessage); + } + } + + public static void validateId(UUID id, Function errorMessageFunction) { + if (id == null) { + throw new IncorrectParameterException(errorMessageFunction.apply(id)); + } + } + + + public static void checkNotNull(Object reference, String errorMessage) { + if (reference == null) { + throw new IncorrectParameterException(errorMessage); + } + } + + +} diff --git a/jcpp-infrastructure-util/src/test/java/sanbing/jcpp/infrastructure/util/codec/BCDUtilTest.java b/jcpp-infrastructure-util/src/test/java/sanbing/jcpp/infrastructure/util/codec/BCDUtilTest.java new file mode 100644 index 0000000..195da2c --- /dev/null +++ b/jcpp-infrastructure-util/src/test/java/sanbing/jcpp/infrastructure/util/codec/BCDUtilTest.java @@ -0,0 +1,28 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.codec; + +import cn.hutool.core.util.HexUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class BCDUtilTest { + + @Test + void toBytesTest() { + String pileCodeHex = "20231212000010"; + + byte[] bytes = HexUtil.decodeHex(pileCodeHex); + + String pileCode = BCDUtil.toString(bytes); + + assert pileCodeHex.equals(pileCode); + + byte[] pileCodeBytes = BCDUtil.toBytes(pileCodeHex); + + assertArrayEquals(pileCodeBytes, bytes); + } +} \ No newline at end of file diff --git a/jcpp-infrastructure-util/src/test/java/sanbing/jcpp/infrastructure/util/codec/CP56Time2aUtilTest.java b/jcpp-infrastructure-util/src/test/java/sanbing/jcpp/infrastructure/util/codec/CP56Time2aUtilTest.java new file mode 100644 index 0000000..6542f33 --- /dev/null +++ b/jcpp-infrastructure-util/src/test/java/sanbing/jcpp/infrastructure/util/codec/CP56Time2aUtilTest.java @@ -0,0 +1,27 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.infrastructure.util.codec; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; + +class CP56Time2aUtilTest { + + @Test + void encodeTest() { + Instant time = Instant.ofEpochMilli(1727798453000L); + + byte[] bytes = CP56Time2aUtil.encode(time); + + System.out.println(Arrays.toString(bytes)); + + Instant decode = CP56Time2aUtil.decode(bytes); + + assert time.equals(decode); + } + +} \ No newline at end of file diff --git a/jcpp-protocol-api/pom.xml b/jcpp-protocol-api/pom.xml new file mode 100644 index 0000000..6935c02 --- /dev/null +++ b/jcpp-protocol-api/pom.xml @@ -0,0 +1,50 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-protocol-api + jar + JChargePointProtocol Protocol Api Module + 协议API + + + ${basedir}/.. + + + + + sanbing + jcpp-infrastructure-util + + + sanbing + jcpp-infrastructure-queue + + + org.springframework.boot + spring-boot-starter-reactor-netty + + + com.github.ben-manes.caffeine + caffeine + + + + + + + diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolBootstrap.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolBootstrap.java new file mode 100644 index 0000000..6a8ffd6 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolBootstrap.java @@ -0,0 +1,125 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import sanbing.jcpp.protocol.cfg.ForwarderCfg; +import sanbing.jcpp.protocol.cfg.ProtocolCfg; +import sanbing.jcpp.protocol.cfg.TcpCfg; +import sanbing.jcpp.protocol.cfg.enums.ForwarderType; +import sanbing.jcpp.protocol.forwarder.Forwarder; +import sanbing.jcpp.protocol.forwarder.KafkaForwarder; +import sanbing.jcpp.protocol.forwarder.MemoryForwarder; +import sanbing.jcpp.protocol.listener.Listener; +import sanbing.jcpp.protocol.listener.tcp.TcpListener; + +import static org.springframework.boot.actuate.health.Status.UP; + +/** + * @author baigod + */ +@Slf4j +public abstract class ProtocolBootstrap implements HealthIndicator { + + @Resource + protected ProtocolContext protocolContext; + + protected ProtocolCfg protocolCfg; + + protected Listener listener; + + protected Forwarder forwarder; + + @PostConstruct + public void init() throws InterruptedException { + String protocolName = getProtocolName(); + + log.info("Protocol Service [{}] Initializing...", protocolName); + + protocolCfg = protocolContext.getProtocolsConfigProvider().loadConfig(protocolName); + + ForwarderCfg forwarderCfg = protocolCfg.getForwarder(); + + if (protocolContext.getServiceInfoProvider().isMonolith() && forwarderCfg.getType() == ForwarderType.memory) { + + forwarder = new MemoryForwarder(getProtocolName(), forwarderCfg, + protocolContext.getStatsFactory(), + protocolContext.getAppQueueFactory(), + protocolContext.getPartitionProvider(), + protocolContext.getServiceInfoProvider()); + + } else if (forwarderCfg.getType() == ForwarderType.kafka) { + + forwarder = new KafkaForwarder(getProtocolName(), forwarderCfg, + protocolContext.getStatsFactory(), + protocolContext.getAppQueueFactory(), + protocolContext.getPartitionProvider(), + protocolContext.getServiceInfoProvider()); + } else { + throw new IllegalArgumentException("Unknown Forwarder type: " + forwarderCfg.getType()); + } + + TcpCfg tcpCfg = protocolCfg.getListener().getTcp(); + + if (tcpCfg != null) { + + listener = new TcpListener<>(protocolName, tcpCfg, messageProcessor(), protocolContext.getStatsFactory()); + } + + _init(); + } + + @PreDestroy + public void destroy() throws InterruptedException { + log.info("{} destroy...", getProtocolName()); + + if (listener != null) { + listener.destroy(); + } + + if (forwarder != null) { + forwarder.destroy(); + } + + _destroy(); + } + + + @Override + public Health health() { + Health.Builder healthBuilder; + + if (listener != null && listener.health().getStatus() == UP && forwarder != null && forwarder.health().getStatus() == UP) { + healthBuilder = Health.up(); + } else { + healthBuilder = Health.down(); + } + + if (listener != null) { + healthBuilder.withDetail("listener", listener.health().getStatus()); + } + + if (forwarder != null) { + healthBuilder.withDetail("forwarder", forwarder.health().getStatus()); + } + + return healthBuilder.build(); + } + + + protected abstract String getProtocolName(); + + protected abstract void _init(); + + protected abstract void _destroy(); + + protected abstract ProtocolMessageProcessor messageProcessor(); + +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolContext.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolContext.java new file mode 100644 index 0000000..0bad837 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolContext.java @@ -0,0 +1,64 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol; + +import io.netty.util.ResourceLeakDetector; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider; +import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider; +import sanbing.jcpp.infrastructure.queue.provider.AppQueueFactory; +import sanbing.jcpp.infrastructure.stats.StatsFactory; +import sanbing.jcpp.infrastructure.util.config.ShardingThreadPool; +import sanbing.jcpp.protocol.provider.ProtocolSessionRegistryProvider; +import sanbing.jcpp.protocol.provider.ProtocolsConfigProvider; + +/** + * @author baigod + */ +@Component +@Getter +@Slf4j +public class ProtocolContext { + + private final StatsFactory statsFactory; + + private final ProtocolsConfigProvider protocolsConfigProvider; + + private final ProtocolSessionRegistryProvider protocolSessionRegistryProvider; + + private final ServiceInfoProvider serviceInfoProvider; + + private final PartitionProvider partitionProvider; + + private final AppQueueFactory appQueueFactory; + + private final ShardingThreadPool shardingThreadPool; + + public ProtocolContext(StatsFactory statsFactory, + ProtocolsConfigProvider protocolsConfigProvider, + ProtocolSessionRegistryProvider protocolSessionRegistryProvider, + ServiceInfoProvider serviceInfoProvider, + @Autowired(required = false) PartitionProvider partitionProvider, + @Autowired(required = false) AppQueueFactory appQueueFactory, + ShardingThreadPool shardingThreadPool) { + this.statsFactory = statsFactory; + this.protocolsConfigProvider = protocolsConfigProvider; + this.protocolSessionRegistryProvider = protocolSessionRegistryProvider; + this.serviceInfoProvider = serviceInfoProvider; + this.partitionProvider = partitionProvider; + this.appQueueFactory = appQueueFactory; + this.shardingThreadPool = shardingThreadPool; + } + + @PostConstruct + public void init() { + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED); + log.info("Setting resource leak detector level to {}", ResourceLeakDetector.Level.DISABLED); + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolMessageProcessor.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolMessageProcessor.java new file mode 100644 index 0000000..848cfd3 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/ProtocolMessageProcessor.java @@ -0,0 +1,66 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol; + +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.stats.MessagesStats; +import sanbing.jcpp.infrastructure.util.exception.DownlinkException; +import sanbing.jcpp.infrastructure.util.trace.TracerRunnable; +import sanbing.jcpp.protocol.domain.ListenerToHandlerMsg; +import sanbing.jcpp.protocol.domain.SessionToHandlerMsg; +import sanbing.jcpp.protocol.forwarder.Forwarder; + +import java.util.UUID; + +/** + * @author baigod + */ +@Slf4j +public abstract class ProtocolMessageProcessor { + protected final Forwarder forwarder; + protected final ProtocolContext protocolContext; + + protected ProtocolMessageProcessor(Forwarder forwarder, ProtocolContext protocolContext) { + this.forwarder = forwarder; + this.protocolContext = protocolContext; + } + + public void uplinkHandleAsync(ListenerToHandlerMsg listenerToHandlerMsg, MessagesStats uplinkMsgStats) { + + UUID id = listenerToHandlerMsg.session().getId(); + + protocolContext.getShardingThreadPool().execute(id, new TracerRunnable(() -> { + try { + + listenerToHandlerMsg.session().setForwarder(forwarder); + + uplinkHandle(listenerToHandlerMsg); + + } catch (Exception e) { + + uplinkMsgStats.incrementFailed(); + + log.error("{} 消息处理器处理报文异常", listenerToHandlerMsg.session(), e); + } + })); + } + + protected abstract void uplinkHandle(ListenerToHandlerMsg listenerToHandlerMsg) throws Exception; + + public void downlinkHandle(SessionToHandlerMsg sessionToHandlerMsg, MessagesStats downlinkMsgStats) throws DownlinkException { + try { + + downlinkHandle(sessionToHandlerMsg); + + } catch (Exception e) { + + downlinkMsgStats.incrementFailed(); + + throw new DownlinkException(e.getMessage(), e); + } + } + + protected abstract void downlinkHandle(SessionToHandlerMsg sessionToHandlerMsg) throws Exception; +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/DownlinkController.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/DownlinkController.java new file mode 100644 index 0000000..94b6906 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/DownlinkController.java @@ -0,0 +1,78 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.adapter; + +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; +import sanbing.jcpp.protocol.domain.ProtocolSession; +import sanbing.jcpp.protocol.provider.ProtocolSessionRegistryProvider; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * @author baigod + */ +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class DownlinkController { + + @Value("${api.timeout.onDownlink:3000}") + public long onDownlinkTimeout; + + + @Resource + ProtocolSessionRegistryProvider protocolSessionRegistryProvider; + + @PostMapping(value = "/onDownlink", consumes = "application/x-protobuf", produces = "application/x-protobuf") + public DeferredResult> onDownlink(@RequestBody DownlinkRestMessage downlinkMsg) { + log.info("收到REST下行请求 {}", downlinkMsg); + + final DeferredResult> response = new DeferredResult<>(onDownlinkTimeout, + ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).build()); + + UUID protocolSessionId = new UUID(downlinkMsg.getSessionIdMSB(),downlinkMsg.getSessionIdLSB()) ; + + CompletableFuture protocolSessionCompletableFuture = protocolSessionRegistryProvider.get(protocolSessionId); + + protocolSessionCompletableFuture.thenAccept(session -> { + if (session != null) { + + session.onDownlink(downlinkMsg); + + response.setResult(ResponseEntity.status(HttpStatus.OK).build()); + } else { + + log.warn("下发报文时Session未找到 sessionId: {}", protocolSessionId); + + response.setResult(ResponseEntity.status(HttpStatus.NOT_FOUND).body("Protocol Session not found for ID:" + protocolSessionId)); + } + }).whenComplete((unused, throwable) -> { + if (throwable != null) { + + log.warn("下发报文时处理失败 sessionId: {}", protocolSessionId, throwable); + + if (!response.isSetOrExpired()) { + + response.setResult(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(throwable.getMessage())); + } + } + }); + + return response; + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/TracerInterceptor.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/TracerInterceptor.java new file mode 100644 index 0000000..90a68c3 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/TracerInterceptor.java @@ -0,0 +1,41 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.adapter.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import sanbing.jcpp.infrastructure.util.mdc.MDCUtils; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; + +import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.*; + +@Component +public class TracerInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String tracerId = request.getHeader(JCPP_TRACER_ID); + String tracerOrigin = request.getHeader(JCPP_TRACER_ORIGIN); + String tracerTsStr = request.getHeader(JCPP_TRACER_TS); + + long tracerTs; + if (tracerTsStr != null) { + try { + tracerTs = Long.parseLong(tracerTsStr); + } catch (NumberFormatException e) { + tracerTs = System.currentTimeMillis(); + } + } else { + tracerTs = System.currentTimeMillis(); + } + + TracerContextUtil.newTracer(tracerId, tracerOrigin, tracerTs); + MDCUtils.recordTracer(); + + return true; + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/UndertowServletWebServerCustomizer.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/UndertowServletWebServerCustomizer.java new file mode 100644 index 0000000..21830f8 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/UndertowServletWebServerCustomizer.java @@ -0,0 +1,24 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.adapter.config; + +import io.undertow.server.DefaultByteBufferPool; +import io.undertow.websockets.jsr.WebSocketDeploymentInfo; +import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + + +@Component +public class UndertowServletWebServerCustomizer implements WebServerFactoryCustomizer { + @Override + public void customize(UndertowServletWebServerFactory factory) { + factory.addDeploymentInfoCustomizers(deploymentInfo -> { + WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo(); + webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 128 * 1024 * 1024)); + deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo); + }); + } +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/WebMvcConfiguration.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/WebMvcConfiguration.java new file mode 100644 index 0000000..94e4f3a --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/adapter/config/WebMvcConfiguration.java @@ -0,0 +1,45 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.adapter.config; + +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +@Configuration +public class WebMvcConfiguration implements WebMvcConfigurer { + + @Resource + private TracerInterceptor tracerInterceptor; + + @Override + public void configureMessageConverters(List> converters) { + for (HttpMessageConverter converter : converters) { + if (converter instanceof StringHttpMessageConverter) { + ((StringHttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8); + } + + if (converter instanceof MappingJackson2HttpMessageConverter) { + ((MappingJackson2HttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8); + } + } + + // protobuf 序列化 + converters.add( new ProtobufHttpMessageConverter()); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(tracerInterceptor).addPathPatterns("/**"); + } +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ForwarderCfg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ForwarderCfg.java new file mode 100644 index 0000000..b6feb8e --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ForwarderCfg.java @@ -0,0 +1,24 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.cfg; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import sanbing.jcpp.protocol.cfg.enums.ForwarderType; + +@Setter +@Getter +public class ForwarderCfg { + + @NotNull + private ForwarderType type; + + private MemoryCfg memory; + + @Valid + private KafkaCfg kafka; +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/KafkaCfg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/KafkaCfg.java new file mode 100644 index 0000000..6b9885e --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/KafkaCfg.java @@ -0,0 +1,49 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.cfg; + +import lombok.Getter; +import lombok.Setter; +import sanbing.jcpp.infrastructure.util.property.PropertyUtils; + +import java.util.Map; + +@Getter +@Setter +public class KafkaCfg { + + private String topic; + + private boolean jcppPartition; + + private String bootstrapServers; + + private String acks; + + private EncoderType encoder; + + private int retries; + + private String compressionType; // none, gzip, snappy, lz4, zstd + + private int batchSize; + + private int lingerMs; + + private long bufferMemory; + + private Map otherProperties; // Other inline properties if necessary + + private String topicProperties; + + public void setOtherProperties(String otherProperties) { + this.otherProperties = PropertyUtils.getProps(otherProperties); + } + + public enum EncoderType { + protobuf, + json + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ListenerCfg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ListenerCfg.java new file mode 100644 index 0000000..7baf6b6 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ListenerCfg.java @@ -0,0 +1,17 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.cfg; + +import jakarta.validation.Valid; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ListenerCfg { + + @Valid + private TcpCfg tcp; +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/MemoryCfg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/MemoryCfg.java new file mode 100644 index 0000000..203f474 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/MemoryCfg.java @@ -0,0 +1,18 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.cfg; + +import lombok.Getter; +import lombok.Setter; + +/** + * @author baigod + */ +@Getter +@Setter +public class MemoryCfg { + + private String topic; +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ProtocolCfg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ProtocolCfg.java new file mode 100644 index 0000000..f482522 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/ProtocolCfg.java @@ -0,0 +1,25 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.cfg; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ProtocolCfg { + + private boolean enabled; + + @NotNull + @Valid + private ListenerCfg listener; + + @NotNull + @Valid + private ForwarderCfg forwarder; +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/TcpCfg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/TcpCfg.java new file mode 100644 index 0000000..7312c82 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/TcpCfg.java @@ -0,0 +1,45 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.cfg; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TcpCfg { + + private String bindAddress; + + @Max(65000) + private int bindPort; + + @Min(1) + private int bossGroupThreadCount; + + @Min(1) + private int workerGroupThreadCount; + + private boolean soKeepAlive; + + @Min(1) + @Max(65500) + private int soBacklog; + + @Min(1) + private int soRcvbuf; + + @Min(1) + private int soSndbuf; + + private boolean nodelay; + + @Valid + private TcpHandlerCfg handler; + +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/TcpHandlerCfg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/TcpHandlerCfg.java new file mode 100644 index 0000000..d752a46 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/TcpHandlerCfg.java @@ -0,0 +1,55 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.cfg; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.validation.constraints.Min; +import lombok.Getter; +import lombok.Setter; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.property.PropertyUtils; +import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType; +import sanbing.jcpp.protocol.listener.tcp.configs.BinaryHandlerConfiguration; +import sanbing.jcpp.protocol.listener.tcp.configs.HandlerConfiguration; +import sanbing.jcpp.protocol.listener.tcp.configs.JsonHandlerConfiguration; +import sanbing.jcpp.protocol.listener.tcp.configs.TextHandlerConfiguration; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class TcpHandlerCfg { + + @Getter + private TcpHandlerType type; + + @Min(1) + @Setter + @Getter + private int idleTimeoutSeconds; + + @Min(1) + @Setter + @Getter + private int maxConnections; + + private final Map HANDLER_MAP = new ConcurrentHashMap<>(); + + public HandlerConfiguration getConfiguration(TcpHandlerType type) { + return HANDLER_MAP.get(type); + } + + public void setConfiguration(String configuration) { + final JsonNode cfgJson = JacksonUtil.valueToTree(PropertyUtils.getProps(configuration)); + + type = TcpHandlerType.valueOf(cfgJson.get("type").asText()); + + switch (type) { + case TEXT -> HANDLER_MAP.put(type, JacksonUtil.treeToValue(cfgJson, TextHandlerConfiguration.class)); + case JSON -> HANDLER_MAP.put(type, JacksonUtil.treeToValue(cfgJson, JsonHandlerConfiguration.class)); + case BINARY -> HANDLER_MAP.put(type, JacksonUtil.treeToValue(cfgJson, BinaryHandlerConfiguration.class)); + default -> throw new IllegalArgumentException("Unknown TCP handler type: " + type); + } + } +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/enums/ForwarderType.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/enums/ForwarderType.java new file mode 100644 index 0000000..e18adb2 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/enums/ForwarderType.java @@ -0,0 +1,12 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.cfg.enums; + +public enum ForwarderType { + + memory, // 本地队列模式 + + kafka // Kafka模式 - 发送到外部 +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/enums/TcpHandlerType.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/enums/TcpHandlerType.java new file mode 100644 index 0000000..4c1a882 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/cfg/enums/TcpHandlerType.java @@ -0,0 +1,14 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.cfg.enums; + +/** + * @author baigod + */ +public enum TcpHandlerType { + TEXT, + BINARY, + JSON +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/DownlinkCmdEnum.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/DownlinkCmdEnum.java new file mode 100644 index 0000000..4fbe3f5 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/DownlinkCmdEnum.java @@ -0,0 +1,23 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.domain; + +/** + * @author baigod + */ +public enum DownlinkCmdEnum { + + LOGIN_ACK, + + VERIFY_PRICING_ACK, + + QUERY_PRICING_ACK, + + SET_PRICING, + + REMOTE_START_CHARGING, + + TRANSACTION_RECORD, +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ListenerToHandlerMsg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ListenerToHandlerMsg.java new file mode 100644 index 0000000..0b560bf --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ListenerToHandlerMsg.java @@ -0,0 +1,11 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.domain; + +import java.util.UUID; + +public record ListenerToHandlerMsg(UUID id, byte[] msg, ProtocolSession session) { + +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ProtocolSession.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ProtocolSession.java new file mode 100644 index 0000000..b8ead56 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ProtocolSession.java @@ -0,0 +1,87 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.domain; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; +import sanbing.jcpp.protocol.forwarder.Forwarder; + +import java.io.Closeable; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.function.Function; + +/** + * @author baigod + */ +@Getter +@Slf4j +public abstract class ProtocolSession implements Closeable { + + private static final int REQUEST_CACHE_LIMIT = 1000; + + protected final String protocolName; + + protected final UUID id; + + @Setter + protected LocalDateTime lastActivityTime; + + protected final Set pileCodeSet; + + private final Map> scheduledFutures = new ConcurrentHashMap<>(); + + private final Cache requestCache = Caffeine.newBuilder() + .initialCapacity(REQUEST_CACHE_LIMIT) + .maximumSize(REQUEST_CACHE_LIMIT) + .expireAfterAccess(Duration.ofMinutes(1)) + .build(); + + @Setter + private Forwarder forwarder; + + public ProtocolSession(String protocolName) { + this.protocolName = protocolName; + this.pileCodeSet = new LinkedHashSet<>(); + this.id = UUID.randomUUID(); + this.lastActivityTime = LocalDateTime.now(); + } + + public abstract void onDownlink(DownlinkRestMessage downlinkMsg); + + public void close() { + close(SessionCloseReason.DESTRUCTION); + } + + public void close(SessionCloseReason reason) { + log.info("[{}] Protocol会话关闭,原因: {}", this, reason); + + scheduledFutures.values().forEach(scheduledFuture -> scheduledFuture.cancel(true)); + scheduledFutures.clear(); + } + + @Override + public String toString() { + return "[" + id + "]" + pileCodeSet; + } + + public void addPileCode(String pileCode) { + this.pileCodeSet.add(pileCode); + } + + public void addSchedule(String name, Function> scheduledFutureFunction) { + scheduledFutures.computeIfAbsent(name, scheduledFutureFunction); + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ProtocolUplinkMsg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ProtocolUplinkMsg.java new file mode 100644 index 0000000..cdeabf8 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/ProtocolUplinkMsg.java @@ -0,0 +1,22 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.domain; + +import io.netty.buffer.ByteBufUtil; + +import java.net.SocketAddress; +import java.util.UUID; + +public record ProtocolUplinkMsg(SocketAddress address, UUID id, T data, int size) { + + @Override + public String toString() { + if (data instanceof byte[]) { + return ByteBufUtil.hexDump((byte[]) data); + } else { + return data.toString(); + } + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/SessionCloseReason.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/SessionCloseReason.java new file mode 100644 index 0000000..7c7fd58 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/SessionCloseReason.java @@ -0,0 +1,14 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.domain; + +/** + * @author baigod + */ +public enum SessionCloseReason { + DESTRUCTION, + INACTIVE, + MANUALLY +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/SessionToHandlerMsg.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/SessionToHandlerMsg.java new file mode 100644 index 0000000..fdf5a59 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/domain/SessionToHandlerMsg.java @@ -0,0 +1,13 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.domain; + +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; + +/** + * @author baigod + */ +public record SessionToHandlerMsg(DownlinkRestMessage downlinkMsg, ProtocolSession session) { +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/Forwarder.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/Forwarder.java new file mode 100644 index 0000000..7cd2c22 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/Forwarder.java @@ -0,0 +1,110 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.forwarder; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.health.Health; +import sanbing.jcpp.infrastructure.queue.*; +import sanbing.jcpp.infrastructure.queue.common.TopicPartitionInfo; +import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider; +import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider; +import sanbing.jcpp.infrastructure.queue.discovery.ServiceType; +import sanbing.jcpp.infrastructure.stats.MessagesStats; +import sanbing.jcpp.infrastructure.stats.StatsFactory; +import sanbing.jcpp.infrastructure.util.codec.ByteUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.mdc.MDCUtils; +import sanbing.jcpp.infrastructure.util.trace.Tracer; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; + +import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_PREFIX; +import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_TS; +import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ID; +import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ORIGIN; + +/** + * @author baigod + */ +@Slf4j +public abstract class Forwarder { + protected static final String ERROR = "error"; + + AtomicBoolean healthy = new AtomicBoolean(true); + + @Getter + private final String protocolName; + + protected MessagesStats forwarderMessagesStats; + + + protected final PartitionProvider partitionProvider; + protected final ServiceInfoProvider serviceInfoProvider; + + protected final boolean isMonolith; + protected QueueProducer> producer; + + public Forwarder(String protocolName, StatsFactory statsFactory, PartitionProvider partitionProvider, ServiceInfoProvider serviceInfoProvider) { + this.protocolName = protocolName; + this.partitionProvider = partitionProvider; + this.serviceInfoProvider = serviceInfoProvider; + + this.forwarderMessagesStats = statsFactory.createMessagesStats("forwarderMessages", "protocol", protocolName); + + this.isMonolith = serviceInfoProvider.isMonolith(); + } + + public abstract Health health(); + + public abstract void destroy(); + + protected void jcppForward(String topic, String key, UplinkQueueMessage msg, BiConsumer consumer) { + QueueMsgHeaders headers = new DefaultQueueMsgHeaders(); + + Tracer currentTracer = TracerContextUtil.getCurrentTracer(); + headers.put(MSG_MD_PREFIX + JCPP_TRACER_ID, ByteUtil.stringToBytes(currentTracer.getTraceId())); + headers.put(MSG_MD_PREFIX + JCPP_TRACER_ORIGIN, ByteUtil.stringToBytes(currentTracer.getOrigin())); + headers.put(MSG_MD_PREFIX + MSG_MD_TS, ByteUtil.longToBytes(currentTracer.getTracerTs())); + + TopicPartitionInfo tpi = partitionProvider.resolve(ServiceType.APP, topic, key); + producer.send(tpi, new ProtoQueueMsg<>(key, msg, headers), new QueueCallback() { + @Override + public void onSuccess(QueueMsgMetadata metadata) { + + TracerContextUtil.newTracer(currentTracer.getTraceId(), currentTracer.getOrigin(), currentTracer.getTracerTs()); + MDCUtils.recordTracer(); + log.trace("单体消息转发成功 key:{}", key); + + if (consumer != null) { + consumer.accept(true, JacksonUtil.newObjectNode()); + } + } + + @Override + public void onFailure(Throwable t) { + + TracerContextUtil.newTracer(currentTracer.getTraceId(), currentTracer.getOrigin(), currentTracer.getTracerTs()); + MDCUtils.recordTracer(); + log.warn("单体消息转发异常", t); + + if (consumer != null) { + ObjectNode objectNode = JacksonUtil.newObjectNode(); + objectNode.put(ERROR, t.getClass() + ": " + t.getMessage()); + consumer.accept(true, objectNode); + } + } + }); + } + + public abstract void sendMessage(UplinkQueueMessage msg, BiConsumer consumer); + + public abstract void sendMessage(UplinkQueueMessage msg); + +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/KafkaForwarder.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/KafkaForwarder.java new file mode 100644 index 0000000..ae1ebf3 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/KafkaForwarder.java @@ -0,0 +1,204 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.forwarder; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; +import org.apache.kafka.common.config.SslConfigs; +import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.header.internals.RecordHeader; +import org.apache.kafka.common.header.internals.RecordHeaders; +import org.apache.kafka.common.serialization.ByteArraySerializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.boot.actuate.health.Health; +import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider; +import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider; +import sanbing.jcpp.infrastructure.queue.provider.AppQueueFactory; +import sanbing.jcpp.infrastructure.stats.StatsFactory; +import sanbing.jcpp.infrastructure.util.codec.ByteUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.mdc.MDCUtils; +import sanbing.jcpp.infrastructure.util.trace.Tracer; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.cfg.ForwarderCfg; +import sanbing.jcpp.protocol.cfg.KafkaCfg; + +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; + +import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_PREFIX; +import static sanbing.jcpp.infrastructure.queue.common.QueueConstants.MSG_MD_TS; +import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ID; +import static sanbing.jcpp.infrastructure.util.trace.TracerContextUtil.JCPP_TRACER_ORIGIN; + +/** + * @author baigod + */ +@Slf4j +public class KafkaForwarder extends Forwarder { + AtomicBoolean healthy = new AtomicBoolean(true); + + private static final String OFFSET = "offset"; + private static final String PARTITION = "partition"; + private static final String TOPIC = "topic"; + + private final KafkaCfg kafkaCfg; + protected final boolean jcppPartition; + + private KafkaProducer kafkaProducer; + + public KafkaForwarder(String protocolName, + ForwarderCfg forwarderCfg, + StatsFactory statsFactory, + AppQueueFactory appQueueFactory, + PartitionProvider partitionProvider, + ServiceInfoProvider serviceInfoProvider) { + super(protocolName, statsFactory, partitionProvider, serviceInfoProvider); + + this.kafkaCfg = forwarderCfg.getKafka(); + this.jcppPartition = kafkaCfg.isJcppPartition(); + + if (this.isMonolith || jcppPartition) { + + this.producer = appQueueFactory.createProtocolUplinkMsgProducer(kafkaCfg.getTopic()); + + } else { + Properties properties = new Properties(); + properties.put(ProducerConfig.CLIENT_ID_CONFIG, "kafka-forwarder-" + getProtocolName() + "-" + serviceInfoProvider.getServiceId()); + properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaCfg.getBootstrapServers()); + properties.put(ProducerConfig.ACKS_CONFIG, kafkaCfg.getAcks()); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); + properties.put(ProducerConfig.RETRIES_CONFIG, kafkaCfg.getRetries()); + properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, kafkaCfg.getCompressionType()); + properties.put(ProducerConfig.BATCH_SIZE_CONFIG, kafkaCfg.getBatchSize()); + properties.put(ProducerConfig.LINGER_MS_CONFIG, kafkaCfg.getLingerMs()); + properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, kafkaCfg.getBufferMemory()); + if (this.kafkaCfg.getOtherProperties() != null) { + this.kafkaCfg.getOtherProperties().forEach((k, v) -> { + if (SslConfigs.SSL_KEYSTORE_CERTIFICATE_CHAIN_CONFIG.equals(k) + || SslConfigs.SSL_KEYSTORE_KEY_CONFIG.equals(k) + || SslConfigs.SSL_TRUSTSTORE_CERTIFICATES_CONFIG.equals(k)) { + v = v.replace("\\n", "\n"); + } + properties.put(k, v); + }); + } + + this.kafkaProducer = new KafkaProducer<>(properties); + } + } + + + @Override + public Health health() { + if (healthy.get()) { + return Health.up().withDetail("producer", "Kafka producer is healthy").build(); + } else { + return Health.down().withDetail("producer", "Kafka producer is unhealthy").build(); + } + } + + @Override + public void destroy() { + healthy.set(false); + + if (this.kafkaProducer != null) { + try { + this.kafkaProducer.close(); + } catch (Exception e) { + log.error("Failed to close producer during destroy()", e); + } + } + } + + @Override + public void sendMessage(UplinkQueueMessage msg, BiConsumer consumer) { + String topic = kafkaCfg.getTopic(); + + try { + + String messageKey = msg.getMessageKey(); + + if (isMonolith || jcppPartition) { + + jcppForward(topic, messageKey, msg, consumer); + + } else { + + kafkaForward(topic, messageKey, msg, consumer); + } + + } catch (Exception e) { + log.debug("[{}] Failed to forward Kafka message: {}", getProtocolName(), msg, e); + } + } + + @Override + public void sendMessage(UplinkQueueMessage msg) { + sendMessage(msg, null); + } + + private void kafkaForward(String topic, String key, UplinkQueueMessage msg, BiConsumer consumer) throws InvalidProtocolBufferException { + Headers headers = new RecordHeaders(); + + Tracer currentTracer = TracerContextUtil.getCurrentTracer(); + headers.add(new RecordHeader(MSG_MD_PREFIX + JCPP_TRACER_ID, ByteUtil.stringToBytes(currentTracer.getTraceId()))); + headers.add(new RecordHeader(MSG_MD_PREFIX + JCPP_TRACER_ORIGIN, ByteUtil.stringToBytes(currentTracer.getOrigin()))); + headers.add(new RecordHeader(MSG_MD_PREFIX + MSG_MD_TS, ByteUtil.longToBytes(currentTracer.getTracerTs()))); + + if (kafkaCfg.getEncoder() == KafkaCfg.EncoderType.json) { + + String protoJson = JsonFormat.printer().print(msg); + + log.info("[{}] Kafka forwarder send json headers:{}, message:{}", getProtocolName(), headers, protoJson); + + kafkaProducer.send(new ProducerRecord<>(topic, null, null, key, ByteUtil.stringToBytes(protoJson), headers), + (metadata, e) -> logAndDoConsumer(consumer, metadata, e, currentTracer)); + } else { + + log.info("[{}] Kafka forwarder send protobuf headers:{}, message:{}", getProtocolName(), headers, msg); + + kafkaProducer.send(new ProducerRecord<>(topic, null, null, key, msg.toByteArray(), headers), + (metadata, e) -> logAndDoConsumer(consumer, metadata, e, currentTracer)); + } + } + + private void logAndDoConsumer(BiConsumer consumer, RecordMetadata metadata, Exception e, Tracer currentTracer) { + TracerContextUtil.newTracer(currentTracer.getTraceId(), currentTracer.getOrigin(), currentTracer.getTracerTs()); + MDCUtils.recordTracer(); + log.debug("Kafka 消息转发完成, success:{}", e == null); + + if (consumer != null) { + onComplete(metadata, e, consumer); + } + } + + private void onComplete(RecordMetadata metadata, Exception e, BiConsumer consumer) { + if (consumer == null) { + return; + } + + ObjectNode objectNode = JacksonUtil.newObjectNode(); + objectNode.put(OFFSET, String.valueOf(metadata.offset())); + objectNode.put(PARTITION, String.valueOf(metadata.partition())); + objectNode.put(TOPIC, metadata.topic()); + + if (e != null) { + objectNode.put(ERROR, e.getClass() + ": " + e.getMessage()); + } + + consumer.accept(e == null, objectNode); + } + +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/MemoryForwarder.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/MemoryForwarder.java new file mode 100644 index 0000000..cbbbb17 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/forwarder/MemoryForwarder.java @@ -0,0 +1,84 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.forwarder; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.health.Health; +import sanbing.jcpp.infrastructure.queue.discovery.PartitionProvider; +import sanbing.jcpp.infrastructure.queue.discovery.ServiceInfoProvider; +import sanbing.jcpp.infrastructure.queue.provider.AppQueueFactory; +import sanbing.jcpp.infrastructure.stats.StatsFactory; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.cfg.ForwarderCfg; +import sanbing.jcpp.protocol.cfg.MemoryCfg; + +import java.util.function.BiConsumer; + +/** + * @author baigod + */ +@Slf4j +public class MemoryForwarder extends Forwarder { + + private final MemoryCfg memoryCfg; + + + public MemoryForwarder(String protocolName, + ForwarderCfg forwarderCfg, + StatsFactory statsFactory, + AppQueueFactory appQueueFactory, + PartitionProvider partitionProvider, + ServiceInfoProvider serviceInfoProvider) { + super(protocolName, statsFactory, partitionProvider, serviceInfoProvider); + + this.memoryCfg = forwarderCfg.getMemory(); + + super.producer = appQueueFactory.createProtocolUplinkMsgProducer(memoryCfg.getTopic()); + } + + @Override + public Health health() { + if (healthy.get()) { + return Health.up().withDetail("producer", "Memory producer is healthy").build(); + } else { + return Health.down().withDetail("producer", "Memory producer is unhealthy").build(); + } + } + + @Override + public void destroy() { + healthy.set(false); + + if (this.producer != null) { + try { + this.producer.stop(); + } catch (Exception e) { + log.error("Failed to close producer during destroy()", e); + } + } + } + + @Override + public void sendMessage(UplinkQueueMessage msg, BiConsumer consumer) { + String topic = memoryCfg.getTopic(); + + String key = msg.getMessageKey(); + + try { + + jcppForward(topic, key, msg, consumer); + + } catch (Exception e) { + + log.warn("[{}] Failed to forward Memory message: {}", getProtocolName(), msg, e); + } + } + + @Override + public void sendMessage(UplinkQueueMessage msg) { + sendMessage(msg, null); + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/ChannelHandlerInitializer.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/ChannelHandlerInitializer.java new file mode 100644 index 0000000..e2c1f62 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/ChannelHandlerInitializer.java @@ -0,0 +1,142 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.DelimiterBasedFrameDecoder; +import io.netty.handler.codec.Delimiters; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.json.JsonObjectDecoder; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.GlobalEventExecutor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType; +import sanbing.jcpp.protocol.listener.tcp.TcpChannelHandler; +import sanbing.jcpp.protocol.listener.tcp.configs.BinaryHandlerConfiguration; +import sanbing.jcpp.protocol.listener.tcp.configs.TextHandlerConfiguration; +import sanbing.jcpp.protocol.listener.tcp.decoder.JCPPHeadTailFrameDecoder; +import sanbing.jcpp.protocol.listener.tcp.decoder.JCPPLengthFieldBasedFrameDecoder; +import sanbing.jcpp.protocol.listener.tcp.decoder.TcpMsgDecoder; +import sanbing.jcpp.protocol.listener.tcp.handler.ConnectionLimitHandler; +import sanbing.jcpp.protocol.listener.tcp.handler.IdleEventHandler; +import sanbing.jcpp.protocol.listener.tcp.handler.TracerHandler; + +import java.nio.ByteOrder; + +import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.BINARY; +import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.TEXT; +import static sanbing.jcpp.protocol.listener.tcp.configs.BinaryHandlerConfiguration.LITTLE_ENDIAN_BYTE_ORDER; +import static sanbing.jcpp.protocol.listener.tcp.configs.TextHandlerConfiguration.SYSTEM_LINE_SEPARATOR; + +/** + * @author baigod + */ +@Slf4j +@RequiredArgsConstructor +public abstract class ChannelHandlerInitializer extends ChannelInitializer { + + protected final ChannelGroup CHANNEL_GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); + + @Override + protected abstract void initChannel(C ch); + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + super.exceptionCaught(ctx, cause); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + super.channelUnregistered(ctx); + } + + public static ChannelHandlerInitializer createTcpChannelHandler(ChannelHandlerParameter parameter) { + TcpHandlerType type = parameter.handlerCfg().getType(); + + return switch (type) { + case TEXT -> new ChannelHandlerInitializer<>() { + @Override + protected void initChannel(SocketChannel socketChannel) { + TextHandlerConfiguration textHandlerConfig = (TextHandlerConfiguration) parameter.handlerCfg().getConfiguration(TEXT); + ByteBuf[] delimiters = SYSTEM_LINE_SEPARATOR.equals(textHandlerConfig.getMessageSeparator()) + ? Delimiters.lineDelimiter() : Delimiters.nulDelimiter(); + DelimiterBasedFrameDecoder framer = new DelimiterBasedFrameDecoder(textHandlerConfig.getMaxFrameLength(), + textHandlerConfig.isStripDelimiter(), delimiters); + socketChannel.pipeline() + .addLast("tracerHandler", new TracerHandler()) + .addLast("connectionLimitHandler", new ConnectionLimitHandler(parameter.protocolName(), parameter.handlerCfg().getMaxConnections(), CHANNEL_GROUP, parameter.connectionsGauge())) + .addLast("idleStateHandler", new IdleStateHandler(parameter.handlerCfg().getIdleTimeoutSeconds(), 0, 0)) + .addLast("idleEventHandler", new IdleEventHandler(parameter.protocolName())) + .addLast("framer", framer) + .addLast("tcpTextDecoder", new TcpMsgDecoder<>(parameter.protocolName(), msg -> TcpMsgDecoder.toString(msg, textHandlerConfig.getCharsetName()))) + .addLast("tcpStringInHandler", new TcpChannelHandler<>(parameter)); + + } + }; + case JSON -> new ChannelHandlerInitializer<>() { + @Override + protected void initChannel(SocketChannel socketChannel) { + socketChannel.pipeline() + .addLast("tracerHandler", new TracerHandler()) + .addLast("connectionLimitHandler", new ConnectionLimitHandler(parameter.protocolName(), parameter.handlerCfg().getMaxConnections(), CHANNEL_GROUP, parameter.connectionsGauge())) + .addLast("idleStateHandler", + new IdleStateHandler(parameter.handlerCfg().getIdleTimeoutSeconds(), 0, 0)) + .addLast("idleEventHandler", new IdleEventHandler(parameter.protocolName())) + .addLast("datagramToJsonDecoder", new JsonObjectDecoder()) + .addLast("tcpJsonDecoder", new TcpMsgDecoder<>(parameter.protocolName(), TcpMsgDecoder::toJson)) + .addLast("tcpJsonInHandler", new TcpChannelHandler<>(parameter)); + } + }; + case BINARY -> new ChannelHandlerInitializer<>() { + @Override + protected void initChannel(SocketChannel socketChannel) { + BinaryHandlerConfiguration binaryHandlerConfig = (BinaryHandlerConfiguration) parameter.handlerCfg().getConfiguration(BINARY); + + ByteOrder byteOrder = LITTLE_ENDIAN_BYTE_ORDER.equalsIgnoreCase(binaryHandlerConfig.getByteOrder()) + ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + + socketChannel.pipeline() + .addLast("tracerHandler", new TracerHandler()) + .addLast("connectionLimitHandler", new ConnectionLimitHandler(parameter.protocolName(), parameter.handlerCfg().getMaxConnections(), CHANNEL_GROUP, parameter.connectionsGauge())) + .addLast("idleStateHandler", new IdleStateHandler(parameter.handlerCfg().getIdleTimeoutSeconds(), 0, 0)) + .addLast("idleEventHandler", new IdleEventHandler(parameter.protocolName())); + + if (LengthFieldBasedFrameDecoder.class.isAssignableFrom(binaryHandlerConfig.getDecoder())) { + LengthFieldBasedFrameDecoder framer = new LengthFieldBasedFrameDecoder(byteOrder, + binaryHandlerConfig.getMaxFrameLength(), binaryHandlerConfig.getLengthFieldOffset(), + binaryHandlerConfig.getLengthFieldLength(), binaryHandlerConfig.getLengthAdjustment(), + binaryHandlerConfig.getInitialBytesToStrip(), binaryHandlerConfig.isFailFast()); + socketChannel.pipeline().addLast("LengthFieldBasedFrameDecoder", framer); + } else if (JCPPLengthFieldBasedFrameDecoder.class.isAssignableFrom(binaryHandlerConfig.getDecoder())) { + JCPPLengthFieldBasedFrameDecoder framer = new JCPPLengthFieldBasedFrameDecoder(binaryHandlerConfig.getHead(), byteOrder, + binaryHandlerConfig.getLengthFieldOffset(), binaryHandlerConfig.getLengthFieldLength(), + binaryHandlerConfig.getLengthAdjustment(), binaryHandlerConfig.getInitialBytesToStrip()); + socketChannel.pipeline().addLast("JCPPLengthFieldBasedFrameDecoder", framer); + } else if (JCPPHeadTailFrameDecoder.class.isAssignableFrom(binaryHandlerConfig.getDecoder())) { + JCPPHeadTailFrameDecoder framer = new JCPPHeadTailFrameDecoder(binaryHandlerConfig.getHead(), + binaryHandlerConfig.getTail()); + socketChannel.pipeline().addLast("JCPPHeadTailFrameDecoder", framer); + } else { + throw new IllegalArgumentException("Unknown binary decoder"); + } + + socketChannel.pipeline() + .addLast("tcpByteDecoderOverride", new TcpMsgDecoder<>(parameter.protocolName(), TcpMsgDecoder::toByteArray)) + .addLast("tcpByteHandler", new TcpChannelHandler<>(parameter)); + } + }; + case null -> throw new IllegalArgumentException("Unknown: " + parameter.handlerCfg()); + }; + } + + +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/ChannelHandlerParameter.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/ChannelHandlerParameter.java new file mode 100644 index 0000000..dc5509d --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/ChannelHandlerParameter.java @@ -0,0 +1,24 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener; + +import io.micrometer.core.instrument.Timer; +import sanbing.jcpp.infrastructure.stats.DefaultCounter; +import sanbing.jcpp.infrastructure.stats.MessagesStats; +import sanbing.jcpp.protocol.ProtocolMessageProcessor; +import sanbing.jcpp.protocol.cfg.TcpHandlerCfg; + +import java.util.concurrent.atomic.AtomicInteger; + +public record ChannelHandlerParameter(String protocolName, + TcpHandlerCfg handlerCfg, + ProtocolMessageProcessor protocolMessageProcessor, + AtomicInteger connectionsGauge, + MessagesStats uplinkMsgStats, + MessagesStats downlinkMsgStats, + DefaultCounter uplinkTrafficCounter, + DefaultCounter downlinkTrafficCounter, + Timer downlinkTimer) { +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/Listener.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/Listener.java new file mode 100644 index 0000000..b438b6f --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/Listener.java @@ -0,0 +1,45 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener; + +import io.micrometer.core.instrument.Timer; +import lombok.Getter; +import org.springframework.boot.actuate.health.Health; +import sanbing.jcpp.infrastructure.stats.DefaultCounter; +import sanbing.jcpp.infrastructure.stats.MessagesStats; +import sanbing.jcpp.infrastructure.stats.StatsFactory; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author baigod + */ +public abstract class Listener { + + @Getter + private final String protocolName; + + protected AtomicInteger connectionsGauge = new AtomicInteger(); + protected MessagesStats uplinkMsgStats; + protected MessagesStats downlinkMsgStats; + protected DefaultCounter uplinkTrafficCounter; + protected DefaultCounter downlinkTrafficCounter; + protected Timer downlinkTimer; + + public Listener(String protocolName, StatsFactory statsFactory) { + this.protocolName = protocolName; + + statsFactory.createGauge("openConnections", connectionsGauge, "protocol", protocolName); + this.uplinkMsgStats = statsFactory.createMessagesStats("listenerUplinkMessage", "protocol", protocolName); + this.downlinkMsgStats = statsFactory.createMessagesStats("listenerDownlinkMessage", "protocol", protocolName); + this.uplinkTrafficCounter = statsFactory.createDefaultCounter("listenerUplinkTraffic", "protocol", protocolName); + this.downlinkTrafficCounter = statsFactory.createDefaultCounter("listenerDownlinkTraffic", "protocol", protocolName); + this.downlinkTimer = statsFactory.createTimer("listenerDownlink", "protocol", protocolName); + } + + public abstract Health health(); + + public abstract void destroy() throws InterruptedException; +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpChannelHandler.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpChannelHandler.java new file mode 100644 index 0000000..03d5d24 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpChannelHandler.java @@ -0,0 +1,238 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp; + +import com.fasterxml.jackson.databind.JsonNode; +import io.micrometer.core.instrument.Timer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.util.concurrent.Future; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.stats.DefaultCounter; +import sanbing.jcpp.infrastructure.stats.MessagesStats; +import sanbing.jcpp.infrastructure.util.exception.DownlinkException; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; +import sanbing.jcpp.protocol.ProtocolMessageProcessor; +import sanbing.jcpp.protocol.domain.ListenerToHandlerMsg; +import sanbing.jcpp.protocol.domain.ProtocolUplinkMsg; +import sanbing.jcpp.protocol.domain.SessionCloseReason; +import sanbing.jcpp.protocol.domain.SessionToHandlerMsg; +import sanbing.jcpp.protocol.listener.ChannelHandlerParameter; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.function.Supplier; + +@Slf4j +public class TcpChannelHandler extends SimpleChannelInboundHandler> { + private final String protocolName; + private final ProtocolMessageProcessor protocolMessageProcessor; + + private final MessagesStats uplinkMsgStats; + private final DefaultCounter uplinkTrafficCounter; + private final MessagesStats downlinkMsgStats; + private final DefaultCounter downlinkTrafficCounter; + private final Timer downlinkTimer; + + private final TcpSession tcpSession; + + @SneakyThrows + public TcpChannelHandler(ChannelHandlerParameter parameter) { + this.protocolName = parameter.protocolName(); + this.protocolMessageProcessor = parameter.protocolMessageProcessor(); + + this.uplinkMsgStats = parameter.uplinkMsgStats(); + this.uplinkTrafficCounter = parameter.uplinkTrafficCounter(); + this.downlinkMsgStats = parameter.downlinkMsgStats(); + this.downlinkTrafficCounter = parameter.downlinkTrafficCounter(); + this.downlinkTimer = parameter.downlinkTimer(); + + tcpSession = new TcpSession(protocolName, this::onDownlink, this::writeAndFlush); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ProtocolUplinkMsg msg) { + + if (log.isDebugEnabled()) { + + log.debug("[{}]{}{} Netty拆出到上行报文:{}", protocolName, ctx.channel(), tcpSession, msg); + } + + uplinkMsgStats.incrementTotal(); + + uplinkTrafficCounter.add(msg.size()); + + tcpSession.setLastActivityTime(LocalDateTime.now()); + + if (tcpSession.getAddress() == null) { + + tcpSession.setAddress(msg.address()); + } + + if (tcpSession.getCtx() == null) { + + tcpSession.setCtx(ctx); + } + + T data = msg.data(); + + if (Objects.isNull(data)) { + + log.debug("[{}]{}{} 上行报文为空被过滤 [{}]", protocolName, ctx.channel(), tcpSession, msg); + + return; + } + + try { + + process(msg, ctx); + + uplinkMsgStats.incrementSuccessful(); + + } catch (Exception e) { + + uplinkMsgStats.incrementFailed(); + + log.error("[{}]{}{} TCP管道处理报文异常", protocolName, ctx.channel(), tcpSession, e); + } + } + + private void process(ProtocolUplinkMsg msg, ChannelHandlerContext ctx) { + switch (msg.data()) { + case byte[] bytes -> + protocolMessageProcessor.uplinkHandleAsync(new ListenerToHandlerMsg(msg.id(), bytes, tcpSession), uplinkMsgStats); + case JsonNode json -> + protocolMessageProcessor.uplinkHandleAsync(new ListenerToHandlerMsg(msg.id(), JacksonUtil.writeValueAsBytes(json), tcpSession), uplinkMsgStats); + case String text -> + protocolMessageProcessor.uplinkHandleAsync(new ListenerToHandlerMsg(msg.id(), JacksonUtil.writeValueAsBytes(text.getBytes()), tcpSession), uplinkMsgStats); + case null, default -> { + assert msg.data() != null; + log.warn("[{}]{}{} 不支持的TCP上行报文类型:{}", protocolName, ctx.channel(), tcpSession, msg.data().getClass()); + } + } + } + + protected void onDownlink(ChannelHandlerContext ctx, DownlinkRestMessage downlinkMsg) throws DownlinkException { + protocolMessageProcessor.downlinkHandle(new SessionToHandlerMsg(downlinkMsg, tcpSession), downlinkMsgStats); + } + + protected void writeAndFlush(ChannelHandlerContext ctx, ByteBuf... byteBufList) { + if (byteBufList == null || byteBufList.length == 0) { + + return; + } + + if (ctx.isRemoved()) { + + tcpSession.close(SessionCloseReason.INACTIVE); + + log.warn("[{}]{}{} TCP会话已失效,因此删除会话", protocolName, ctx.channel(), tcpSession); + + return; + } + + downlinkMsgStats.incrementTotal(byteBufList.length); + + for (ByteBuf byteBuf : byteBufList) { + + try { + + if (Objects.isNull(byteBuf)) { + log.warn("[{}]{}{} 下发空报文被拦截", protocolName, ctx.channel(), tcpSession); + continue; + } + + logDownlinkStart(ctx, byteBuf.readableBytes(), () -> ByteBufUtil.hexDump(byteBuf)); + + ctx.writeAndFlush(Unpooled.wrappedBuffer(byteBuf)) + .addListener(channelFuture -> logDownlinkUnsuccessful(ctx, channelFuture)); + + + downlinkMsgStats.incrementSuccessful(); + + } catch (Exception e) { + + downlinkMsgStats.incrementFailed(); + + throw e; + } + } + + } + + private void logDownlinkStart(ChannelHandlerContext ctx, int payloadSize, Supplier logTransform) { + + downlinkTrafficCounter.add(payloadSize); + + if (log.isDebugEnabled()) { + log.debug("[{}]{}{} 开始发送下行报文:{}", protocolName, ctx.channel(), tcpSession, logTransform.get()); + } + } + + private void logDownlinkUnsuccessful(ChannelHandlerContext ctx, Future channelFuture) { + + downlinkTimer.record(Duration.ofMillis(System.currentTimeMillis() - TracerContextUtil.getCurrentTracer().getTracerTs())); + + if (channelFuture.isDone() && !channelFuture.isSuccess()) { + log.info("[{}]{}{} 下行报文发送未成功", protocolName, ctx.channel(), tcpSession); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + + ctx.flush(); + + if (log.isTraceEnabled()) { + log.trace("[{}]{}{} Channel Read Complete [{}]", protocolName, ctx.channel(), tcpSession, ctx.name()); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("[{}]{}{} Invalid message received, Exception caught", protocolName, ctx.channel(), tcpSession, cause); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + + super.channelRegistered(ctx); + + log.info("[{}]{} 打开通道", protocolName, ctx.channel()); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + + super.channelUnregistered(ctx); + + log.info("[{}]{}{} 关闭通道", protocolName, ctx.channel(), tcpSession); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + + super.channelActive(ctx); + + log.info("[{}]{} 通道活跃", protocolName, ctx.channel()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + + super.channelInactive(ctx); + + log.info("[{}]{}{} 通道不活跃", protocolName, ctx.channel(), tcpSession); + } + + +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpListener.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpListener.java new file mode 100644 index 0000000..8f99074 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpListener.java @@ -0,0 +1,114 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.Future; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.health.Health; +import sanbing.jcpp.infrastructure.stats.StatsFactory; +import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory; +import sanbing.jcpp.protocol.ProtocolMessageProcessor; +import sanbing.jcpp.protocol.cfg.TcpCfg; +import sanbing.jcpp.protocol.listener.ChannelHandlerInitializer; +import sanbing.jcpp.protocol.listener.ChannelHandlerParameter; +import sanbing.jcpp.protocol.listener.Listener; + +/** + * @author baigod + */ +@Slf4j +public class TcpListener extends Listener { + + private Channel serverChannel; + private EventLoopGroup bossGroup; + private EventLoopGroup workerGroup; + + private final ChannelHandlerParameter parameter; + + public TcpListener(String protocolName, TcpCfg tcpCfg, ProtocolMessageProcessor protocolMessageProcessor, StatsFactory statsFactory) throws InterruptedException { + super(protocolName, statsFactory); + + parameter = new ChannelHandlerParameter(protocolName, tcpCfg.getHandler(), protocolMessageProcessor, connectionsGauge, uplinkMsgStats, downlinkMsgStats, uplinkTrafficCounter, downlinkTrafficCounter, downlinkTimer); + + tcpServerBootstrap(tcpCfg, getProtocolName()); + } + + private void tcpServerBootstrap(TcpCfg tcpCfg, String protocolName) throws InterruptedException { + bossGroup = new NioEventLoopGroup(tcpCfg.getBossGroupThreadCount(), JCPPThreadFactory.forName("tcp-boss")); + workerGroup = new NioEventLoopGroup(tcpCfg.getWorkerGroupThreadCount(), JCPPThreadFactory.forName("tcp-worker")); + + ChannelHandlerInitializer channelHandler = ChannelHandlerInitializer.createTcpChannelHandler(parameter); + + ServerBootstrap server = new ServerBootstrap() + .group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, tcpCfg.getSoBacklog()) + .option(ChannelOption.SO_REUSEADDR, true) + .option(ChannelOption.SO_RCVBUF, tcpCfg.getSoRcvbuf()) + .childOption(ChannelOption.SO_KEEPALIVE, tcpCfg.isSoKeepAlive()) + .childOption(ChannelOption.TCP_NODELAY, tcpCfg.isNodelay()) + .childOption(ChannelOption.SO_SNDBUF, tcpCfg.getSoSndbuf()) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(channelHandler); + serverChannel = server.bind(tcpCfg.getBindAddress(), tcpCfg.getBindPort()).sync().channel(); + log.info("Tcp server [{}] started, BindAddress:[{}], BindPort: [{}]", protocolName, tcpCfg.getBindAddress(), tcpCfg.getBindPort()); + } + + + private void tcpServerShutdown() throws InterruptedException { + if (this.serverChannel != null) { + ChannelFuture cf = this.serverChannel.close().sync(); + cf.awaitUninterruptibly(); + } + + Future bossFuture = null; + Future workerFuture = null; + + if (bossGroup != null) { + bossFuture = bossGroup.shutdownGracefully(); + } + if (workerGroup != null) { + workerFuture = workerGroup.shutdownGracefully(); + } + + log.info("[{}] Awaiting shutdown gracefully boss and worker groups...", getProtocolName()); + + if (bossFuture != null) { + bossFuture.sync(); + } + if (workerFuture != null) { + workerFuture.sync(); + } + + log.info("[{}] Protocol server stopped!", getProtocolName()); + } + + @Override + public Health health() { + if (serverChannel != null) { + if (serverChannel.isActive()) { + return Health.up().withDetail("TcpServer", "Active").build(); + } else { + return Health.down().withDetail("TcpServer", "Inactive").build(); + } + } + return Health.down().build(); + } + + @Override + public void destroy() throws InterruptedException { + tcpServerShutdown(); + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpSession.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpSession.java new file mode 100644 index 0000000..7071521 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/TcpSession.java @@ -0,0 +1,90 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; +import sanbing.jcpp.protocol.domain.ProtocolSession; +import sanbing.jcpp.protocol.domain.SessionCloseReason; +import sanbing.jcpp.protocol.listener.tcp.enums.SequenceNumberLength; + +import java.net.SocketAddress; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; + +/** + * 设备会话 + * + * @author baigod + */ +@EqualsAndHashCode(callSuper = true) +@Getter +@Setter +public class TcpSession extends ProtocolSession { + + private SocketAddress address; + + private ChannelHandlerContext ctx; + + private final BiConsumer sendDownlinkConsumer; + + private final BiConsumer writeAndFlushConsumer; + + private final AtomicInteger sequenceNumber = new AtomicInteger(0); + + public int nextSeqNo(SequenceNumberLength sequenceNumberLength) { + synchronized (sequenceNumber) { + int result = sequenceNumber.incrementAndGet(); + switch (sequenceNumberLength) { + case BYTE -> { + if (result == 0xFF) { + sequenceNumber.set(0); + } + } + case SHORT -> { + if (result == Short.MAX_VALUE) { + sequenceNumber.set(0); + } + } + default -> { + if (result == Integer.MAX_VALUE) { + sequenceNumber.set(0); + } + } + } + + return result; + } + } + + public TcpSession(String protocolName, + BiConsumer sendDownlinkConsumer, + BiConsumer writeAndFlushConsumer) { + super(protocolName); + this.sendDownlinkConsumer = sendDownlinkConsumer; + this.writeAndFlushConsumer = writeAndFlushConsumer; + } + + @Override + public void onDownlink(DownlinkRestMessage downlinkMsg) { + sendDownlinkConsumer.accept(ctx, downlinkMsg); + } + + @Override + public void close(SessionCloseReason reason) { + super.close(reason); + + ctx.flush(); + ctx.close(); + } + + public void writeAndFlush(ByteBuf byteBuf) { + writeAndFlushConsumer.accept(ctx, byteBuf); + } +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/BinaryHandlerConfiguration.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/BinaryHandlerConfiguration.java new file mode 100644 index 0000000..39606ef --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/BinaryHandlerConfiguration.java @@ -0,0 +1,75 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.configs; + +import io.netty.handler.codec.ByteToMessageDecoder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType; + +import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.BINARY; + +@Data +@ToString +@EqualsAndHashCode +public class BinaryHandlerConfiguration implements HandlerConfiguration { + public static final String LITTLE_ENDIAN_BYTE_ORDER = "LITTLE_ENDIAN"; + + /** + * 拆包器 + */ + private Class decoder; + + /** + * 大小端(共用) + */ + private String byteOrder; + + /** + * 起始域HEX字符串 + */ + private String head; + + /** + * 结束域(HeadTailFrameDecoder) + */ + private String tail; + + /** + * 最大帧长(LengthFieldBasedFrameDecoder) + */ + private int maxFrameLength; + + /** + * 长度域位置(共用) + */ + private int lengthFieldOffset; + + /** + * 长度域长度(共用) + */ + private int lengthFieldLength; + + /** + * 长度调整(共用) + */ + private int lengthAdjustment; + + /** + * 初始跳过字节数(共用) + */ + private int initialBytesToStrip; + + /** + * 快速失败(LengthFieldBasedFrameDecoder) + */ + private boolean failFast; + + public TcpHandlerType getType() { + return BINARY; + } + +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/HandlerConfiguration.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/HandlerConfiguration.java new file mode 100644 index 0000000..251db93 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/HandlerConfiguration.java @@ -0,0 +1,37 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.configs; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType; + +@JsonTypeInfo( + use = Id.NAME, + property = "type" +) +@JsonSubTypes({ + @Type( + value = TextHandlerConfiguration.class, + name = "TEXT" + ), + @Type( + value = BinaryHandlerConfiguration.class, + name = "BINARY" + ), + @Type( + value = JsonHandlerConfiguration.class, + name = "JSON" + ) +}) +@JsonIgnoreProperties( + ignoreUnknown = true +) +public interface HandlerConfiguration { + TcpHandlerType getType(); +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/JsonHandlerConfiguration.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/JsonHandlerConfiguration.java new file mode 100644 index 0000000..fb68f07 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/JsonHandlerConfiguration.java @@ -0,0 +1,22 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.configs; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType; + +import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.JSON; + +@Data +@ToString +@EqualsAndHashCode +public class JsonHandlerConfiguration implements HandlerConfiguration { + public TcpHandlerType getType() { + return JSON; + } + +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/TextHandlerConfiguration.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/TextHandlerConfiguration.java new file mode 100644 index 0000000..af92ecf --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/configs/TextHandlerConfiguration.java @@ -0,0 +1,32 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.configs; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import sanbing.jcpp.protocol.cfg.enums.TcpHandlerType; + +import static sanbing.jcpp.protocol.cfg.enums.TcpHandlerType.TEXT; + +@Data +@ToString +@EqualsAndHashCode +public class TextHandlerConfiguration implements HandlerConfiguration { + public static final String SYSTEM_LINE_SEPARATOR = "SYSTEM_LINE_SEPARATOR"; + + private int maxFrameLength; + + private boolean stripDelimiter; + + private String messageSeparator; + + private String charsetName; + + public TcpHandlerType getType() { + return TEXT; + } + +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPHeadTailFrameDecoder.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPHeadTailFrameDecoder.java new file mode 100644 index 0000000..a06f9d6 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPHeadTailFrameDecoder.java @@ -0,0 +1,145 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.decoder; + +import cn.hutool.core.util.HexUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static sanbing.jcpp.protocol.listener.tcp.enums.ReadAct.BREAK; +import static sanbing.jcpp.protocol.listener.tcp.enums.ReadAct.CONTINUE; + +/** + * 起始域结束域拆包 + * + * @author baigod + */ +@Slf4j +public class JCPPHeadTailFrameDecoder extends ByteToMessageDecoder { + /** + * 起始域 + */ + private final byte[] headBytes; + + /** + * 结束域 + */ + private final byte[] tailBytes; + + public JCPPHeadTailFrameDecoder(String head, String tail) { + checkNotNull(head, "head"); + checkNotNull(head, "tail"); + + this.headBytes = HexUtil.decodeHex(head); + this.tailBytes = HexUtil.decodeHex(tail); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + while (in.isReadable()) { + Object decoded = decode(ctx, in); + + if (decoded == null || decoded == BREAK) { + break; + } + + if (decoded == CONTINUE) { + continue; + } + + out.add(decoded); + } + } + + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) { + if (log.isTraceEnabled()) { + String hexDump = ByteBufUtil.hexDump(in); + log.trace("{} 开始解析16进制报文:{}", ctx.channel(), hexDump); + } + // 剩余可读长度 + int readableBytes = in.readableBytes(); + + // 读取到的字节长度小于起始域+结束语长度,则跳过先不处理 + if (readableBytes < headBytes.length + tailBytes.length) { + log.debug("{} 可读长度过短,因此跳过,可读长度:{}", ctx.channel(), readableBytes); + return BREAK; + } + + // byteBuf当前的读索引 + int buffIndex = in.readerIndex(); + + // 查看前n个字节判断消息头 + byte[] firstBytes = new byte[headBytes.length]; + for (int i = 0; i < headBytes.length; i++, buffIndex++) { + firstBytes[i] = in.getByte(buffIndex); + } + + // 校验起始域,如果不符,则丢弃1字节,直到读取到正确的起始域为止 + if (!Arrays.equals(firstBytes, headBytes)) { + byte aByte = in.readByte(); + if (log.isDebugEnabled()) { + log.debug("{} 丢弃1字节 {}", ctx.channel(), String.format("%02X", aByte & 0xFF)); + } + return CONTINUE; + } + + // 记住起始字节的位置 + int startIndex = in.readerIndex(); + + // 找到结束字节序列 + int endIndex = indexOf(in, tailBytes, headBytes.length); + + if (endIndex < 0) { + log.debug("{} 未找到结束域索引,因此先跳过", ctx.channel()); + return BREAK; + } + + // 提取报文 + int length = endIndex + tailBytes.length; + ByteBuf frame = in.retainedSlice(startIndex, length); + in.readerIndex(startIndex + length); + + return frame; + } + + public static int indexOf(ByteBuf in, byte[] bytes, int fromIndex) { + if (bytes.length == 0) { + return 0; + } + if (in.readableBytes() == 0) { + return -1; + } + + int targetCount = bytes.length; + int readerIndex = in.readerIndex() + fromIndex; + int writerIndex = in.writerIndex(); + + int lastIndex = in.indexOf(readerIndex, writerIndex, bytes[0]); + while (lastIndex != -1) { + if (lastIndex + targetCount <= writerIndex) { + boolean matched = true; + for (int i = 1; i < targetCount; i++) { + if (in.getByte(lastIndex + i) != bytes[i]) { + matched = false; + break; + } + } + if (matched) { + return lastIndex - in.readerIndex(); + } + } + lastIndex = in.indexOf(lastIndex + 1, writerIndex, bytes[0]); + } + + return -1; + } +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPLengthFieldBasedFrameDecoder.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPLengthFieldBasedFrameDecoder.java new file mode 100644 index 0000000..cd3073c --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/JCPPLengthFieldBasedFrameDecoder.java @@ -0,0 +1,217 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.decoder; + +import cn.hutool.core.util.HexUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.DecoderException; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; +import static sanbing.jcpp.protocol.listener.tcp.enums.ReadAct.BREAK; +import static sanbing.jcpp.protocol.listener.tcp.enums.ReadAct.CONTINUE; + +/** + * JCPP长度域拆包 + * + * @author baigod + */ +@Slf4j +public class JCPPLengthFieldBasedFrameDecoder extends ByteToMessageDecoder { + /** + * 起始域 + */ + private final byte[] headBytes; + private final ByteOrder byteOrder; + private final int lengthFieldOffset; + private final int lengthFieldLength; + private final int lengthFieldEndOffset; + private final int lengthAdjustment; + private final int initialBytesToStrip; + private int frameLengthInt = -1; + + public JCPPLengthFieldBasedFrameDecoder(String head, ByteOrder byteOrder, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, + int initialBytesToStrip) { + checkNotNull(head, "head"); + + this.headBytes = HexUtil.decodeHex(head); + + this.byteOrder = checkNotNull(byteOrder, "byteOrder"); + + checkPositiveOrZero(lengthFieldOffset, "lengthFieldOffset"); + + checkPositiveOrZero(initialBytesToStrip, "initialBytesToStrip"); + + this.lengthFieldOffset = lengthFieldOffset; + this.lengthFieldLength = lengthFieldLength; + this.lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength; + this.lengthAdjustment = lengthAdjustment; + this.initialBytesToStrip = initialBytesToStrip; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + while (in.isReadable()) { + Object decoded = decode(ctx, in); + + if (decoded == null || decoded == BREAK) { + break; + } + + if (decoded == CONTINUE) { + continue; + } + + out.add(decoded); + } + } + + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) { + if (log.isDebugEnabled()) { + String hexDump = ByteBufUtil.hexDump(in); + log.debug("{} 开始解析16进制报文:{}", ctx.channel(), hexDump); + } + // 帧长 + long frameLength = 0; + // new frame + if (frameLengthInt == -1) { + // 剩余可读长度 + int readableBytes = in.readableBytes(); + + // 读取到的字节长度小于长度域结束位置,则跳过先不处理 + if (readableBytes < lengthFieldEndOffset) { + log.debug("{} 读取到的字节长度小于长度域结束位置,则跳过先不处理 readableBytes:{}", ctx.channel(), readableBytes); + return BREAK; + } + + // byteBuf当前的读索引 + int buffIndex = in.readerIndex(); + + // 查看前n个字节判断消息头 + byte[] firstBytes = new byte[headBytes.length]; + for (int i = 0; i < headBytes.length; i++, buffIndex++) { + firstBytes[i] = in.getByte(buffIndex); + } + + // 校验起始域,如果不符,则丢弃1字节,直到读取到正确的起始域为止 + if (!Arrays.equals(firstBytes, headBytes)) { + in.skipBytes(1); + return CONTINUE; + } + + // 实际长度域位置 + int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset; + frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset); + + // 如果帧长<0,则跳过buf并抛出异常 + if (frameLength < 0) { + failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset); + } + + // 帧长 = 调整长度 + 长度域结束位置 + frameLength += lengthAdjustment + lengthFieldEndOffset; + + // 如果帧长<长度与结束位置,则跳过并抛出异常 + if (frameLength < lengthFieldEndOffset) { + failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset); + } + + frameLengthInt = (int) frameLength; + } + + // frameLengthInt exist , just check buf + if (in.readableBytes() < frameLengthInt) { + log.debug("{} 可读长度小于帧长,因此跳过 {}", ctx.channel(), frameLengthInt); + return BREAK; + } + + // 初始跳过长度如果大于帧长,则跳过并抛出异常 + if (initialBytesToStrip > frameLengthInt) { + failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip); + } + in.skipBytes(initialBytesToStrip); + + // extract frame + int readerIndex = in.readerIndex(); + int actualFrameLength = frameLengthInt - initialBytesToStrip; + ByteBuf frame = extractFrame(in, readerIndex, actualFrameLength); + in.readerIndex(readerIndex + actualFrameLength); + frameLengthInt = -1; // start processing the next frame + + return frame; + } + + /** + * 获取未调整的帧长度 + */ + protected long getUnadjustedFrameLength(ByteBuf buf, int offset) { + long frameLength; + switch (lengthFieldLength) { + case 1 -> frameLength = buf.getUnsignedByte(offset); + case 2 -> { + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + frameLength = buf.getUnsignedShortLE(offset); + } else { + frameLength = buf.getUnsignedShort(offset); + } + } + case 3 -> { + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + frameLength = buf.getUnsignedMediumLE(offset); + } else { + frameLength = buf.getUnsignedMedium(offset); + } + } + case 4 -> { + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + frameLength = buf.getUnsignedIntLE(offset); + } else { + frameLength = buf.getUnsignedInt(offset); + } + } + case 8 -> { + if (byteOrder == ByteOrder.LITTLE_ENDIAN) { + frameLength = buf.getLongLE(offset); + } else { + frameLength = buf.getLong(offset); + } + } + default -> + throw new DecoderException("unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)"); + } + return frameLength; + } + + private static void failOnNegativeLengthField(ByteBuf in, long frameLength, int lengthFieldEndOffset) { + in.skipBytes(lengthFieldEndOffset); + throw new CorruptedFrameException("negative pre-adjustment length field: " + frameLength); + } + + private static void failOnFrameLengthLessThanLengthFieldEndOffset(ByteBuf in, long frameLength, int lengthFieldEndOffset) { + in.skipBytes(lengthFieldEndOffset); + throw new CorruptedFrameException( + "Adjusted frame length (" + frameLength + ") is less " + "than lengthFieldEndOffset: " + lengthFieldEndOffset); + } + + private static void failOnFrameLengthLessThanInitialBytesToStrip(ByteBuf in, long frameLength, int initialBytesToStrip) { + in.skipBytes((int) frameLength); + throw new CorruptedFrameException( + "Adjusted frame length (" + frameLength + ") is less " + "than initialBytesToStrip: " + initialBytesToStrip); + } + + protected ByteBuf extractFrame(ByteBuf buffer, int index, int length) { + return buffer.retainedSlice(index, length); + } + +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/TcpMsgDecoder.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/TcpMsgDecoder.java new file mode 100644 index 0000000..e1ae127 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/decoder/TcpMsgDecoder.java @@ -0,0 +1,51 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.decoder; + +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.protocol.domain.ProtocolUplinkMsg; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.UUID; +import java.util.function.Function; + +@RequiredArgsConstructor +@Slf4j +public class TcpMsgDecoder extends MessageToMessageDecoder { + private final String protocolName; + private final Function transformer; + + @Override + public void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) { + try { + out.add(new ProtocolUplinkMsg<>(ctx.pipeline().channel().remoteAddress(), UUID.randomUUID(), this.transformer.apply(msg), msg.readableBytes())); + } catch (Exception e) { + log.error("[{}][{}] Exception during of decoding message", protocolName, ctx.channel(), e); + throw new RuntimeException(e); + } + } + + public static byte[] toByteArray(ByteBuf buffer) { + byte[] bytes = new byte[buffer.readableBytes()]; + buffer.readBytes(bytes); + return bytes; + } + + public static String toString(ByteBuf buffer, String charsetName) { + return buffer.toString(Charset.forName(charsetName)); + } + + public static JsonNode toJson(ByteBuf buffer) { + return JacksonUtil.fromBytes(toByteArray(buffer)); + } + +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/enums/ReadAct.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/enums/ReadAct.java new file mode 100644 index 0000000..25a51ed --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/enums/ReadAct.java @@ -0,0 +1,15 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.enums; + +/** + * 读取动作,辅助枚举 + * + * @author baigod + */ +public enum ReadAct { + BREAK, + CONTINUE +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/enums/SequenceNumberLength.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/enums/SequenceNumberLength.java new file mode 100644 index 0000000..3a48681 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/enums/SequenceNumberLength.java @@ -0,0 +1,21 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.enums; + +/** + * @author baigod + */ +public enum SequenceNumberLength { + + // 1字节 + BYTE, + + // 2字节 + SHORT, + + // 4字节 + INT, + +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/ConnectionLimitHandler.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/ConnectionLimitHandler.java new file mode 100644 index 0000000..9508e26 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/ConnectionLimitHandler.java @@ -0,0 +1,60 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.handler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.group.ChannelGroup; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +@RequiredArgsConstructor +public class ConnectionLimitHandler extends ChannelInboundHandlerAdapter { + private final String protocolName; + private final int maxConnections; + private final ChannelGroup channelGroup; + private final AtomicInteger connectionsGauge; + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + if (connectionsGauge.incrementAndGet() > maxConnections) { + ctx.close(); + log.info("[{}]{} channelRegistered超过最大连接数 {},因此关闭连接 {}",protocolName, ctx.channel(), maxConnections, ctx.channel()); + } else { + super.channelRegistered(ctx); + log.info("[{}]{} channelRegistered 当前连接数 {} / {}",protocolName, ctx.channel(), connectionsGauge.get(), maxConnections); + } + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + connectionsGauge.decrementAndGet(); + super.channelUnregistered(ctx); + log.info("[{}]{} channelUnregistered 当前连接数 {} / {}",protocolName, ctx.channel(), connectionsGauge.get(), maxConnections); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + channelGroup.add(ctx.channel()); + log.info("[{}]{} channelActive 当前连接数管道数 {} / {}",protocolName, ctx.channel(), channelGroup.size(), maxConnections); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + channelGroup.remove(ctx.channel()); + log.info("[{}]{} channelInactive 当前连接数管道数 {} / {}",protocolName, ctx.channel(), channelGroup.size(), maxConnections); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("[{}]{} ConnectionLimitHandler exceptionCaught",protocolName, ctx.channel(), cause); + ctx.close(); + } +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/IdleEventHandler.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/IdleEventHandler.java new file mode 100644 index 0000000..469db25 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/IdleEventHandler.java @@ -0,0 +1,35 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.handler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 心跳检测 + * + * @author baigod + */ +@Slf4j +@RequiredArgsConstructor +public class IdleEventHandler extends ChannelInboundHandlerAdapter { + + private final String protocolName; + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof IdleStateEvent event) { + if (event.state() == IdleState.READER_IDLE) { + ctx.close(); + log.info("[{}]{} 检测到空闲连接,连接关闭", protocolName, ctx.channel()); + } + } + } + +} diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/TracerHandler.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/TracerHandler.java new file mode 100644 index 0000000..cd44e81 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/listener/tcp/handler/TracerHandler.java @@ -0,0 +1,25 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.listener.tcp.handler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import sanbing.jcpp.infrastructure.util.mdc.MDCUtils; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; + +/** + * @author baigod + */ +public class TracerHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + + TracerContextUtil.newTracer("jcpp-protocol"); + + MDCUtils.recordTracer(); + + super.channelRead(ctx, msg); + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/ProtocolSessionRegistryProvider.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/ProtocolSessionRegistryProvider.java new file mode 100644 index 0000000..be674f8 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/ProtocolSessionRegistryProvider.java @@ -0,0 +1,30 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.provider; + +import sanbing.jcpp.protocol.domain.ProtocolSession; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * @author baigod + */ +public interface ProtocolSessionRegistryProvider { + + /** + * 注册会话 + */ + void register(ProtocolSession protocolSession); + + void unregister(UUID sessionId); + + CompletableFuture get(UUID sessionId); + + /** + * 活跃会话 + */ + void activate(ProtocolSession protocolSession); +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/ProtocolsConfigProvider.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/ProtocolsConfigProvider.java new file mode 100644 index 0000000..aafa9d9 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/ProtocolsConfigProvider.java @@ -0,0 +1,15 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.provider; + +import sanbing.jcpp.protocol.cfg.ProtocolCfg; + +/** + * @author baigod + */ +public interface ProtocolsConfigProvider { + + ProtocolCfg loadConfig(String protocol); +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/impl/DefaultProtocolSessionRegistryProvider.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/impl/DefaultProtocolSessionRegistryProvider.java new file mode 100644 index 0000000..c32ef51 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/impl/DefaultProtocolSessionRegistryProvider.java @@ -0,0 +1,115 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.provider.impl; + +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import sanbing.jcpp.infrastructure.util.async.JCPPThreadFactory; +import sanbing.jcpp.infrastructure.util.config.ThreadPoolConfiguration; +import sanbing.jcpp.protocol.domain.ProtocolSession; +import sanbing.jcpp.protocol.domain.SessionCloseReason; +import sanbing.jcpp.protocol.provider.ProtocolSessionRegistryProvider; + +import java.time.LocalDateTime; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + + +/** + * @author baigod + */ +@Service +@Slf4j +public class DefaultProtocolSessionRegistryProvider implements ProtocolSessionRegistryProvider { + private static final int INIT_CACHE_LIMIT = 100_000; + + @Value("${service.protocols.sessions.default-inactivity-timeout-in-sec}") + private int defaultInactivityTimeoutInSec; + + @Value("${service.protocols.sessions.default-state-check-interval-in-sec}") + private int defaultStateCheckIntervalInSec; + + @Getter + private final AsyncCache SESSION_CACHE = buildCache(); + + private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(JCPPThreadFactory.forName("session-state-checker")); + + @PostConstruct + public void init() { + scheduledExecutorService.scheduleAtFixedRate(() -> + SESSION_CACHE.asMap().forEach((id, sessionCompletableFuture) -> + sessionCompletableFuture.whenComplete((protocolSession, throwable) -> { + if (throwable == null && protocolSession != null) { + if (protocolSession.getLastActivityTime().isBefore(LocalDateTime.now().minusSeconds(defaultInactivityTimeoutInSec))) { + protocolSession.close(SessionCloseReason.INACTIVE); + unregister(protocolSession.getId()); + } + } + }) + ), defaultStateCheckIntervalInSec, defaultStateCheckIntervalInSec, TimeUnit.SECONDS); + } + + @PreDestroy + public void destroy() { + scheduledExecutorService.shutdownNow(); + } + + @Override + public void register(ProtocolSession protocolSession) { + + if (log.isDebugEnabled()) { + log.debug("Registering session {}", protocolSession); + } + + SESSION_CACHE.put(protocolSession.getId(), CompletableFuture.supplyAsync(() -> protocolSession, ThreadPoolConfiguration.JCPP_COMMON_THREAD_POOL)); + + } + + @Override + public void unregister(UUID sessionId) { + + log.info("Unregistering session {}", sessionId); + + SESSION_CACHE.synchronous().invalidate(sessionId); + } + + @Override + public CompletableFuture get(UUID sessionId) { + + log.debug("Get session {}", sessionId); + + return SESSION_CACHE.get(sessionId, uuid -> null); + + } + + @Override + public void activate(ProtocolSession protocolSession) { + + if (log.isDebugEnabled()) { + log.debug("Activating session {}", protocolSession); + } + + protocolSession.setLastActivityTime(LocalDateTime.now()); + + SESSION_CACHE.put(protocolSession.getId(), CompletableFuture.supplyAsync(() -> protocolSession, ThreadPoolConfiguration.JCPP_COMMON_THREAD_POOL)); + } + + private AsyncCache buildCache() { + return Caffeine.newBuilder() + .initialCapacity(INIT_CACHE_LIMIT) + .maximumSize(INIT_CACHE_LIMIT * 10) + .executor(ThreadPoolConfiguration.JCPP_COMMON_THREAD_POOL) + .buildAsync(); + } +} \ No newline at end of file diff --git a/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/impl/DefaultProtocolsConfigProvider.java b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/impl/DefaultProtocolsConfigProvider.java new file mode 100644 index 0000000..774d239 --- /dev/null +++ b/jcpp-protocol-api/src/main/java/sanbing/jcpp/protocol/provider/impl/DefaultProtocolsConfigProvider.java @@ -0,0 +1,37 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.provider.impl; + +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Service; +import sanbing.jcpp.infrastructure.util.config.ConstraintValidator; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.protocol.cfg.ProtocolCfg; +import sanbing.jcpp.protocol.provider.ProtocolsConfigProvider; + +import java.util.Map; + +@Setter +@Service +@Slf4j +@ConfigurationProperties("service") +public class DefaultProtocolsConfigProvider implements ProtocolsConfigProvider { + + private Map protocols; + + @Override + public ProtocolCfg loadConfig(String protocol) { + + ProtocolCfg protocolCfg = protocols.get(protocol); + + log.info("load {}'s configuration: \n{}", protocol, JacksonUtil.toPrettyString(protocolCfg)); + + ConstraintValidator.validateFields(protocolCfg, "'" + protocol + "' configuration is invalid:"); + + return protocolCfg; + } +} \ No newline at end of file diff --git a/jcpp-protocol-bootstrap/pom.xml b/jcpp-protocol-bootstrap/pom.xml new file mode 100644 index 0000000..d2fdb39 --- /dev/null +++ b/jcpp-protocol-bootstrap/pom.xml @@ -0,0 +1,75 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-protocol-bootstrap + jar + JChargePointProtocol Protocol Bootstrap Module + 前置协议服务引导程序 + + + ${basedir}/.. + 3.4.4 + + + + + sanbing + jcpp-protocol-yunkuaichong + + + + + application + + + org.springframework.boot + spring-boot-maven-plugin + + false + ZIP + sanbing.jcpp.protocol.JCPPProtocolServiceApplication + true + + true + ${project.basedir}/src/layers.xml + + + + + + repackage + build-info + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/jcpp-protocol-bootstrap/src/layers.xml b/jcpp-protocol-bootstrap/src/layers.xml new file mode 100644 index 0000000..ebf721a --- /dev/null +++ b/jcpp-protocol-bootstrap/src/layers.xml @@ -0,0 +1,33 @@ + + + + + + org/springframework/boot/loader/** + + + + + + + + + *:*:*SNAPSHOT + + + + + dependencies + spring-boot-loader + snapshot-dependencies + application + + diff --git a/jcpp-protocol-bootstrap/src/main/java/sanbing/jcpp/protocol/JCPPProtocolServiceApplication.java b/jcpp-protocol-bootstrap/src/main/java/sanbing/jcpp/protocol/JCPPProtocolServiceApplication.java new file mode 100644 index 0000000..7c381f8 --- /dev/null +++ b/jcpp-protocol-bootstrap/src/main/java/sanbing/jcpp/protocol/JCPPProtocolServiceApplication.java @@ -0,0 +1,42 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol; + +import org.springframework.boot.Banner; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.util.Arrays; + +/** + * @author baigod + */ +@SpringBootApplication(scanBasePackages = {"sanbing.jcpp.protocol", + "sanbing.jcpp.infrastructure.stats", + "sanbing.jcpp.infrastructure.queue", + "sanbing.jcpp.infrastructure.util"}) +@EnableAsync +@EnableScheduling +public class JCPPProtocolServiceApplication { + + private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; + private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "protocol-service"; + + public static void main(String[] args) { + new SpringApplicationBuilder(JCPPProtocolServiceApplication.class).bannerMode(Banner.Mode.LOG).run(updateArguments(args)); + } + + private static String[] updateArguments(String[] args) { + if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) { + String[] modifiedArgs = new String[args.length + 1]; + System.arraycopy(args, 0, modifiedArgs, 0, args.length); + modifiedArgs[args.length] = DEFAULT_SPRING_CONFIG_PARAM; + return modifiedArgs; + } + return args; + } +} \ No newline at end of file diff --git a/jcpp-protocol-bootstrap/src/main/resources/banner.txt b/jcpp-protocol-bootstrap/src/main/resources/banner.txt new file mode 100644 index 0000000..0c37cd9 --- /dev/null +++ b/jcpp-protocol-bootstrap/src/main/resources/banner.txt @@ -0,0 +1,12 @@ + + ___ ________ ________ ________ + |\ \|\ ____\|\ __ \|\ __ \ + \ \ \ \ \___|\ \ \|\ \ \ \|\ \ + __ \ \ \ \ \ \ \ ____\ \ ____\ +|\ \\_\ \ \ \____\ \ \___|\ \ \___| +\ \________\ \_______\ \__\ \ \__\ + \|________|\|_______|\|__| \|__| + +=================================================== +:: ${application.title} :: ${application.formatted-version} +=================================================== \ No newline at end of file diff --git a/jcpp-protocol-bootstrap/src/main/resources/log4j2.xml b/jcpp-protocol-bootstrap/src/main/resources/log4j2.xml new file mode 100644 index 0000000..34d9fdf --- /dev/null +++ b/jcpp-protocol-bootstrap/src/main/resources/log4j2.xml @@ -0,0 +1,56 @@ + + + + + /var/log/sanbing/jcpp + %d{yyyy-MM-dd HH:mm:ss:SSS} [%X{TRACE_ID}] [%t] %p %c{1} %m%n%throwable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jcpp-protocol-bootstrap/src/main/resources/protocol-service.yml b/jcpp-protocol-bootstrap/src/main/resources/protocol-service.yml new file mode 100644 index 0000000..4baf214 --- /dev/null +++ b/jcpp-protocol-bootstrap/src/main/resources/protocol-service.yml @@ -0,0 +1,152 @@ +server: + address: "${HTTP_BIND_ADDRESS:0.0.0.0}" + port: "${HTTP_BIND_PORT:8081}" + undertow: + buffer-size: "${SERVER_UNDERTOW_BUFFER_SIZE:16384}" + directBuffers: "${SERVER_UNDERTOW_DIRECT_BUFFERS:true}" + threads: + io: "${SERVER_UNDERTOW_THREADS_IO:4}" + worker: "${SERVER_UNDERTOW_THREADS_WORKER:128}" + max-http-post-size: "${SERVER_UNDERTOW_MAX_HTTP_POST_SIZE:10MB}" + no-request-timeout: "${SERVER_UNDERTOW_NO_REQUEST_TIMEOUT:10000}" + accesslog: + enabled: true + pattern: "%t %a %r %s (%D ms)" + dir: /var/log/sanbing/accesslog + options: + server: + record-request-start-time: true + +spring: + application: + name: "${SPRING_APPLICATION_NAME:java-charge-point-protocol-server}" + +management: + endpoints: + web: + exposure: + include: '${METRICS_ENDPOINTS_EXPOSE:prometheus,health}' + endpoint: + health: + show-details: always + +metrics: + enabled: "${METRICS_ENABLED:true}" + timer: + percentiles: "${METRICS_TIMER_PERCENTILES:0.5}" + +service: + # 服务类型:纯协议解析前置 - protocol,纯应用后端 - app,单体服务(包含protocol和app) - monolith + type: "${SERVICE_TYPE:protocol}" + # 可自定义的服务ID,如果不指定,则默认为HOSTNAME + id: "${SERVICE_ID:}" + protocols: + sessions: + default-inactivity-timeout-in-sec: "${PROTOCOLS_SESSIONS_DEFAULT_INACTIVITY_TIMEOUT_IN_SEC:600}" + default-state-check-interval-in-sec: "${PROTOCOLS_SESSIONS_DEFAULT_STATE_CHECK_INTERVAL_IN_SEC:60}" + yunkuaichongV150: + enabled: "${PROTOCOLS_YUNKUAICHONGV150_ENABLED:true}" + listener: + tcp: + bind-address: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_BIND_ADDRESS:0.0.0.0}" + bind-port: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_BIND_PORT:38001}" + boss-group-thread_count: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_BOSS_GROUP_THREADS:4}" + worker-group-thread-count: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_WORKER_GROUP_THREADS:16}" + so-keep-alive: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_SO_KEEPALIVE:true}" + so-backlog: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_SO_BACKLOG:128}" + so-rcvbuf: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_SO_RCVBUF:65536}" + so-sndbuf: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_SO_SNDBUF:65536}" + nodelay: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_NODELAY:true}" + handler: + idle-timeout-seconds: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_HANDLER_IDLE_TIMEOUT_SECONDS:600}" + max_connections: "${PROTOCOLS_YUNKUAICHONGV150_LISTENER_TCP_HANDLER_MAX_CONNECTIONS:100000}" + # 默认为二进制类型的拆包器 + # 可选JSON类型的拆包器 "${PROTOCOLS_YUNKUAICHONGV150_NETTY_HANDLER_BINARY_CONFIGURATION:type:JSON}" + # 可选纯文本类型的拆包器 "${PROTOCOLS_YUNKUAICHONGV150_NETTY_HANDLER_BINARY_CONFIGURATION:type:TEXT;maxFrameLength:128;stripDelimiter:true;messageSeparator:null;charsetName:UTF-8}" + configuration: "${PROTOCOLS_YUNKUAICHONGV150_NETTY_HANDLER_BINARY_CONFIGURATION:type:BINARY;decoder:sanbing.jcpp.protocol.listener.tcp.decoder.JCPPLengthFieldBasedFrameDecoder;byteOrder:LITTLE_ENDIAN;head:68;lengthFieldOffset:1;lengthFieldLength:1;lengthAdjustment:2;initialBytesToStrip:0}" + forwarder: + # 作为前置服务单独启时可选:kafka、kafka-sharding,未来计划扩展RocketMQ, GRpc、REST + type: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_TYPE:kafka}" + kafka: + topic: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_TOPIC:protocol_uplink}" + jcpp-partition: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_JCPP_PARTITION:true}" # 是否利用JCPP的分片框架 + # 以下配置只有在service.type为protocol时且jcpp-partition为false时才生效 + bootstrap-servers: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_SERVERS:10.102.12.102:9092}" + acks: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_ACKS:1}" + # # 可选 protobuf(推荐)、json + encoder: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_ENCODER:protobuf}" + retries: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_RETRIES:1}" + compression-type: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_COMPRESSION_TYPE:lz4}" # none, gzip, snappy, lz4, zstd + batch-size: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_BATCH_SIZE:16384}" + linger-ms: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_KAFKA_LINGER_MS:0}" + buffer-memory: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_BUFFER_MEMORY:33554432}" + other-properties: "${PROTOCOLS_YUNKUAICHONGV150_FORWARD_QUEUE_KAFKA_OTHER_PROPERTIES:}" + +# 应用程序服务注册中心配置 +zk: + enabled: "${ZOOKEEPER_ENABLED:true}" + url: "${ZOOKEEPER_URL:zookeeper:2181}" + retry-interval-ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" + connection-timeout-ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" + session-timeout-ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" + zk-dir: "${ZOOKEEPER_NODES_DIR:/jcpp}" + recalculate-delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" + +# 队列配置 +queue: + # 在protocol服务中只能选择 kafka + type: "${QUEUE_TYPE:kafka}" + partitions: + hash_function_name: "${QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 + in_memory: + stats: + print-interval-ms: "${QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}" + kafka: + bootstrap-servers: "${KAFKA_SERVERS:kafka:9092}" + ssl: + enabled: "${KAFKA_SSL_ENABLED:false}" + truststore-location: "${KAFKA_SSL_TRUSTSTORE_LOCATION:}" + truststore-password: "${KAFKA_SSL_TRUSTSTORE_PASSWORD:}" + keystore-location: "${KAFKA_SSL_KEYSTORE_LOCATION:}" + keystore-password: "${KAFKA_SSL_KEYSTORE_PASSWORD:}" + key-password: "${KAFKA_SSL_KEY_PASSWORD:}" + acks: "${KAFKA_ACKS:1}" + retries: "${KAFKA_RETRIES:1}" + compression-type: "${KAFKA_COMPRESSION_TYPE:lz4}" # none, gzip, snappy, lz4, zstd + batch-size: "${KAFKA_BATCH_SIZE:1048576}" + linger-ms: "${KAFKA_LINGER_MS:1}" + max-request-size: "${KAFKA_MAX_REQUEST_SIZE:1048576}" + max-in-flight-requests-per-connection: "${KAFKA_MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION:5}" + buffer-memory: "${BUFFER_MEMORY:33554432}" + replication-factor: "${QUEUE_KAFKA_REPLICATION_FACTOR:1}" + max-poll-interval-ms: "${QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}" + max-poll-records: "${QUEUE_KAFKA_MAX_POLL_RECORDS:10240}" + max-partition-fetch-bytes: "${QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" + fetch-max-bytes: "${QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request-timeout-ms: "${QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" + session-timeout-ms: "${QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" + auto-offset-reset: "${QUEUE_KAFKA_AUTO_OFFSET_RESET:earliest}" + other-inline: "${QUEUE_KAFKA_OTHER_PROPERTIES:}" + topic-properties: + app: "${QUEUE_KAFKA_APP_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" + consumer-stats: + enabled: "${QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" + print-interval-ms: "${QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}" + kafka-response-timeout-ms: "${QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}" + app: + topic: "${QUEUE_APP_TOPIC:protocol_uplink}" + poll-interval: "${QUEUE_APP_POLL_INTERVAL_MS:5}" + pack-processing-timeout: "${QUEUE_APP_PACK_PROCESSING_TIMEOUT_MS:2000}" + consumer-per-partition: "${QUEUE_APP_CONSUMER_PER_PARTITION:true}" + partitions: "${QUEUE_APP_PARTITIONS:10}" + # 可选 protobuf(推荐)、json,需要跟..forwarder.kafka.encoder保持一致 + decoder: "${QUEUE_APP_DECODER:protobuf}" + stats: + enabled: "${QUEUE_APP_STATS_ENABLED:true}" + print-interval-ms: "${QUEUE_APP_STATS_PRINT_INTERVAL_MS:60000}" + +thread-pool: + sharding: + hash_function_name: "${THREAD_POOL_SHARDING_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 + parallelism: "${THREAD_POOL_SHARDING_PARALLELISM:128}" + stats-print-interval-ms: "${THREAD_POOL_SHARDING_STATS_PRINT_INTERVAL_MS:10000}" diff --git a/jcpp-protocol-bootstrap/src/test/java/sanbing/jcpp/protocol/AbstractProtocolTestBase.java b/jcpp-protocol-bootstrap/src/test/java/sanbing/jcpp/protocol/AbstractProtocolTestBase.java new file mode 100644 index 0000000..a9ad896 --- /dev/null +++ b/jcpp-protocol-bootstrap/src/test/java/sanbing/jcpp/protocol/AbstractProtocolTestBase.java @@ -0,0 +1,38 @@ +package sanbing.jcpp.protocol; /** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +/** + * @author baigod + */ +@ActiveProfiles("test") +@SpringBootTest(classes = JCPPProtocolServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@AutoConfigureMockMvc +public class AbstractProtocolTestBase { + + static { + System.setProperty("spring.config.name", "protocol-service"); + } + + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected Environment environment; +} \ No newline at end of file diff --git a/jcpp-protocol-bootstrap/src/test/java/sanbing/jcpp/protocol/adapter/DownlinkControllerTest.java b/jcpp-protocol-bootstrap/src/test/java/sanbing/jcpp/protocol/adapter/DownlinkControllerTest.java new file mode 100644 index 0000000..fb5c959 --- /dev/null +++ b/jcpp-protocol-bootstrap/src/test/java/sanbing/jcpp/protocol/adapter/DownlinkControllerTest.java @@ -0,0 +1,147 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.adapter; + +import cn.hutool.core.util.HexUtil; +import com.fasterxml.jackson.databind.JsonNode; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import jakarta.annotation.Resource; +import org.junit.After; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.property.PropertyUtils; +import sanbing.jcpp.proto.gen.ProtocolProto; +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; +import sanbing.jcpp.protocol.AbstractProtocolTestBase; +import sanbing.jcpp.protocol.domain.DownlinkCmdEnum; +import sanbing.jcpp.protocol.domain.ProtocolSession; +import sanbing.jcpp.protocol.listener.tcp.configs.BinaryHandlerConfiguration; +import sanbing.jcpp.protocol.listener.tcp.decoder.JCPPLengthFieldBasedFrameDecoder; +import sanbing.jcpp.protocol.listener.tcp.decoder.TcpMsgDecoder; +import sanbing.jcpp.protocol.provider.impl.DefaultProtocolSessionRegistryProvider; + +import java.nio.ByteOrder; +import java.util.UUID; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static sanbing.jcpp.protocol.listener.tcp.configs.BinaryHandlerConfiguration.LITTLE_ENDIAN_BYTE_ORDER; + +class DownlinkControllerTest extends AbstractProtocolTestBase { + final String PROTOCOL_NAME = "yunkuaichongV150"; + + @Value("${service.protocols.yunkuaichongV150.listener.tcp.handler.configuration}") + private String yunkuaichongV150TcpHandler; + + @Value("${service.protocols.yunkuaichongV150.listener.tcp.bind-port}") + private int yunkuaichongV150TcpPort; + + @Resource + DefaultProtocolSessionRegistryProvider sessionRegistryProvider; + + private EventLoopGroup group; + private Channel channel; + + @BeforeEach + void setUp() throws InterruptedException { + final JsonNode cfgJson = JacksonUtil.valueToTree(PropertyUtils.getProps(yunkuaichongV150TcpHandler)); + + BinaryHandlerConfiguration binaryHandlerConfig = JacksonUtil.treeToValue(cfgJson, BinaryHandlerConfiguration.class); + + ByteOrder byteOrder = LITTLE_ENDIAN_BYTE_ORDER.equalsIgnoreCase(binaryHandlerConfig.getByteOrder()) + ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN; + + JCPPLengthFieldBasedFrameDecoder framer = new JCPPLengthFieldBasedFrameDecoder(binaryHandlerConfig.getHead(), byteOrder, + binaryHandlerConfig.getLengthFieldOffset(), binaryHandlerConfig.getLengthFieldLength(), + binaryHandlerConfig.getLengthAdjustment(), binaryHandlerConfig.getInitialBytesToStrip()); + + + group = new NioEventLoopGroup(); + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline() + .addLast(framer) + .addLast("tcpByteDecoderOverride", new TcpMsgDecoder<>(PROTOCOL_NAME, TcpMsgDecoder::toByteArray)) + .addLast(new SimpleChannelInboundHandler<>() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + + log.info("接收到字节码:{}", msg); + } + }); + } + }); + + // 连接到服务器 + ChannelFuture f = b.connect("127.0.0.1", yunkuaichongV150TcpPort).sync(); + channel = f.channel(); + } + + @After + public void tearDown() { + if (channel != null) { + channel.close(); + } + group.shutdownGracefully(); + } + + @Test + void remoteStartCharging() throws Exception { + // 先发送一段登录 + channel.writeAndFlush(Unpooled.wrappedBuffer(HexUtil.decodeHex("6822001900012023121200001001011047562e393572313300898604d11722d0348606024E87"))).sync(); + + // 停一会等注册完成 todo 也可以读下行消息判断是否登录成功 + Thread.sleep(1000); + + UUID messageId = UUID.randomUUID(); + ProtocolSession protocolSession = sessionRegistryProvider.getSESSION_CACHE().asMap().values().stream().findFirst().get().get(); + UUID sessionId = protocolSession.getId(); + UUID requestId = UUID.randomUUID(); + + // 创建 DownlinkRestMessage 实例 + String pileCode = "20231212000010"; + DownlinkRestMessage downlinkMsg = DownlinkRestMessage.newBuilder() + .setMessageIdMSB(messageId.getMostSignificantBits()) + .setMessageIdLSB(messageId.getLeastSignificantBits()) + .setSessionIdMSB(sessionId.getMostSignificantBits()) + .setSessionIdLSB(sessionId.getLeastSignificantBits()) + .setProtocolName(PROTOCOL_NAME) + .setPileCode(pileCode) + .setRequestIdMSB(requestId.getMostSignificantBits()) + .setRequestIdLSB(requestId.getLeastSignificantBits()) + .setDownlinkCmd(DownlinkCmdEnum.REMOTE_START_CHARGING.name()) + .setRemoteStartChargingRequest(ProtocolProto.RemoteStartChargingRequest.newBuilder() + .setPileCode(pileCode) + .setGunCode("01") + .setLimitCent(10000) + .setTradeNo("12345678901234567890") + .build()) + .build(); + + // 序列化为 Protobuf 字节流 + byte[] protobufContent = downlinkMsg.toByteArray(); + + // 调用 POST 接口 + mockMvc.perform(post("/api/onDownlink") + .contentType("application/x-protobuf") + .content(protobufContent)) + .andDo(print()) // 打印请求和响应信息 + .andExpect(status().is(HttpStatus.OK.value())); + } + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/pom.xml b/jcpp-protocol-yunkuaichong/pom.xml new file mode 100644 index 0000000..b702510 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/pom.xml @@ -0,0 +1,35 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-protocol-yunkuaichong + jar + JChargePointProtocol Yunkuaichong Protocol Module + 云快充1.5 + + + ${basedir}/.. + + + + + sanbing + jcpp-protocol-api + + + + diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/AbstractYunKuaiChongCmdExe.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/AbstractYunKuaiChongCmdExe.java new file mode 100644 index 0000000..f38e54a --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/AbstractYunKuaiChongCmdExe.java @@ -0,0 +1,154 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong; + +import cn.hutool.core.text.CharSequenceUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.apache.commons.lang3.StringUtils; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.proto.gen.ProtocolProto; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.listener.tcp.enums.SequenceNumberLength; +import sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.time.LocalTime; +import java.util.List; + +import static sanbing.jcpp.infrastructure.util.codec.ByteUtil.crcSum; +import static sanbing.jcpp.infrastructure.util.codec.ByteUtil.toBytes; + +/** + * @author baigod + */ +public class AbstractYunKuaiChongCmdExe { + + private static final byte TOP_BYTE = 0x00; + private static final byte PEAK_BYTE = 0x01; + private static final byte FLAT_BYTE = 0x02; + private static final byte VALLEY_BYTE = 0x03; + + protected static final byte YUNKUAICHONG_HEAD = 0x68; + protected static final int YUNKUAICHONG_NORMAL_ENCRYPTION_FLAG = 0; + + private static final DecimalFormat PRICING_ID_DECIMAL_FORMAT = new DecimalFormat("0000"); + + protected static String decodeTradeNo(byte[] tradeNo) { + String tradeNoStr = BCDUtil.toString(tradeNo); + return CharSequenceUtil.strip(tradeNoStr, "0", null); + } + + protected byte[] encodePricingId(long pricingId) { + return BCDUtil.toBytes(PRICING_ID_DECIMAL_FORMAT.format(pricingId % 10000)); + } + + protected static byte getFlagForCurrentTime(List periodList, LocalTime currentTime) { + for (ProtocolProto.PeriodProto period : periodList) { + LocalTime beginLt = LocalTime.parse(period.getBegin()); + LocalTime endLt = "00:00".equals(period.getEnd()) ? LocalTime.MAX : LocalTime.parse(period.getEnd()); + if ((currentTime.equals(beginLt) || currentTime.isAfter(beginLt)) && currentTime.isBefore(endLt)) { + switch (period.getFlag()) { + case TOP: + return TOP_BYTE; + case PEAK: + return PEAK_BYTE; + case FLAT: + return FLAT_BYTE; + case VALLEY: + return VALLEY_BYTE; + } + } + } + return FLAT_BYTE; // 默认情况下返回平价 + } + + protected static byte[] encodePileCode(String pileCode) { + if (StringUtils.length(pileCode) > 32) { + throw new IllegalArgumentException("云快充1.5可接受最大桩编号为14位"); + } + + String pileCodeStr = StringUtils.leftPad(pileCode, 14, '0'); + + return BCDUtil.toBytes(pileCodeStr); + } + + protected static byte[] encodeGunCode(String gunCode) { + if (StringUtils.length(gunCode) > 2) { + throw new IllegalArgumentException("云快充1.5可接受最大枪编号为2位"); + } + + String gunCodeStr = StringUtils.leftPad(gunCode, 2, '0'); + + return BCDUtil.toBytes(gunCodeStr); + } + + protected static byte[] encodeTradeNo(String tradeNo) { + if (StringUtils.length(tradeNo) > 32) { + throw new IllegalArgumentException("云快充1.5可接受最大交易流水号为32位"); + } + + String tradeNoStr = StringUtils.leftPad(tradeNo, 32, '0'); + + return BCDUtil.toBytes(tradeNoStr); + } + + + protected byte[] encode(YunKuaiChongV150DownlinkCmdEnum downlinkCmd, + int seqNo, + int encryptionFlag, + ByteBuf msgBody) { + int msgBodyLength = msgBody.readableBytes(); + ByteBuf response = Unpooled.buffer(msgBodyLength + 6); + response.writeByte(YUNKUAICHONG_HEAD); + response.writeByte(msgBodyLength + 4); + response.writeShortLE(seqNo); + response.writeByte(encryptionFlag); + response.writeByte(downlinkCmd.getCmd()); + response.writeBytes(msgBody); + + // 帧校验域:从序列号域到数据域的 CRC 校验,校验多项式为 0x180D,低字节在前,高字节在后 + byte[] checkArr = new byte[msgBodyLength + 4]; + System.arraycopy(response.array(), 2, checkArr, 0, checkArr.length); + + response.writeShortLE(crcSum(checkArr)); + + return toBytes(response); + } + + protected void encodeAndWriteFlush(YunKuaiChongV150DownlinkCmdEnum downlinkCmd, + int seqNo, + int encryptionFlag, + ByteBuf msgBody, + TcpSession tcpSession) { + + byte[] encode = encode(downlinkCmd, seqNo, encryptionFlag, msgBody); + + tcpSession.writeAndFlush(Unpooled.copiedBuffer(encode)); + } + + protected void encodeAndWriteFlush(YunKuaiChongV150DownlinkCmdEnum downlinkCmd, + ByteBuf msgBody, + TcpSession tcpSession) { + + byte[] encode = encode(downlinkCmd, + tcpSession.nextSeqNo(SequenceNumberLength.SHORT), + YUNKUAICHONG_NORMAL_ENCRYPTION_FLAG, + msgBody); + + tcpSession.writeAndFlush(Unpooled.copiedBuffer(encode)); + } + + protected static BigDecimal reduceMagnification(long value, int magnification) { + return new BigDecimal(value).divide(new BigDecimal(magnification), 4, RoundingMode.HALF_UP); + } + + protected static BigDecimal reduceMagnification(long value, int magnification, int scale) { + return new BigDecimal(value).divide(new BigDecimal(magnification), scale, RoundingMode.HALF_UP); + } + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongDownlinkCmdExe.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongDownlinkCmdExe.java new file mode 100644 index 0000000..41c5f97 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongDownlinkCmdExe.java @@ -0,0 +1,17 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong; + +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; + +/** + * @author baigod + */ +public abstract class YunKuaiChongDownlinkCmdExe extends AbstractYunKuaiChongCmdExe{ + + public abstract void execute(TcpSession tcpSession, YunKuaiChongDwonlinkMessage yunKuaiChongDwonlinkMessage, ProtocolContext ctx); + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongDwonlinkMessage.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongDwonlinkMessage.java new file mode 100644 index 0000000..83db1a7 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongDwonlinkMessage.java @@ -0,0 +1,40 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; + +import java.io.Serializable; +import java.util.UUID; + +/** + * @author baigod + */ +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class YunKuaiChongDwonlinkMessage implements Serializable { + public static final byte SUCCESS_BYTE = 0x00; + public static final byte FAILURE_BYTE = 0x01; + + // 消息ID + private UUID id; + + // 请求ID(如有) + private UUID requestId; + + // 指令 + private int cmd; + + // 消息体 + private DownlinkRestMessage msg; + + // 上行消息 + private YunKuaiChongUplinkMessage requestData; + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongUplinkCmdExe.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongUplinkCmdExe.java new file mode 100644 index 0000000..450dd81 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongUplinkCmdExe.java @@ -0,0 +1,33 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; + +/** + * @author baigod + */ +@Slf4j +public abstract class YunKuaiChongUplinkCmdExe extends AbstractYunKuaiChongCmdExe{ + + public abstract void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx); + + protected static UplinkQueueMessage.Builder uplinkMessageBuilder(String messageKey, TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage) { + return UplinkQueueMessage.newBuilder() + .setMessageIdMSB(yunKuaiChongUplinkMessage.getId().getMostSignificantBits()) + .setMessageIdLSB(yunKuaiChongUplinkMessage.getId().getLeastSignificantBits()) + .setSessionIdMSB(tcpSession.getId().getMostSignificantBits()) + .setSessionIdLSB(tcpSession.getId().getLeastSignificantBits()) + .setRequestData(ByteString.copyFrom(JacksonUtil.writeValueAsBytes(yunKuaiChongUplinkMessage))) + .setMessageKey(messageKey) + .setProtocolName(tcpSession.getProtocolName()); + } + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongUplinkMessage.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongUplinkMessage.java new file mode 100644 index 0000000..b9a9aeb --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/YunKuaiChongUplinkMessage.java @@ -0,0 +1,51 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.UUID; + +@Data +@Accessors(chain = true) +public class YunKuaiChongUplinkMessage implements Serializable { + // 消息ID + private final UUID id; + + // 起始域 + private int head; + + // 数据长度 + private int dataLength; + + // 序列号 + private int sequenceNumber; + + // 加密标识 + private int encryptionFlag; + + // 指令 + private int cmd; + + // 消息体 + private byte[] msgBody; + + // 校验和 + private int checkSum; + + // 真实报文 + private byte[] rawFrame; + + public YunKuaiChongUplinkMessage(UUID id) { + this.id = id; + } + + public YunKuaiChongUplinkMessage() { + this(UUID.randomUUID()); + } + +} diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/annotation/YunKuaiChongCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/annotation/YunKuaiChongCmd.java new file mode 100644 index 0000000..d86321b --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/annotation/YunKuaiChongCmd.java @@ -0,0 +1,19 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.annotation; + +import java.lang.annotation.*; + +/** + * @author baigod + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface YunKuaiChongCmd { + + byte value(); + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/YunKuaiChongV15ProtocolMessageProcessor.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/YunKuaiChongV15ProtocolMessageProcessor.java new file mode 100644 index 0000000..43164b8 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/YunKuaiChongV15ProtocolMessageProcessor.java @@ -0,0 +1,224 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150; + +import cn.hutool.core.util.ClassUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.JCPPPair; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.DownlinkRestMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.ProtocolMessageProcessor; +import sanbing.jcpp.protocol.domain.ListenerToHandlerMsg; +import sanbing.jcpp.protocol.domain.SessionToHandlerMsg; +import sanbing.jcpp.protocol.forwarder.Forwarder; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDownlinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; +import sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum; + +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import static sanbing.jcpp.infrastructure.util.codec.ByteUtil.checkCrcSum; + +@Slf4j +public class YunKuaiChongV15ProtocolMessageProcessor extends ProtocolMessageProcessor { + private final Map UPLINK_CMD_EXE_MAP = new ConcurrentHashMap<>(); + private final Map DOWNLINK_CMD_EXE_MAP = new ConcurrentHashMap<>(); + + public YunKuaiChongV15ProtocolMessageProcessor(Forwarder forwarder, ProtocolContext protocolContext) { + super(forwarder, protocolContext); + + Set> cmdClasses = ClassUtil.scanPackageByAnnotation(ClassUtil.getPackage(this.getClass()), YunKuaiChongCmd.class); + cmdClasses.stream().filter(YunKuaiChongUplinkCmdExe.class::isAssignableFrom) + .forEach(clazz -> { + byte cmd = clazz.getAnnotation(YunKuaiChongCmd.class).value(); + try { + YunKuaiChongUplinkCmdExe yunKuaiChongUplinkCmdExe = (YunKuaiChongUplinkCmdExe) clazz.getDeclaredConstructor().newInstance(); + UPLINK_CMD_EXE_MAP.put(cmd, yunKuaiChongUplinkCmdExe); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + + cmdClasses.stream().filter(YunKuaiChongDownlinkCmdExe.class::isAssignableFrom) + .forEach(clazz -> { + byte cmd = clazz.getAnnotation(YunKuaiChongCmd.class).value(); + try { + YunKuaiChongDownlinkCmdExe yunKuaiChongDownlinkCmdExe = (YunKuaiChongDownlinkCmdExe) clazz.getDeclaredConstructor().newInstance(); + DOWNLINK_CMD_EXE_MAP.put(cmd, yunKuaiChongDownlinkCmdExe); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public void uplinkHandle(ListenerToHandlerMsg listenerToHandlerMsg) { + UUID msgId = listenerToHandlerMsg.id(); + byte[] msg = listenerToHandlerMsg.msg(); + TcpSession session = (TcpSession) listenerToHandlerMsg.session(); + + ByteBuf in = Unpooled.copiedBuffer(msg); + + in.markReaderIndex(); + + findStartFlag(in); + + // 判断是否可以读取报头,8个字节 + if (in.readableBytes() < 6) { + in.resetReaderIndex(); + return; + } + + // 起始标识, 固定为0x68 + int startFlag = in.readUnsignedByte(); + if (startFlag != 0x68) { + in.resetReaderIndex(); + return; + } + + // 数据长度 = 序列号域+加密标志+帧类型标志+消息体 + int dataLength = in.readUnsignedByte(); + + // 报文的流水号 + int seqNo = in.readUnsignedShortLE(); + + // 加密标志 + int encrpyFlag = in.readUnsignedByte(); + + // 帧类型标志 + int frameType = in.readUnsignedByte(); + + // 判断是否可以读取消息体,N-4个字节 + int msgBodyLength = dataLength - 4; + if (in.readableBytes() < msgBodyLength) { + in.resetReaderIndex(); + return; + } + + // 消息体 + byte[] msgBody = new byte[msgBodyLength]; + in.readBytes(msgBody); + + // 判断是否可以读取校验和, 2个字节 + if (in.readableBytes() < 2) { + in.resetReaderIndex(); + return; + } + + byte[] byCheckSum = new byte[2]; + in.readBytes(byCheckSum); + ByteBuf csTemp = Unpooled.buffer(); + csTemp.writeBytes(byCheckSum); + + // 校验校验和 + int checkSum = csTemp.readUnsignedShort(); + + byte[] checkData = new byte[dataLength]; + + System.arraycopy(msg, 2, checkData, 0, dataLength); + + JCPPPair checkResult = checkCrcSum(checkData, checkSum); + + if (!checkResult.getFirst()) { + csTemp.writeBytes(byCheckSum); + checkSum = csTemp.readUnsignedShortLE(); + checkResult = checkCrcSum(checkData, checkSum); + log.info("云快充V1.5检验和 第二次检查: checkResult:{}, checkSum:{}", checkResult, checkSum); + } + + if (!checkResult.getFirst()) { + log.info("云快充V1.5检验和不一致两次不通过 不处理! CMD:{},校验域:{},正确校验和:{}", frameType, checkSum, checkResult.getSecond()); + return; + } + + YunKuaiChongUplinkMessage message = new YunKuaiChongUplinkMessage(msgId); + message.setHead(startFlag); + message.setDataLength(dataLength); + message.setSequenceNumber(seqNo); + message.setEncryptionFlag(encrpyFlag); + message.setCmd(frameType); + message.setMsgBody(msgBody); + message.setCheckSum(checkSum); + message.setRawFrame(msg); + + exeCmd(message, session); + } + + @Override + public void downlinkHandle(SessionToHandlerMsg sessionToHandlerMsg) throws Exception { + TcpSession session = (TcpSession) sessionToHandlerMsg.session(); + + DownlinkRestMessage protocolDownlinkMsg = sessionToHandlerMsg.downlinkMsg(); + + int cmd = YunKuaiChongV150DownlinkCmdEnum.valueOf(protocolDownlinkMsg.getDownlinkCmd()).getCmd(); + + YunKuaiChongDwonlinkMessage message = new YunKuaiChongDwonlinkMessage(); + message.setId(new UUID(protocolDownlinkMsg.getMessageIdMSB(), protocolDownlinkMsg.getMessageIdLSB())); + message.setCmd(cmd); + message.setMsg(protocolDownlinkMsg); + + if (protocolDownlinkMsg.hasRequestIdMSB() && protocolDownlinkMsg.hasRequestIdLSB()) { + message.setRequestId(new UUID(protocolDownlinkMsg.getRequestIdMSB(), protocolDownlinkMsg.getRequestIdLSB())); + } + + if (protocolDownlinkMsg.hasRequestData()) { + message.setRequestData(JacksonUtil.fromBytes(protocolDownlinkMsg.getRequestData().toByteArray(), YunKuaiChongUplinkMessage.class)); + } + + exeCmd(message, session); + } + + private void exeCmd(YunKuaiChongUplinkMessage message, TcpSession session) { + YunKuaiChongUplinkCmdExe uplinkCmdExe = UPLINK_CMD_EXE_MAP.get((byte) message.getCmd()); + + if (uplinkCmdExe == null) { + + log.info("[{}] 云快充V1.5协议接收到未知的上行指令 {}", session, message.getCmd()); + + return; + } + + uplinkCmdExe.execute(session, message, protocolContext); + } + + private void exeCmd(YunKuaiChongDwonlinkMessage message, TcpSession session) { + YunKuaiChongDownlinkCmdExe downlinkCmdExe = DOWNLINK_CMD_EXE_MAP.get((byte) message.getCmd()); + + if (downlinkCmdExe == null) { + + log.info("[{}] 云快充V1.5协议接收到未知的下行指令 {}", session, message.getCmd()); + + return; + } + + downlinkCmdExe.execute(session, message, protocolContext); + } + + private static void findStartFlag(ByteBuf buf) { + int count = buf.readableBytes(); + for (int index = buf.readerIndex(); index < count - 1; index++) { + if (buf.getByte(index) == (byte) 0x68) { + buf.readerIndex(index); + return; + } + } + buf.resetReaderIndex(); + } + + +} diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/YunkuaichongV150ProtocolBootstrap.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/YunkuaichongV150ProtocolBootstrap.java new file mode 100644 index 0000000..6fb79d9 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/YunkuaichongV150ProtocolBootstrap.java @@ -0,0 +1,45 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150; + +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.annotation.ProtocolComponent; +import sanbing.jcpp.protocol.ProtocolBootstrap; +import sanbing.jcpp.protocol.ProtocolMessageProcessor; + +import static sanbing.jcpp.protocol.yunkuaichong.v150.YunkuaichongV150ProtocolBootstrap.PROTOCOL_NAME; + +/** + * @author baigod + */ + +@ProtocolComponent(PROTOCOL_NAME) +@Slf4j +public class YunkuaichongV150ProtocolBootstrap extends ProtocolBootstrap { + + public static final String PROTOCOL_NAME = "yunkuaichongV150"; + + @Override + protected String getProtocolName() { + return PROTOCOL_NAME; + } + + @Override + protected void _init() { + // do nothing + } + + @Override + protected void _destroy() { + // do nothing + } + + @Override + protected ProtocolMessageProcessor messageProcessor() { + return new YunKuaiChongV15ProtocolMessageProcessor(forwarder, protocolContext); + } + + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150BmsHandshakeULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150BmsHandshakeULCmd.java new file mode 100644 index 0000000..b5126a3 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150BmsHandshakeULCmd.java @@ -0,0 +1,118 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import cn.hutool.core.util.HexUtil; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import java.nio.charset.StandardCharsets; + +/** + * 云快充1.5.0 充电握手 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x15) +public class YunKuaiChongV150BmsHandshakeULCmd extends YunKuaiChongUplinkCmdExe { + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.debug("{} 云快充1.5.0充电握手", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + // 1.交易流水号 + byte[] tradeNoBytes = new byte[16]; + byteBuf.readBytes(tradeNoBytes); + String tradeNo = BCDUtil.toString(tradeNoBytes); + additionalInfo.put("交易流水号", tradeNo); + + // 2.桩编号 + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + additionalInfo.put("桩编号", tradeNo); + + // 3.抢号 + byte gunCodeByte = byteBuf.readByte(); + String gunCode = BCDUtil.toString(gunCodeByte); + additionalInfo.put("抢号", tradeNo); + + // 4.BMS 通信协议版本号 + byte[] bmsConnectVersionBytes = new byte[3]; + byteBuf.readBytes(bmsConnectVersionBytes); + additionalInfo.put("BMS 通信协议版本号", HexUtil.encodeHexStr(bmsConnectVersionBytes)); + + // 5.BMS 电池类型 + int bmsBatteryType = byteBuf.readUnsignedByte(); + additionalInfo.put("BMS电池类型", bmsBatteryType); + + // 6.BMS 整车动力蓄电池系统额定容量 + int bmsPowerCapacity = byteBuf.readUnsignedShortLE(); + additionalInfo.put("BMS整车动力蓄电池系统额定容量", bmsPowerCapacity); + + // 7.BMS 整车动力蓄电池系统额定总电压 + int bmsPowerMaxVoltage = byteBuf.readUnsignedShortLE(); + additionalInfo.put("BMS整车动力蓄电池系统额定总电压", bmsPowerMaxVoltage); + + // 8.BMS 电池生产厂商名称 + byte[] bmsFactoryBytes = new byte[4]; + byteBuf.readBytes(bmsFactoryBytes); + String bmsFactory = new String(bmsFactoryBytes, StandardCharsets.US_ASCII); + additionalInfo.put("BMS电池生产厂商名称", bmsFactory); + + // 9.BMS 电池组序号 + int bmsSerialNo = byteBuf.readIntLE(); + additionalInfo.put("BMS 电池组序号", bmsSerialNo); + + // 10.BMS 电池组生产日期年 + int bmsCreateYear = byteBuf.readUnsignedByte(); + additionalInfo.put("BMS 电池组生产日期年", bmsCreateYear); + + // 11.BMS 电池组生产日期月 + int bmsCreateMonth = byteBuf.readUnsignedByte(); + additionalInfo.put("BMS 电池组生产日期月", bmsCreateMonth); + + // 12.BMS 电池组生产日期日 + int bmsCreateDay = byteBuf.readUnsignedByte(); + additionalInfo.put("BMS 电池组生产日期日", bmsCreateDay); + + // 13.BMS 电池组充电次数 + int bmsChargeCount = byteBuf.readUnsignedMedium(); + additionalInfo.put("BMS 电池组充电次数", bmsChargeCount); + + // 14.BMS 电池组产权标识 + int bmsPropertyRightLabel = byteBuf.readUnsignedByte(); + additionalInfo.put("BMS 电池组产权标识", bmsPropertyRightLabel); + + // 15.预留位 + byteBuf.skipBytes(1); + + // 16.BMS 车辆识别码 + byte[] carVINBytes = new byte[17]; + byteBuf.readBytes(carVINBytes); + String bmsVinCode = new String(carVINBytes, StandardCharsets.US_ASCII); + additionalInfo.put("电动汽车唯一标识", bmsVinCode); + + // 17.BMS 软件版本号 + byte[] bmsSoftVersionBytes = new byte[8]; + byteBuf.readBytes(bmsSoftVersionBytes); + additionalInfo.put("BMS 软件版本号", HexUtil.encodeHexStr(bmsSoftVersionBytes)); + + // TODO 先打印日志,暂不转发 + log.debug("{} 云快充1.5.0充电握手信息解析完成:{}", tcpSession, additionalInfo); + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150HeartbeatULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150HeartbeatULCmd.java new file mode 100644 index 0000000..e95a2a4 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150HeartbeatULCmd.java @@ -0,0 +1,78 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.HeartBeatRequest; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.HEARTBEAT; + +/** + * 云快充1.5.0 充电桩心跳包 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x03) +public class YunKuaiChongV150HeartbeatULCmd extends YunKuaiChongUplinkCmdExe { + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.debug("{} 云快充1.5.0充电桩心跳包", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + + byte gunCodeByte = byteBuf.readByte(); + int gunCode = Integer.parseInt(BCDUtil.toString(gunCodeByte)); + additionalInfo.put("枪号", gunCode); + + int gunState = byteBuf.readUnsignedByte(); + additionalInfo.put("枪状态(0正常 1故障)", gunState); + + // 刷新前置会话 + ctx.getProtocolSessionRegistryProvider().activate(tcpSession); + + // 转发到后端 + HeartBeatRequest heartBeatRequest = HeartBeatRequest.newBuilder() + .setPileCode(pileCode) + .setRemoteAddress(tcpSession.getAddress().toString()) + .setNodeId(ctx.getServiceInfoProvider().getServiceId()) + .setNodeWebapiIpPort(ctx.getServiceInfoProvider().getServiceWebapiEndpoint()) + .setAdditionalInfo(additionalInfo.toString()) + .build(); + UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(heartBeatRequest.getPileCode(), tcpSession, yunKuaiChongUplinkMessage) + .setHeartBeatRequest(heartBeatRequest) + .build(); + tcpSession.getForwarder().sendMessage(uplinkQueueMessage); + + pingAck(tcpSession, yunKuaiChongUplinkMessage, pileCodeBytes, gunCodeByte); + } + + private void pingAck(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, byte[] pileCodeBytes, byte gunCodeByte) { + ByteBuf pingAckMsgBody = Unpooled.buffer(9); + pingAckMsgBody.writeBytes(pileCodeBytes); + pingAckMsgBody.writeByte(gunCodeByte); + pingAckMsgBody.writeByte(0); + + encodeAndWriteFlush(HEARTBEAT, + pingAckMsgBody, + tcpSession); + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginAckDLCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginAckDLCmd.java new file mode 100644 index 0000000..d7d0b70 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginAckDLCmd.java @@ -0,0 +1,118 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.CP56Time2aUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.mdc.MDCUtils; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.LoginResponse; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.listener.tcp.enums.SequenceNumberLength; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDownlinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import java.time.Instant; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import static sanbing.jcpp.infrastructure.util.config.ThreadPoolConfiguration.PROTOCOL_SESSION_SCHEDULED; +import static sanbing.jcpp.protocol.domain.SessionCloseReason.MANUALLY; +import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.FAILURE_BYTE; +import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.SUCCESS_BYTE; +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.LOGIN_ACK; +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.SYNC_TIME; + +/** + * 云快充1.5.0登录认证应答 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x02) +public class YunKuaiChongV150LoginAckDLCmd extends YunKuaiChongDownlinkCmdExe { + + @Override + public void execute(TcpSession tcpSession, YunKuaiChongDwonlinkMessage yunKuaiChongDwonlinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0登录认证应答", tcpSession); + + if (!yunKuaiChongDwonlinkMessage.getMsg().hasLoginResponse()) { + return; + } + + LoginResponse loginResponse = yunKuaiChongDwonlinkMessage.getMsg().getLoginResponse(); + + YunKuaiChongUplinkMessage requestData = JacksonUtil.fromBytes(yunKuaiChongDwonlinkMessage.getMsg().getRequestData().toByteArray(), YunKuaiChongUplinkMessage.class); + + // 获取上行报文 + byte[] uplinkRawFrame = requestData.getRawFrame(); + // 从上行报文中取出桩编号字节数组 + byte[] pileCodeBytes = Arrays.copyOfRange(uplinkRawFrame, 6, 13); + + if (loginResponse.getSuccess()) { + + // 构造并下发登录ACK + loginAck(tcpSession, pileCodeBytes, requestData, true); + + // 构造定时对时 + registerSyncTimeTask(tcpSession, pileCodeBytes, requestData); + + } else { + + log.info("云快充V1.5登录认证失败,服务端断开连接。 pileCode:{}", loginResponse.getPileCode()); + + // 构造并下发登录ACK + loginAck(tcpSession, pileCodeBytes, requestData, false); + + // 断开连接 + tcpSession.close(MANUALLY); + } + } + + private void loginAck(TcpSession tcpSession, byte[] pileCodeBytes, YunKuaiChongUplinkMessage requestData, boolean loginSuccess) { + // 创建ACK消息体7字节桩编号+1字节登录结果 + ByteBuf loginAckMsgBody = Unpooled.buffer(8); + loginAckMsgBody.writeBytes(pileCodeBytes); + loginAckMsgBody.writeByte(loginSuccess ? SUCCESS_BYTE : FAILURE_BYTE); + + encodeAndWriteFlush(LOGIN_ACK, + requestData.getSequenceNumber(), + requestData.getEncryptionFlag(), + loginAckMsgBody, + tcpSession); + } + + private void registerSyncTimeTask(TcpSession tcpSession, byte[] pileCodeBytes, YunKuaiChongUplinkMessage requestData) { + tcpSession.addSchedule("auto-sync-time", k -> { + log.info("{} 云快充1.5.0开始注册定时对时任务", tcpSession); + return PROTOCOL_SESSION_SCHEDULED.scheduleAtFixedRate(() -> + syncTime(tcpSession, pileCodeBytes, requestData), + 0, 8, TimeUnit.HOURS); + } + ); + } + + private void syncTime(TcpSession tcpSession, byte[] pileCodeBytes, YunKuaiChongUplinkMessage requestData) { + TracerContextUtil.newTracer(); + MDCUtils.recordTracer(); + log.info("{} 云快充1.5.0开始下发对时报文", tcpSession); + ByteBuf syncTimeMsgBody = Unpooled.buffer(14); + syncTimeMsgBody.writeBytes(pileCodeBytes); + syncTimeMsgBody.writeBytes(CP56Time2aUtil.encode(Instant.now())); + + encodeAndWriteFlush(SYNC_TIME, + tcpSession.nextSeqNo(SequenceNumberLength.SHORT), + requestData.getEncryptionFlag(), + syncTimeMsgBody, + tcpSession); + } + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginULCmd.java new file mode 100644 index 0000000..8bb1991 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150LoginULCmd.java @@ -0,0 +1,77 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.LoginRequest; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import java.nio.charset.StandardCharsets; + +/** + * 云快充1.5.0充电桩登录认证 + */ +@Slf4j +@YunKuaiChongCmd(0x01) +public class YunKuaiChongV150LoginULCmd extends YunKuaiChongUplinkCmdExe { + + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0登录认证请求", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + + int pileType = byteBuf.readUnsignedByte(); + additionalInfo.put("桩类型(0直流1交流)", pileType); + + int gunsNum = byteBuf.readUnsignedByte(); + additionalInfo.put("充电枪数量", gunsNum); + additionalInfo.put("通信协议版本", byteBuf.readUnsignedByte()); + byte[] bytes = new byte[8]; + byteBuf.readBytes(bytes); + additionalInfo.put("程序版本", new String(bytes, StandardCharsets.US_ASCII)); + additionalInfo.put("网络链接类型", byteBuf.readUnsignedByte()); + + byte[] simB = new byte[10]; + byteBuf.readBytes(simB); + String sim = BCDUtil.toString(simB); + additionalInfo.put("Sim卡", sim); + additionalInfo.put("运营商", byteBuf.readUnsignedByte()); + + tcpSession.addPileCode(pileCode); + + // 注册前置会话 + ctx.getProtocolSessionRegistryProvider().register(tcpSession); + + // 转发到后端 + LoginRequest loginRequest = LoginRequest.newBuilder() + .setPileCode(pileCode) + .setRemoteAddress(tcpSession.getAddress().toString()) + .setNodeId(ctx.getServiceInfoProvider().getServiceId()) + .setNodeWebapiIpPort(ctx.getServiceInfoProvider().getServiceWebapiEndpoint()) + .setAdditionalInfo(additionalInfo.toString()) + .build(); + UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(loginRequest.getPileCode(), tcpSession, yunKuaiChongUplinkMessage) + .setLoginRequest(loginRequest) + .build(); + tcpSession.getForwarder().sendMessage(uplinkQueueMessage); + } + +} diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150QueryPricingModelAckDLCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150QueryPricingModelAckDLCmd.java new file mode 100644 index 0000000..3094e62 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150QueryPricingModelAckDLCmd.java @@ -0,0 +1,93 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.FlagPriceProto; +import sanbing.jcpp.proto.gen.ProtocolProto.PeriodProto; +import sanbing.jcpp.proto.gen.ProtocolProto.PricingModelProto; +import sanbing.jcpp.proto.gen.ProtocolProto.QueryPricingResponse; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDownlinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import java.math.BigDecimal; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; + +import static sanbing.jcpp.proto.gen.ProtocolProto.PricingModelFlag.*; +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.QUERY_PRICING_ACK; + +/** + * 计费模型请求应答 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x0A) +public class YunKuaiChongV150QueryPricingModelAckDLCmd extends YunKuaiChongDownlinkCmdExe { + + @Override + public void execute(TcpSession tcpSession, YunKuaiChongDwonlinkMessage yunKuaiChongDwonlinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0计费模型请求应答", tcpSession); + + if (!yunKuaiChongDwonlinkMessage.getMsg().hasQueryPricingResponse()) { + return; + } + + QueryPricingResponse queryPricingResponse = yunKuaiChongDwonlinkMessage.getMsg().getQueryPricingResponse(); + + YunKuaiChongUplinkMessage requestData = JacksonUtil.fromBytes(yunKuaiChongDwonlinkMessage.getMsg().getRequestData().toByteArray(), YunKuaiChongUplinkMessage.class); + + long pricingId = queryPricingResponse.getPricingId(); + String pileCode = queryPricingResponse.getPileCode(); + PricingModelProto pricingModel = queryPricingResponse.getPricingModel(); + Map flagPriceMap = pricingModel.getFlagPriceMap(); + List periodList = pricingModel.getPeriodList(); + + // 从上行报文中取出桩编号字节数组 + byte[] pileCodeBytes = encodePileCode(pileCode); + + // 创建ACK消息体7字节桩编号+2字节计费模型编号+4x4x2字节尖峰平谷电价和服务费+1字节计损比例+48字节时段标识 + ByteBuf queryPricingAckMsgBody = Unpooled.buffer(90); + queryPricingAckMsgBody.writeBytes(pileCodeBytes); + queryPricingAckMsgBody.writeBytes(encodePricingId(pricingId)); + + // 4字节电价+4字节服务费 + BigDecimal accurate = new BigDecimal(1000); + queryPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(TOP.ordinal()).getElec()).multiply(accurate).intValue()); + queryPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(TOP.ordinal()).getServ()).multiply(accurate).intValue()); + queryPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(PEAK.ordinal()).getElec()).multiply(accurate).intValue()); + queryPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(PEAK.ordinal()).getServ()).multiply(accurate).intValue()); + queryPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(FLAT.ordinal()).getElec()).multiply(accurate).intValue()); + queryPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(FLAT.ordinal()).getServ()).multiply(accurate).intValue()); + queryPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(VALLEY.ordinal()).getElec()).multiply(accurate).intValue()); + queryPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(VALLEY.ordinal()).getServ()).multiply(accurate).intValue()); + + // 计损比例 + queryPricingAckMsgBody.writeByte(0); + + // 48段半小时 + byte[] bytes = new byte[48]; + LocalTime currentTime = LocalTime.MIDNIGHT; + for (int i = 0; i < 48; i++) { + bytes[i] = getFlagForCurrentTime(periodList, currentTime); + currentTime = currentTime.plusMinutes(30); // 每次时间增加30分钟 + } + queryPricingAckMsgBody.writeBytes(bytes); + + encodeAndWriteFlush(QUERY_PRICING_ACK, + queryPricingAckMsgBody, + tcpSession); + + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150QueryPricingModelULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150QueryPricingModelULCmd.java new file mode 100644 index 0000000..3741366 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150QueryPricingModelULCmd.java @@ -0,0 +1,50 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.QueryPricingRequest; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +/** + * 云快充1.5.0充电桩计费模型请求 + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x09) +public class YunKuaiChongV150QueryPricingModelULCmd extends YunKuaiChongUplinkCmdExe { + + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0充电桩计费模型请求", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + + // 转发到后端 + QueryPricingRequest queryPricingRequest = QueryPricingRequest.newBuilder() + .setPileCode(pileCode) + .setAdditionalInfo(additionalInfo.toString()) + .build(); + UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(queryPricingRequest.getPileCode(), tcpSession, yunKuaiChongUplinkMessage) + .setQueryPricingRequest(queryPricingRequest) + .build(); + tcpSession.getForwarder().sendMessage(uplinkQueueMessage); + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RealTimeDataULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RealTimeDataULCmd.java new file mode 100644 index 0000000..4b5db9e --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RealTimeDataULCmd.java @@ -0,0 +1,236 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.ChargingProgressProto; +import sanbing.jcpp.proto.gen.ProtocolProto.GunRunStatus; +import sanbing.jcpp.proto.gen.ProtocolProto.GunRunStatusProto; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 云快充1.5.0上传实时监测数据 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x13) +public class YunKuaiChongV150RealTimeDataULCmd extends YunKuaiChongUplinkCmdExe { + + // 故障说明列表 + private static final String[] faultDescriptions = { + "急停按钮动作故障", // Bit 1 + "无可用整流模块", // Bit 2 + "出风口温度过高", // Bit 3 + "交流防雷故障", // Bit 4 + "交直流模块 DC20 通信中断", // Bit 5 + "绝缘检测模块 FC08 通信中断", // Bit 6 + "电度表通信中断", // Bit 7 + "读卡器通信中断", // Bit 8 + "RC10 通信中断", // Bit 9 + "风扇调速板故障", // Bit 10 + "直流熔断器故障", // Bit 11 + "高压接触器故障", // Bit 12 + "门打开" // Bit 13 + }; + + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0上传实时监测数据", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + // 从Tracer总获取当前时间 + long ts = TracerContextUtil.getCurrentTracer().getTracerTs(); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + // 1.交易流水号 + byte[] tradeNoBytes = new byte[16]; + byteBuf.readBytes(tradeNoBytes); + String tradeNo = decodeTradeNo(tradeNoBytes); + + // 2.桩编号 + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + + // 3.抢号 + byte gunCodeByte = byteBuf.readByte(); + String gunCode = BCDUtil.toString(gunCodeByte); + + // 4.状态 0x00:离线 0x01:故障 0x02:空闲 0x03:充电 + int gunStatus = byteBuf.readUnsignedByte(); + + // 5.枪是否归位 0x00:否 0x01:是 0x02:未知 + int gunHoming = byteBuf.readUnsignedByte(); + additionalInfo.put("枪是否归位(0否1是)", gunHoming); + + // 6.是否插枪 0x00:否 0x01:是 + int gunInsert = byteBuf.readUnsignedByte(); + + // 7.输出电压 + BigDecimal outputVoltage = reduceMagnification(byteBuf.readUnsignedShortLE(), 10); + + // 8.输出电流 + BigDecimal outputCurrent = reduceMagnification(byteBuf.readUnsignedShortLE(), 10); + + // 9.枪线温度 + short gunLineTemperature = byteBuf.readUnsignedByte(); + additionalInfo.put("枪线温度", gunLineTemperature); + + // 10.枪线编码 + long gunLineCode = byteBuf.readLongLE(); + additionalInfo.put("枪线编码", gunLineCode); + + // 11.soc + int soc = byteBuf.readUnsignedByte(); + + // 12.电池组最高温度 + short maxBatteryTemperature = byteBuf.readUnsignedByte(); + additionalInfo.put("电池组最高温度", maxBatteryTemperature); + + // 13.累计充电时间(分钟) + int totalChargeTime = byteBuf.readUnsignedShortLE(); + + // 14.剩余时间(分钟) + int remainMin = byteBuf.readUnsignedShortLE(); + additionalInfo.put("剩余时间", remainMin); + + //15.充电度数(kWh) + BigDecimal chargeEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000, 4); + additionalInfo.put("充电度数", chargeEnergy); + + //16.计损充电度数(kWh) + BigDecimal loseEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000, 4); + + // 17.已充金额 (电费+服务费)*计损充电度数 + BigDecimal chargeAmount = reduceMagnification(byteBuf.readUnsignedIntLE(), 100); + + // 18.硬件故障 测试发现需要使用小端计算bit, 然后对照故障表查询故障码 + byte[] warnCodeBytes = new byte[2]; + byteBuf.readBytes(warnCodeBytes); + // 解析 14 个比特位 + List faults = getFaultDescriptions(parseFaults(warnCodeBytes)); + + // 抢状态 + GunRunStatus gunRunStatus = parseGUnRunStatus(gunStatus, gunInsert, tradeNo); + GunRunStatusProto.Builder gunRunStatusProtoBuilder = GunRunStatusProto.newBuilder() + .setTs(ts) + .setPileCode(pileCode) + .setGunCode(gunCode) + .setGunRunStatus(gunRunStatus) + .addAllFaultMessages(faults) + .setAdditionalInfo(additionalInfo.toString()); + + // 转发到后端 + UplinkQueueMessage gunRunStatusMessage = uplinkMessageBuilder(pileCode, tcpSession, yunKuaiChongUplinkMessage) + .setGunRunStatusProto(gunRunStatusProtoBuilder) + .build(); + + tcpSession.getForwarder().sendMessage(gunRunStatusMessage); + + if (StringUtils.isNotBlank(tradeNo)) { + + // 充电进度 + ChargingProgressProto.Builder chargingProgressProtoBuilder = ChargingProgressProto.newBuilder() + .setTs(ts) + .setPileCode(pileCode) + .setGunCode(gunCode) + .setTradeNo(tradeNo) + .setOutputVoltage(outputVoltage.floatValue()) + .setOutputCurrent(outputCurrent.floatValue()) + .setSoc(soc) + .setTotalChargingDurationMin(totalChargeTime) + .setTotalChargingEnergyKWh(loseEnergy.floatValue()) + .setTotalChargingCostCent(chargeAmount.longValue()) + .setAdditionalInfo(additionalInfo.toString()); + + UplinkQueueMessage chargingProgressMessage = uplinkMessageBuilder(pileCode, tcpSession, yunKuaiChongUplinkMessage) + .setChargingProgressProto(chargingProgressProtoBuilder) + .build(); + + tcpSession.getForwarder().sendMessage(chargingProgressMessage); + } + + } + + /** + * 解释枪运行状态 + */ + private static GunRunStatus parseGUnRunStatus(int gunStatus, int gunInsert, String tradeNo) { + GunRunStatus gunRunStatus = GunRunStatus.UNKNOWN; + if (gunStatus == 0) { + gunRunStatus = GunRunStatus.FAULT; + } else if (gunStatus == 1) { + gunRunStatus = GunRunStatus.FAULT; + } else if (gunStatus == 2) { + gunRunStatus = GunRunStatus.IDLE; + if (gunInsert == 1) { + gunRunStatus = GunRunStatus.INSERTED; + } + } else if (gunStatus == 3) { + if (StringUtils.isBlank(tradeNo)) { + gunRunStatus = GunRunStatus.INSERTED; + } else { + gunRunStatus = GunRunStatus.CHARGING; + } + } + return gunRunStatus; + } + + + public static boolean[] parseFaults(byte[] bytes) { + // 确保输入有效 + if (bytes.length != 2) { + throw new IllegalArgumentException("输入 byte 数组长度不为 2"); + } + + // 创建一个布尔数组来存储故障状态 + boolean[] faults = new boolean[14]; + + // 读取每个比特并设置到布尔数组中 + for (int i = 0; i < 14; i++) { + // 计算对应的字节和比特位置 + int byteIndex = i / 8; // 字节索引 + int bitIndex = i % 8; // 比特索引 + + // 使用位运算检查该比特位 + faults[i] = ((bytes[byteIndex] >> bitIndex) & 1) == 1; // 如果为 1 则故障 + } + + return faults; + } + + public static List getFaultDescriptions(boolean[] faults) { + List faultList = new ArrayList<>(); + + // 遍历布尔数组,筛选出有故障的说明 + for (int i = 0; i < faults.length; i++) { + if (faults[i]) { + faultList.add(faultDescriptions[i]); + } + } + + // 转换 List 为数组并返回 + return faultList; + } + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStartDLCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStartDLCmd.java new file mode 100644 index 0000000..27064f4 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStartDLCmd.java @@ -0,0 +1,73 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.RemoteStartChargingRequest; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDownlinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.REMOTE_START_CHARGING; + +/** + * 云快充1.5.0 运营平台远程控制启机 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x34) +public class YunKuaiChongV150RemoteStartDLCmd extends YunKuaiChongDownlinkCmdExe { + + @Override + public void execute(TcpSession tcpSession, YunKuaiChongDwonlinkMessage yunKuaiChongDwonlinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0运营平台远程控制启机", tcpSession); + + if (!yunKuaiChongDwonlinkMessage.getMsg().hasRemoteStartChargingRequest()) { + return; + } + + RemoteStartChargingRequest remoteStartChargingRequest = yunKuaiChongDwonlinkMessage.getMsg().getRemoteStartChargingRequest(); + String pileCode = remoteStartChargingRequest.getPileCode(); + String gunCode = remoteStartChargingRequest.getGunCode(); + String tradeNo = remoteStartChargingRequest.getTradeNo(); + int limitCent = remoteStartChargingRequest.getLimitCent(); + + byte[] cardNo = encodeCardNo(tradeNo); + + ByteBuf msgBody = Unpooled.buffer(44); + // 交易流水号 + msgBody.writeBytes(encodeTradeNo(tradeNo)); + // 桩编码 + msgBody.writeBytes(encodePileCode(pileCode)); + // 枪号 + msgBody.writeBytes(encodeGunCode(gunCode)); + // 逻辑卡号 BCD码 + msgBody.writeBytes(cardNo); + // 物理卡号 + msgBody.writeBytes(cardNo); + // 账户余额 + msgBody.writeIntLE(limitCent); + + encodeAndWriteFlush(REMOTE_START_CHARGING, + msgBody, + tcpSession); + } + + /** + * 用交易流水号做卡号 + */ + private static byte[] encodeCardNo(String tradeNo) { + tradeNo = StringUtils.right(tradeNo, 16); + tradeNo = StringUtils.leftPad(tradeNo, 16, '0'); + return BCDUtil.toBytes(tradeNo); + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStartResultULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStartResultULCmd.java new file mode 100644 index 0000000..6dca0e4 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStartResultULCmd.java @@ -0,0 +1,93 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.RemoteStartChargingResponse; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +/** + * 云快充1.5.0 远程启动充电命令回复 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x33) +public class YunKuaiChongV150RemoteStartResultULCmd extends YunKuaiChongUplinkCmdExe { + + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0远程启动充电命令回复", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + // 从Tracer总获取当前时间 + long ts = TracerContextUtil.getCurrentTracer().getTracerTs(); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + // 1.交易流水号 + byte[] tradeNoBytes = new byte[16]; + byteBuf.readBytes(tradeNoBytes); + String tradeNo = decodeTradeNo(tradeNoBytes); + + // 2.桩编号 + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + + // 3.抢号 + byte gunCodeByte = byteBuf.readByte(); + String gunCode = BCDUtil.toString(gunCodeByte); + + // 4.命令执行结果 0x00失败 0x01成功 + boolean isSuccess = (byteBuf.readByte() == 0x01); + + // 5.失败原因 0无 1设备编码不匹配 2枪已在充电 3设备故障 4设备离线 5未插枪 + byte failReasonByte = byteBuf.readByte(); + String failReason = mapFailCode(failReasonByte); + + RemoteStartChargingResponse remoteStartChargingResponse = RemoteStartChargingResponse.newBuilder() + .setTs(ts) + .setPileCode(pileCode) + .setGunCode(gunCode) + .setTradeNo(tradeNo) + .setSuccess(isSuccess) + .setFailReason(failReason) + .setAdditionalInfo(additionalInfo.toString()) + .build(); + + // 转发到后端 + UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(pileCode, tcpSession, yunKuaiChongUplinkMessage) + .setRemoteStartChargingResponse(remoteStartChargingResponse) + .build(); + + tcpSession.getForwarder().sendMessage(uplinkQueueMessage); + } + + public static String mapFailCode(byte failCode) { + return switch (failCode) { + case 0x00 -> "无"; + case 0x01 -> "设备编号不匹配"; + case 0x02 -> "枪已在充电"; + case 0x03 -> "设备故障"; + case 0x04 -> "设备离线"; + case 0x05 -> "未插枪"; + case 0x33 -> "充电失败"; // 充电失败或其他相关信息 + case 0x34 -> "待启充"; // 示例,处理收到充电命令 + default -> "未知错误代码"; + }; + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStopDLCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStopDLCmd.java new file mode 100644 index 0000000..67c4793 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStopDLCmd.java @@ -0,0 +1,49 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.proto.gen.ProtocolProto.RemoteStopChargingRequest; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDownlinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.REMOTE_START_CHARGING; + +/** + * 云快充1.5.0 运营平台远程停机 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x36) +public class YunKuaiChongV150RemoteStopDLCmd extends YunKuaiChongDownlinkCmdExe { + @Override + public void execute(TcpSession tcpSession, YunKuaiChongDwonlinkMessage yunKuaiChongDwonlinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0运营平台远程停机", tcpSession); + + if (!yunKuaiChongDwonlinkMessage.getMsg().hasRemoteStopChargingRequest()) { + return; + } + + RemoteStopChargingRequest remoteStopChargingRequest = yunKuaiChongDwonlinkMessage.getMsg().getRemoteStopChargingRequest(); + String pileCode = remoteStopChargingRequest.getPileCode(); + String gunCode = remoteStopChargingRequest.getGunCode(); + + ByteBuf msgBody = Unpooled.buffer(44); + // 桩编码 + msgBody.writeBytes(encodePileCode(pileCode)); + // 枪号 + msgBody.writeBytes(encodeGunCode(gunCode)); + + encodeAndWriteFlush(REMOTE_START_CHARGING, + msgBody, + tcpSession); + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStopResultULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStopResultULCmd.java new file mode 100644 index 0000000..dbda740 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150RemoteStopResultULCmd.java @@ -0,0 +1,82 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.infrastructure.util.trace.TracerContextUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.RemoteStopChargingResponse; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +/** + * 云快充1.5.0 远程停机命令回复 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x35) +public class YunKuaiChongV150RemoteStopResultULCmd extends YunKuaiChongUplinkCmdExe { + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0远程停机命令回复", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + // 从Tracer总获取当前时间 + long ts = TracerContextUtil.getCurrentTracer().getTracerTs(); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + // 1.桩编号 + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + + // 2.抢号 + byte gunCodeByte = byteBuf.readByte(); + String gunCode = BCDUtil.toString(gunCodeByte); + + // 3.命令执行结果 0x00失败 0x01成功 + boolean isSuccess = (byteBuf.readByte() == 0x01); + + // 4.失败原因 0无 1设备编码不匹配 2枪已在充电 3设备故障 4设备离线 5未插枪 + byte failReasonByte = byteBuf.readByte(); + String failReason = mapFailCode(failReasonByte); + + RemoteStopChargingResponse remoteStopChargingResponse = RemoteStopChargingResponse.newBuilder() + .setTs(ts) + .setPileCode(pileCode) + .setGunCode(gunCode) + .setSuccess(isSuccess) + .setFailReason(failReason) + .setAdditionalInfo(additionalInfo.toString()) + .build(); + + // 转发到后端 + UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(pileCode, tcpSession, yunKuaiChongUplinkMessage) + .setRemoteStopChargingResponse(remoteStopChargingResponse) + .build(); + + tcpSession.getForwarder().sendMessage(uplinkQueueMessage); + } + + public static String mapFailCode(byte failCode) { + return switch (failCode) { + case 0x00 -> "无"; + case 0x01 -> "设备编号不匹配"; + case 0x02 -> "枪未处于充电状态"; + case 0x03 -> "其他"; + default -> "未知错误"; // 可以根据需求自定义 + }; + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150SetPricingModelAckULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150SetPricingModelAckULCmd.java new file mode 100644 index 0000000..8e04b1b --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150SetPricingModelAckULCmd.java @@ -0,0 +1,60 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.SetPricingResponse; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.QUERY_PRICING_ACK; + +/** + * 云快充1.5.0 计费模型应答 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x57) +public class YunKuaiChongV150SetPricingModelAckULCmd extends YunKuaiChongUplinkCmdExe { + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0计费模型应答", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + // 1.桩编号 + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + + // 2.设置结果 0x00:失败 0x01:成功 + boolean isSuccess = (byteBuf.readByte() == 0x01); + + // 从缓存取上个请求的pricingId + Object pricingId = tcpSession.getRequestCache().asMap().getOrDefault(pileCode + QUERY_PRICING_ACK.getCmd(), null); + + if (pricingId instanceof Long pricingIdL) { + // 转发到后端 + SetPricingResponse setPricingResponse = SetPricingResponse.newBuilder() + .setPileCode(pileCode) + .setSuccess(isSuccess) + .setPricingId(pricingIdL) + .build(); + UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(setPricingResponse.getPileCode(), tcpSession, yunKuaiChongUplinkMessage) + .setSetPricingResponse(setPricingResponse) + .build(); + tcpSession.getForwarder().sendMessage(uplinkQueueMessage); + } + + + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150SetPricingModelDLCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150SetPricingModelDLCmd.java new file mode 100644 index 0000000..bd94432 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150SetPricingModelDLCmd.java @@ -0,0 +1,91 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.proto.gen.ProtocolProto.FlagPriceProto; +import sanbing.jcpp.proto.gen.ProtocolProto.PeriodProto; +import sanbing.jcpp.proto.gen.ProtocolProto.PricingModelProto; +import sanbing.jcpp.proto.gen.ProtocolProto.SetPricingRequest; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDownlinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import java.math.BigDecimal; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; + +import static sanbing.jcpp.proto.gen.ProtocolProto.PricingModelFlag.*; +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.QUERY_PRICING_ACK; +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.SET_PRICING; + +/** + * 云快充1.5.0 计费模型设置 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x58) +public class YunKuaiChongV150SetPricingModelDLCmd extends YunKuaiChongDownlinkCmdExe { + @Override + public void execute(TcpSession tcpSession, YunKuaiChongDwonlinkMessage yunKuaiChongDwonlinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0计费模型设置", tcpSession); + + if (!yunKuaiChongDwonlinkMessage.getMsg().hasSetPricingRequest()) { + return; + } + + SetPricingRequest setPricingRequest = yunKuaiChongDwonlinkMessage.getMsg().getSetPricingRequest(); + + long pricingId = setPricingRequest.getPricingId(); + String pileCode = setPricingRequest.getPileCode(); + PricingModelProto pricingModel = setPricingRequest.getPricingModel(); + Map flagPriceMap = pricingModel.getFlagPriceMap(); + List periodList = pricingModel.getPeriodList(); + + // 反转取出桩编号字节数组 + byte[] pileCodeBytes = encodePileCode(pileCode); + + // 创建ACK消息体7字节桩编号+2字节计费模型编号+4x4x2字节尖峰平谷电价和服务费+1字节计损比例+48字节时段标识 + ByteBuf setPricingAckMsgBody = Unpooled.buffer(90); + setPricingAckMsgBody.writeBytes(pileCodeBytes); + setPricingAckMsgBody.writeBytes(encodePricingId(pricingId)); + + // 4字节电价+4字节服务费 + BigDecimal accurate = new BigDecimal(1000); + setPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(TOP.ordinal()).getElec()).multiply(accurate).intValue()); + setPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(TOP.ordinal()).getServ()).multiply(accurate).intValue()); + setPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(PEAK.ordinal()).getElec()).multiply(accurate).intValue()); + setPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(PEAK.ordinal()).getServ()).multiply(accurate).intValue()); + setPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(FLAT.ordinal()).getElec()).multiply(accurate).intValue()); + setPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(FLAT.ordinal()).getServ()).multiply(accurate).intValue()); + setPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(VALLEY.ordinal()).getElec()).multiply(accurate).intValue()); + setPricingAckMsgBody.writeIntLE(new BigDecimal(flagPriceMap.get(VALLEY.ordinal()).getServ()).multiply(accurate).intValue()); + + // 计损比例 + setPricingAckMsgBody.writeByte(0); + + // 48段半小时 + byte[] bytes = new byte[48]; + LocalTime currentTime = LocalTime.MIDNIGHT; + for (int i = 0; i < 48; i++) { + bytes[i] = getFlagForCurrentTime(periodList, currentTime); + currentTime = currentTime.plusMinutes(30); // 每次时间增加30分钟 + } + setPricingAckMsgBody.writeBytes(bytes); + + // 放进缓存后再下发 + tcpSession.getRequestCache().put(pileCode + QUERY_PRICING_ACK.getCmd(), Long.valueOf(pricingId)); + + encodeAndWriteFlush(SET_PRICING, + setPricingAckMsgBody, + tcpSession); + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150TransactionRecordAckDLCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150TransactionRecordAckDLCmd.java new file mode 100644 index 0000000..417db72 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150TransactionRecordAckDLCmd.java @@ -0,0 +1,53 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.TransactionRecordAck; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDownlinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.FAILURE_BYTE; +import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.SUCCESS_BYTE; +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.VERIFY_PRICING_ACK; + +/** + * 云快充1.5.0 交易记录确认 + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x40) +public class YunKuaiChongV150TransactionRecordAckDLCmd extends YunKuaiChongDownlinkCmdExe { + @Override + public void execute(TcpSession tcpSession, YunKuaiChongDwonlinkMessage yunKuaiChongDwonlinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0交易记录确认", tcpSession); + + if (!yunKuaiChongDwonlinkMessage.getMsg().hasTransactionRecordAck()) { + return; + } + + TransactionRecordAck transactionRecordAck = yunKuaiChongDwonlinkMessage.getMsg().getTransactionRecordAck(); + + YunKuaiChongUplinkMessage requestData = JacksonUtil.fromBytes(yunKuaiChongDwonlinkMessage.getMsg().getRequestData().toByteArray(), YunKuaiChongUplinkMessage.class); + + // 创建ACK消息体16字节交易流水号 + 1字节确认结果 + ByteBuf msgBody = Unpooled.buffer(17); + msgBody.writeBytes(encodeTradeNo(transactionRecordAck.getTradeNo())); + msgBody.writeByte(transactionRecordAck.getSuccess() ? SUCCESS_BYTE : FAILURE_BYTE); + + encodeAndWriteFlush(VERIFY_PRICING_ACK, + requestData.getSequenceNumber(), + requestData.getEncryptionFlag(), + msgBody, + tcpSession); + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150TransactionRecordULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150TransactionRecordULCmd.java new file mode 100644 index 0000000..931386b --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150TransactionRecordULCmd.java @@ -0,0 +1,290 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.codec.CP56Time2aUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.TransactionRecord; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.Instant; + +/** + * 云快充1.5.0 交易记录 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x3B) +public class YunKuaiChongV150TransactionRecordULCmd extends YunKuaiChongUplinkCmdExe { + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0交易记录", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + // 1.交易流水号 + byte[] tradeNoBytes = new byte[16]; + byteBuf.readBytes(tradeNoBytes); + String tradeNo = decodeTradeNo(tradeNoBytes); + + // 2.桩编号 + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + + // 3.抢号 + byte gunCodeByte = byteBuf.readByte(); + String gunCode = BCDUtil.toString(gunCodeByte); + + // 4.开始时间 + byte[] startTimeBytes = new byte[7]; + byteBuf.readBytes(startTimeBytes); + Instant startTime = CP56Time2aUtil.decode(startTimeBytes); + + // 5.结束时间 + byte[] endTimeBytes = new byte[7]; + byteBuf.readBytes(endTimeBytes); + Instant endTime = CP56Time2aUtil.decode(endTimeBytes); + + // 6.尖单价 + BigDecimal topPrice = reduceMagnification(byteBuf.readUnsignedIntLE(), 1000); + additionalInfo.put("尖单价", topPrice); + // 7. 尖电量 + BigDecimal topEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000); + // 8.计损尖电量 + BigDecimal topLoseEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000); + additionalInfo.put("计损尖电量", topLoseEnergy); + // 9.尖金额 + BigDecimal topAmount = reduceMagnification(byteBuf.readUnsignedIntLE(), 100); + + // 10.峰单价 + BigDecimal peakPrice = reduceMagnification(byteBuf.readUnsignedIntLE(), 1000); + additionalInfo.put("峰单价", peakPrice); + // 11. 峰电量 + BigDecimal peakEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000); + // 12.计损峰电量 + BigDecimal peakLoseEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000); + additionalInfo.put("计损峰电量", peakLoseEnergy); + // 13.峰金额 + BigDecimal peakAmount = reduceMagnification(byteBuf.readUnsignedIntLE(), 100); + + // 14.平单价 + BigDecimal flatPrice = reduceMagnification(byteBuf.readUnsignedIntLE(), 1000); + additionalInfo.put("平单价", flatPrice); + // 15. 平电量 + BigDecimal flatEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000); + // 16.计损平电量 + BigDecimal flatLoseEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000); + additionalInfo.put("计损平电量", flatLoseEnergy); + // 17.平金额 + BigDecimal flatAmount = reduceMagnification(byteBuf.readUnsignedIntLE(), 100); + + // 18.谷单价 + BigDecimal valleyPrice = reduceMagnification(byteBuf.readUnsignedIntLE(), 1000); + additionalInfo.put("谷单价", valleyPrice); + // 19. 谷电量 + BigDecimal valleyEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000); + // 20.计损谷电量 + BigDecimal valleyLoseEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000); + additionalInfo.put("计损谷电量", valleyLoseEnergy); + // 21.谷金额 + BigDecimal valleyAmount = reduceMagnification(byteBuf.readUnsignedIntLE(), 100); + + // 22.电表总起值 + byte[] meterStartValueBytes = new byte[5]; + byteBuf.readBytes(meterStartValueBytes); + BigDecimal startMeterValue = reduceMagnification(readLongLE5Byte(meterStartValueBytes), 10000, 4); + additionalInfo.put("电表总起值", startMeterValue); + + // 23.电表总止值 + byte[] meterEndValueBytes = new byte[5]; + byteBuf.readBytes(meterEndValueBytes); + BigDecimal endMeterValue = reduceMagnification(readLongLE5Byte(meterEndValueBytes), 10000, 4); + additionalInfo.put("电表总止值", endMeterValue); + + // 24.总电量 + BigDecimal totalEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000, 4); + // 25.计损总电量 + BigDecimal totalLoseEnergy = reduceMagnification(byteBuf.readUnsignedIntLE(), 10000, 4); + additionalInfo.put("计损总电量", totalLoseEnergy); + // 26 .消费金额 + BigDecimal totalAmount = reduceMagnification(byteBuf.readUnsignedIntLE(), 100); + + // 27.电动汽车唯一标识 + byte[] carVINBytes = new byte[17]; + byteBuf.readBytes(carVINBytes); + String bmsVinCode = new String(carVINBytes, StandardCharsets.US_ASCII); + additionalInfo.put("电动汽车唯一标识", bmsVinCode); + + // 28.交易标识 0x01:app 启动0x02:卡启动 0x04:离线卡启动 0x05: vin 码启动充电 + byte tradeFlag = byteBuf.readByte(); + additionalInfo.put("交易标识", mapStartFlag(tradeFlag)); + + // 29.交易日期、时间 + byte[] tradeTimeBytes = new byte[7]; + byteBuf.readBytes(tradeTimeBytes); + Instant tradeTime = CP56Time2aUtil.decode(tradeTimeBytes); + + // 30.停止原因 + byte stopReasonByte = byteBuf.readByte(); + String stopReason = mapStopReason(stopReasonByte); + + //31 物理卡号 + byte[] cardNoBytes = new byte[8]; + byteBuf.readBytes(cardNoBytes); + String cardNo = BCDUtil.toString(cardNoBytes); + additionalInfo.put("物理卡号", cardNo); + + TransactionRecord transactionRecord = TransactionRecord.newBuilder() + .setPileCode(pileCode) + .setGunCode(gunCode) + .setTradeNo(tradeNo) + .setStartTs(startTime.toEpochMilli()) + .setEndTs(endTime.toEpochMilli()) + .setTopEnergyKWh(topEnergy.floatValue()) + .setTopAmountCent(topAmount.longValue()) + .setPeakEnergyKWh(peakEnergy.floatValue()) + .setPeakAmountCent(peakAmount.longValue()) + .setFlatEnergyKWh(flatEnergy.floatValue()) + .setFlatAmountCent(flatAmount.longValue()) + .setValleyEnergyKWh(valleyEnergy.floatValue()) + .setValleyAmountCent(valleyAmount.longValue()) + .setTotalEnergyKWh(totalEnergy.floatValue()) + .setTotalAmountCent(totalAmount.longValue()) + .setTradeTs(tradeTime.toEpochMilli()) + .setStopReason(stopReason) + .setAdditionalInfo(additionalInfo.toString()) + .build(); + + // 转发到后端 + UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(pileCode, tcpSession, yunKuaiChongUplinkMessage) + .setTransactionRecord(transactionRecord) + .build(); + + tcpSession.getForwarder().sendMessage(uplinkQueueMessage); + } + + public static long readLongLE5Byte(byte[] bytes) { + // 确保字节数组的长度至少为 5 + if (bytes.length < 5) { + throw new IllegalArgumentException("Byte array must contain at least 5 bytes."); + } + + // 使用小端字节序读取 5 字节数字 + int byte1 = bytes[0] & 0xFF; + int byte2 = bytes[1] & 0xFF; + int byte3 = bytes[2] & 0xFF; + int byte4 = bytes[3] & 0xFF; + int byte5 = bytes[4] & 0xFF; + + // 将读取的字节合并成一个 long 值 + return ((long) byte1) | + ((long) byte2 << 8) | + ((long) byte3 << 16) | + ((long) byte4 << 24) | + ((long) byte5 << 32); + } + + public static String mapStartFlag(byte startFlag) { + return switch (startFlag) { + case 0x01 -> "app 启动"; + case 0x02 -> "卡启动"; + case 0x04 -> "离线卡启动"; + case 0x05 -> "vin 码启动充电"; + default -> "未知启动方式"; + }; + } + + public static String mapStopReason(byte stopReasonCode) { + return switch (stopReasonCode) { + case (byte) 0x40 -> "结束充电,APP 远程停止"; + case (byte) 0x41 -> "结束充电,SOC 达到 100%"; + case (byte) 0x42 -> "结束充电,充电电量满足设定条件"; + case (byte) 0x43 -> "结束充电,充电金额满足设定条件"; + case (byte) 0x44 -> "结束充电,充电时间满足设定条件"; + case (byte) 0x45 -> "结束充电,手动停止充电"; + case (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49 -> "结束充电,其他方式(预留)"; + case (byte) 0x4A -> "充电启动失败,充电桩控制系统故障(需要重启或自动恢复)"; + case (byte) 0x4B -> "充电启动失败,控制导引断开"; + case (byte) 0x4C -> "充电启动失败,断路器跳位"; + case (byte) 0x4D -> "充电启动失败,电表通信中断"; + case (byte) 0x4E -> "充电启动失败,余额不足"; + case (byte) 0x4F -> "充电启动失败,充电模块故障"; + case (byte) 0x50 -> "充电启动失败,急停开入"; + case (byte) 0x51 -> "充电启动失败,防雷器异常"; + case (byte) 0x52 -> "充电启动失败,BMS 未就绪"; + case (byte) 0x53 -> "充电启动失败,温度异常"; + case (byte) 0x54 -> "充电启动失败,电池反接故障"; + case (byte) 0x55 -> "充电启动失败,电子锁异常"; + case (byte) 0x56 -> "充电启动失败,合闸失败"; + case (byte) 0x57 -> "充电启动失败,绝缘异常"; + case (byte) 0x58 -> "充电启动失败,预留"; + case (byte) 0x59 -> "充电启动失败,接收 BMS 握手报文 BHM 超时"; + case (byte) 0x5A -> "充电启动失败,接收 BMS 和车辆的辨识报文超时 BRM"; + case (byte) 0x5B -> "充电启动失败,接收电池充电参数报文超时 BCP"; + case (byte) 0x5C -> "充电启动失败,接收 BMS 完成充电准备报文超时 BRO AA"; + case (byte) 0x5D -> "充电启动失败,接收电池充电总状态报文超时 BCS"; + case (byte) 0x5E -> "充电启动失败,接收电池充电要求报文超时 BCL"; + case (byte) 0x5F -> "充电启动失败,接收电池状态信息报文超时 BSM"; + case (byte) 0x60 -> "充电启动失败,GB2015 电池在 BHM 阶段有电压不允许充电"; + case (byte) 0x61 -> "充电启动失败,GB2015 辨识阶段在 BRO_AA 时候电池实际电压与 BCP 报文电池电压差距大于 5%"; + case (byte) 0x62 -> "充电启动失败,B2015 充电机在预充电阶段从 BRO_AA 变成BRO_00 状态"; + case (byte) 0x63 -> "充电启动失败,接收主机配置报文超时"; + case (byte) 0x64 -> "充电启动失败,充电机未准备就绪,我们没有回 CRO AA,对应老国标"; + case (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69 -> "充电启动失败,其他原因(预留)"; + case (byte) 0x6A -> "充电异常中止,系统闭锁"; + case (byte) 0x6B -> "充电异常中止,导引断开"; + case (byte) 0x6C -> "充电异常中止,断路器跳位"; + case (byte) 0x6D -> "充电异常中止,电表通信中断"; + case (byte) 0x6E -> "充电异常中止,余额不足"; + case (byte) 0x6F -> "充电异常中止,交流保护动作"; + case (byte) 0x70 -> "充电异常中止,直流保护动作"; + case (byte) 0x71 -> "充电异常中止,充电模块故障"; + case (byte) 0x72 -> "充电异常中止,急停开入"; + case (byte) 0x73 -> "充电异常中止,防雷器异常"; + case (byte) 0x74 -> "充电异常中止,温度异常"; + case (byte) 0x75 -> "充电异常中止,输出异常"; + case (byte) 0x76 -> "充电异常中止,充电无流"; + case (byte) 0x77 -> "充电异常中止,电子锁异常"; + case (byte) 0x78 -> "充电异常中止,预留"; + case (byte) 0x79 -> "充电异常中止,总充电电压异常"; + case (byte) 0x7A -> "充电异常中止,总充电电流异常"; + case (byte) 0x7B -> "充电异常中止,单体充电电压异常"; + case (byte) 0x7C -> "充电异常中止,电池组过温"; + case (byte) 0x7D -> "充电异常中止,最高单体充电电压异常"; + case (byte) 0x7E -> "充电异常中止,最高电池组过温"; + case (byte) 0x7F -> "充电异常中止,BMV 单体充电电压异常"; + case (byte) 0x80 -> "充电异常中止,BMT 电池组过温"; + case (byte) 0x81 -> "充电异常中止,电池状态异常停止充电"; + case (byte) 0x82 -> "充电异常中止,车辆发报文禁止充电"; + case (byte) 0x83 -> "充电异常中止,充电桩断电"; + case (byte) 0x84 -> "充电异常中止,接收电池充电总状态报文超时"; + case (byte) 0x85 -> "充电异常中止,接收电池充电要求报文超时"; + case (byte) 0x86 -> "充电异常中止,接收电池状态信息报文超时"; + case (byte) 0x87 -> "充电异常中止,接收 BMS 中止充电报文超时"; + case (byte) 0x88 -> "充电异常中止,接收 BMS 充电统计报文超时"; + case (byte) 0x89 -> "充电异常中止,接收对侧 CCS 报文超时"; + case (byte) 0x8A, (byte) 0x8B, (byte) 0x8C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F -> + "充电异常中止,其他原因(预留)"; + case (byte) 0x90 -> "未知原因停止"; + default -> "无效的错误码"; + }; + } + +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150VerifyPricingModelAckDLCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150VerifyPricingModelAckDLCmd.java new file mode 100644 index 0000000..fbe0a34 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150VerifyPricingModelAckDLCmd.java @@ -0,0 +1,62 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.VerifyPricingResponse; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDownlinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + +import java.util.Arrays; + +import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.FAILURE_BYTE; +import static sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongDwonlinkMessage.SUCCESS_BYTE; +import static sanbing.jcpp.protocol.yunkuaichong.v150.enums.YunKuaiChongV150DownlinkCmdEnum.VERIFY_PRICING_ACK; + +/** + * 云快充1.5.0计费模型验证请求应答 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x06) +public class YunKuaiChongV150VerifyPricingModelAckDLCmd extends YunKuaiChongDownlinkCmdExe { + @Override + public void execute(TcpSession tcpSession, YunKuaiChongDwonlinkMessage yunKuaiChongDwonlinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0计费模型验证请求应答", tcpSession); + + if (!yunKuaiChongDwonlinkMessage.getMsg().hasVerifyPricingResponse()) { + return; + } + + VerifyPricingResponse verifyPricingResponse = yunKuaiChongDwonlinkMessage.getMsg().getVerifyPricingResponse(); + + YunKuaiChongUplinkMessage requestData = JacksonUtil.fromBytes(yunKuaiChongDwonlinkMessage.getMsg().getRequestData().toByteArray(), YunKuaiChongUplinkMessage.class); + + // 获取上行报文 + byte[] uplinkRawFrame = requestData.getRawFrame(); + // 从上行报文中取出桩编号字节数组 + byte[] pileCodeBytes = Arrays.copyOfRange(uplinkRawFrame, 6, 13); + + // 创建ACK消息体7字节桩编号+2字节计费模型编号+1字节验证结果 + ByteBuf verifyPricingAckMsgBody = Unpooled.buffer(10); + verifyPricingAckMsgBody.writeBytes(pileCodeBytes); + verifyPricingAckMsgBody.writeBytes(encodePricingId(verifyPricingResponse.getPricingId())); + verifyPricingAckMsgBody.writeByte(verifyPricingResponse.getSuccess() ? SUCCESS_BYTE : FAILURE_BYTE); + + encodeAndWriteFlush(VERIFY_PRICING_ACK, + requestData.getSequenceNumber(), + requestData.getEncryptionFlag(), + verifyPricingAckMsgBody, + tcpSession); + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150VerifyPricingModelULCmd.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150VerifyPricingModelULCmd.java new file mode 100644 index 0000000..24a6d31 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/cmd/YunKuaiChongV150VerifyPricingModelULCmd.java @@ -0,0 +1,59 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.cmd; + + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import sanbing.jcpp.infrastructure.util.codec.BCDUtil; +import sanbing.jcpp.infrastructure.util.jackson.JacksonUtil; +import sanbing.jcpp.proto.gen.ProtocolProto.UplinkQueueMessage; +import sanbing.jcpp.proto.gen.ProtocolProto.VerifyPricingRequest; +import sanbing.jcpp.protocol.ProtocolContext; +import sanbing.jcpp.protocol.listener.tcp.TcpSession; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkCmdExe; +import sanbing.jcpp.protocol.yunkuaichong.YunKuaiChongUplinkMessage; +import sanbing.jcpp.protocol.yunkuaichong.annotation.YunKuaiChongCmd; + + +/** + * 云快充1.5.0计费模型验证请求 + * + * @author baigod + */ +@Slf4j +@YunKuaiChongCmd(0x05) +public class YunKuaiChongV150VerifyPricingModelULCmd extends YunKuaiChongUplinkCmdExe { + + @Override + public void execute(TcpSession tcpSession, YunKuaiChongUplinkMessage yunKuaiChongUplinkMessage, ProtocolContext ctx) { + log.info("{} 云快充1.5.0计费模型验证请求", tcpSession); + ByteBuf byteBuf = Unpooled.copiedBuffer(yunKuaiChongUplinkMessage.getMsgBody()); + + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + + byte[] pileCodeBytes = new byte[7]; + byteBuf.readBytes(pileCodeBytes); + String pileCode = BCDUtil.toString(pileCodeBytes); + + byte[] pricingModelIdBytes = new byte[2]; + byteBuf.readBytes(pricingModelIdBytes); + long pricingModelId = BCDUtil.bcdBytesToLong(pricingModelIdBytes); + + // 转发到后端 + VerifyPricingRequest heartBeatRequest = VerifyPricingRequest.newBuilder() + .setPileCode(pileCode) + .setPricingId(pricingModelId) + .setAdditionalInfo(additionalInfo.toString()) + .build(); + UplinkQueueMessage uplinkQueueMessage = uplinkMessageBuilder(heartBeatRequest.getPileCode(), tcpSession, yunKuaiChongUplinkMessage) + .setVerifyPricingRequest(heartBeatRequest) + .build(); + tcpSession.getForwarder().sendMessage(uplinkQueueMessage); + + } +} \ No newline at end of file diff --git a/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/enums/YunKuaiChongV150DownlinkCmdEnum.java b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/enums/YunKuaiChongV150DownlinkCmdEnum.java new file mode 100644 index 0000000..5dab6f1 --- /dev/null +++ b/jcpp-protocol-yunkuaichong/src/main/java/sanbing/jcpp/protocol/yunkuaichong/v150/enums/YunKuaiChongV150DownlinkCmdEnum.java @@ -0,0 +1,38 @@ +/** + * 抖音关注:程序员三丙 + * 知识星球:https://t.zsxq.com/j9b21 + */ +package sanbing.jcpp.protocol.yunkuaichong.v150.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author baigod + */ +@AllArgsConstructor +@Getter +public enum YunKuaiChongV150DownlinkCmdEnum { + + LOGIN_ACK(0x02), + + SYNC_TIME(0x56), + + HEARTBEAT(0x04), + + VERIFY_PRICING_ACK(0x06), + + QUERY_PRICING_ACK(0X0A), + + SET_PRICING(0x58), + + REMOTE_START_CHARGING(0x34), + + REMOTE_STOP_CHARGING(0x36), + + TRANSACTION_RECORD(0x40) + ; + + private int cmd; + +} \ No newline at end of file diff --git a/jcpp-testing/pom.xml b/jcpp-testing/pom.xml new file mode 100644 index 0000000..9039b02 --- /dev/null +++ b/jcpp-testing/pom.xml @@ -0,0 +1,100 @@ + + + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + jcpp-testing + jar + JChargePointProtocol Testing Module + 聚合测试模块 + + + ${basedir}/.. + + + + + sanbing + jcpp-app + + + sanbing + jcpp-infrastructure-cache + + + sanbing + jcpp-infrastructure-proto + + + sanbing + jcpp-infrastructure-queue + + + sanbing + jcpp-infrastructure-stats + + + sanbing + jcpp-infrastructure-util + + + sanbing + jcpp-protocol-api + + + sanbing + jcpp-protocol-yunkuaichong + + + + + + org.jacoco + jacoco-maven-plugin + + + report-aggregate + verify + + report-aggregate + + + ${basedir}/../target + + + + merge-results + verify + + merge + + + + + ${basedir}/../ + + **/target/jacoco.exec + + + + ${basedir}/../target/aggregate.exec + + + + + + + diff --git a/license-header-template.txt b/license-header-template.txt new file mode 100644 index 0000000..2047e82 --- /dev/null +++ b/license-header-template.txt @@ -0,0 +1,2 @@ +抖音关注:${owner} +知识星球:https://t.zsxq.com/j9b21 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..bd91b22 --- /dev/null +++ b/pom.xml @@ -0,0 +1,525 @@ + + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + sanbing + jcpp-parent + 1.0.0-SNAPSHOT + pom + JChargePointProtocol + JChargePointProtocol + https://t.zsxq.com/j9b21 + + + 程序员三丙 + github + https://github.com/sanbing-java + + + + + 21 + UTF-8 + ${basedir} + 33.3.0-jre + 0.8.12 + + 1.7.0 + 3.4.4 + 3.21.9 + 4.0.2 + 1.56.1 + 0.5.1 + 3.13.0 + 5.8.32 + 3.5.7 + 5.7.0 + 6.6.2 + 3.9.2 + 3.8.16.Final + + + + + + dev + + true + + + true + true + + + + + unit-test + + false + true + + + + + integration-test + + true + false + + + + + test-all + + false + false + + + + + + jcpp-app-bootstrap + jcpp-protocol-bootstrap + jcpp-app + jcpp-infrastructure-queue + jcpp-infrastructure-cache + jcpp-infrastructure-util + jcpp-infrastructure-stats + jcpp-infrastructure-proto + jcpp-protocol-api + jcpp-testing + jcpp-protocol-yunkuaichong + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + junit + junit + test + + + + + + sanbing + jcpp-app + ${project.version} + + + sanbing + jcpp-protocol-yunkuaichong + ${project.version} + + + sanbing + jcpp-infrastructure-cache + ${project.version} + + + sanbing + jcpp-infrastructure-proto + ${project.version} + + + sanbing + jcpp-infrastructure-queue + ${project.version} + + + sanbing + jcpp-infrastructure-stats + ${project.version} + + + sanbing + jcpp-infrastructure-util + ${project.version} + + + sanbing + jcpp-protocol-api + ${project.version} + + + org.jboss.xnio + xnio-api + ${xnio-api.version} + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + com.google.protobuf + protobuf-java-util + ${protobuf.version} + + + org.glassfish + jakarta.el + ${jakarta.el.version} + + + com.lmax + disruptor + ${disruptor.version} + + + cn.hutool + hutool-core + ${hutool-all.version} + + + com.github.oshi + oshi-core + ${oshi-core.version} + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus-boot-starter.version} + + + org.apache.curator + curator-recipes + ${curator-recipes.version} + + + org.apache.zookeeper + zookeeper + ${zookeeper.version} + + + + + + + + kr.motd.maven + os-maven-plugin + ${os-maven-plugin.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 21 + + -Xlint:deprecation + -Xlint:removal + -Xlint:unchecked + -parameters + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.2.0 + + + copy-conf + process-resources + + copy-resources + + + ${project.build.directory}/conf + + + src/main/resources + + * + + false + + + src/main/resources + + log4j2.xml + application.yml + + true + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/log4j2.xml + + + + ${project.name} + ${project.version} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-protoc + generate-sources + + copy + + + + + com.google.protobuf + protoc + ${protobuf.version} + + ${os.detected.classifier} + exe + true + ${project.build.directory} + + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf-maven-plugin.version} + + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + + grpc-java + + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + + compile + compile-custom + test-compile + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + @{argLine} -Xms256m -Xmx1024m -Dfile.encoding=UTF-8 + -XX:+EnableDynamicAgentLoading + -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=20 + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + + ${skip.unit.test} + 1 + filesystem + + **/*Tests.java + **/*Test.java + + + **/Abstract*.java + **/*IntegrationTest.java + **/IT*.java + **/*IT.java + **/*ITCase.java + examples/codequality/tests/**/*.java + + true + 1 + + + + org.apache.maven.surefire + surefire-junit47 + ${maven-surefire-plugin.version} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + integration-tests + + integration-test + verify + + + ${skip.integration.test} + + **/*IntegrationTest.java + **/IT*.java + **/*IT.java + **/*ITCase.java + + true + ${itCoverageAgent} + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + **/*DO.class + **/*DTO.class + **/*VO.class + **/*BO.class + **/*Model.class + + + + + prepare-unit-tests + + prepare-agent + + + + prepare-agent + + prepare-agent + + pre-integration-test + + itCoverageAgent + + + + + + org.apache.maven.plugins + maven-clean-plugin + + + custom-clean + clean + + clean + + + + + ${project.build.directory} + + **/* + + + + + + + + + com.mycila + license-maven-plugin + 3.0 + +
${main.dir}/license-header-template.txt
+ + 程序员三丙 + + + **/.env + **/*.env + **/.gradle/** + **/*.md + **/LICENSE + **/banner.txt + **/*.properties + src/test/resources/** + src/sh/** + **/*.log + src/main/resources/** + **/*.raw + **/*.patch + .run/** + + + JAVADOC_STYLE + DOUBLEDASHES_STYLE + JAVADOC_STYLE + SLASHSTAR_STYLE + SLASHSTAR_STYLE + SCRIPT_STYLE + JAVADOC_STYLE + SCRIPT_STYLE + SCRIPT_STYLE + SCRIPT_STYLE + +
+ + + + format + check + + + +
+
+
+