From 167b0e02cf8936ea6c45405e04d289b70fce26b5 Mon Sep 17 00:00:00 2001 From: Eric <01714308@yto.net.cn> Date: Mon, 9 Feb 2026 13:29:54 +0800 Subject: [PATCH] jdk17 commit --- .gitignore | 45 ++ .idea/checkstyle-idea.xml | 15 + .idea/workspace.xml | 299 ++++++++ .woodpecker.yml | 135 ++++ README.md | 0 demo/lingniu-framework-demo/pom.xml | 112 +++ .../java/cn/lingniu/demo/DemoApplication.java | 18 + .../ApolloConfigChangeDemoListener.java | 31 + .../lingniu/demo/apollo/ApolloController.java | 26 + .../demo/apollo/ObjcetDemoConfiguration.java | 15 + .../lingniu/demo/apollo/ObjectDemoConfig.java | 39 + .../demo/db/JobZookeeperController.java | 49 ++ .../lingniu/demo/db/JobZookeeperServer.java | 60 ++ .../demo/db/JobZookeeperServerMapper.java | 8 + .../java/cn/lingniu/demo/file/DemoData.java | 26 + .../cn/lingniu/demo/file/FileController.java | 79 +++ .../cn/lingniu/demo/file/FileTestUtil.java | 76 ++ .../demo/jetcache/annotationDemo/User.java | 24 + .../demo/jetcache/annotationDemo/UserDao.java | 53 ++ .../jetcache/annotationDemo/UserService.java | 59 ++ .../controller/JetCacheController.java | 70 ++ .../jetcache/controller/KeyValueEntity.java | 18 + .../cn/lingniu/demo/job/SampleXxlJob.java | 63 ++ .../demo/microservice/CloudController.java | 30 + .../demo/microservice/Contributor.java | 9 + .../demo/microservice/DemoFeignClient.java | 25 + .../microservice/GithubApiClientFallBack.java | 24 + .../GithubApiClientFallbackFactory.java | 21 + .../demo/monitor/PrometheusController.java | 82 +++ .../lingniu/demo/redisson/LockActionDemo.java | 88 +++ .../redisson/RedissonLockControllerDemo.java | 99 +++ .../demo/rocketmq/RocketMqController.java | 58 ++ .../lingniu/demo/rocketmq/TestC1Consumer.java | 21 + .../lingniu/demo/rocketmq/TestMqProducer.java | 12 + .../demo/rocketmq/TestTransMqProducer.java | 49 ++ .../cn/lingniu/demo/web/WebController.java | 54 ++ .../java/cn/lingniu/demo/web/WebEntity.java | 10 + .../src/main/resources/application-db.yml | 12 + .../main/resources/application-jetcache.yml | 58 ++ .../main/resources/application-prometheus.yml | 6 + .../main/resources/application-redisson.yml | 44 ++ .../main/resources/application-rocketmq.yml | 65 ++ .../src/main/resources/application-web.yml | 12 + .../src/main/resources/application-xxljob.yml | 21 + .../src/main/resources/application.yml | 48 ++ .../src/main/resources/applicationTest-a.yml | 22 + .../src/main/resources/logback-spring.xml | 66 ++ demo/lingniu-framework-provider-demo/pom.xml | 60 ++ .../lingniu/demo/ProviderDemoApplication.java | 13 + .../java/cn/lingniu/demo/web/Contributor.java | 11 + .../lingniu/demo/web/GithubApiController.java | 23 + .../src/main/resources/application-web.yml | 9 + .../src/main/resources/application.yml | 15 + .../src/main/resources/logback-spring.xml | 65 ++ doc/apollo安装文档.docx | Bin 0 -> 26773 bytes doc/nacos安装文档.docx | Bin 0 -> 31819 bytes framework.md | 14 + img_1.png | Bin 0 -> 173853 bytes lingniu-framework-dependencies/pom.xml | 664 ++++++++++++++++++ .../lingniu-framework-plugin-jetcache/pom.xml | 52 ++ .../autoconfigure/AbstractCacheAutoInit.java | 95 +++ .../autoconfigure/AutoConfigureBeans.java | 45 ++ .../autoconfigure/BeanDependencyManager.java | 33 + .../CaffeineAutoConfiguration.java | 32 + .../jetcache/autoconfigure/ConfigTree.java | 106 +++ .../autoconfigure/EmbeddedCacheAutoInit.java | 25 + .../autoconfigure/ExternalCacheAutoInit.java | 36 + .../JetCacheAutoConfiguration.java | 83 +++ .../autoconfigure/JetCacheCondition.java | 48 ++ .../autoconfigure/JetCacheProperties.java | 68 ++ .../LinkedHashMapAutoConfiguration.java | 32 + .../MockRemoteCacheAutoConfiguration.java | 40 ++ .../RedissonAutoConfiguration.java | 73 ++ .../redisson/RedissonBroadcastManager.java | 70 ++ .../jetcache/redisson/RedissonCache.java | 180 +++++ .../redisson/RedissonCacheBuilder.java | 52 ++ .../redisson/RedissonCacheConfig.java | 21 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../src/main/resources/jetcache-config.md | 122 ++++ .../lingniu-framework-plugin-redisson/pom.xml | 34 + .../redisson/RedissonClientFactory.java | 28 + .../RedissonClusterLockerService.java | 30 + .../plugin/redisson/RedissonLockAction.java | 51 ++ .../plugin/redisson/RedissonLockType.java | 23 + .../RedissonAbstractClientBuilder.java | 58 ++ .../builder/RedissonClusterClientBuilder.java | 47 ++ .../RedissonStandaloneClientBuilder.java | 41 ++ .../redisson/config/RedisHostAndPort.java | 13 + .../plugin/redisson/config/RedisType.java | 23 + .../redisson/config/RedissonConfig.java | 21 + .../redisson/config/RedissonProperties.java | 42 ++ ...lVariableTableParameterNameDiscoverer.java | 240 +++++++ .../init/RedissonAutoConfiguration.java | 53 ++ .../RedissonBeanRegisterPostProcessor.java | 75 ++ ...sonDistributedLockAspectConfiguration.java | 89 +++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../src/main/resources/redisson-config.md | 121 ++++ .../lingniu-framework-plugin-apollo/pom.xml | 56 ++ .../plugin/apollo/config/ApolloConfig.java | 29 + .../extend/ApolloAnnotationProcessor.java | 79 +++ .../extend/ApolloConfigChangeLogListener.java | 58 ++ .../plugin/apollo/extend/ClassPoolUtils.java | 26 + ...kConfigPropertySourcesProcessorHelper.java | 47 ++ .../FrameworkApolloAnnotationProcessor.java | 265 +++++++ .../FrameworkApolloConfigRegistrarHelper.java | 118 ++++ .../apollo/init/ApolloAutoConfiguration.java | 26 + .../plugin/apollo/init/ApolloInit.java | 82 +++ ...llo.spring.spi.ApolloConfigRegistrarHelper | 1 + ...g.spi.ConfigPropertySourcesProcessorHelper | 1 + .../main/resources/META-INF/spring.factories | 2 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../src/main/resources/apollo-config.md | 37 + .../lingniu-framework-plugin-mybatis/pom.xml | 100 +++ .../mybatis/base/DBSqlLogInterceptor.java | 183 +++++ .../plugin/mybatis/base/DataSourceEnum.java | 22 + .../mybatis/base/DruidAdRemoveFilter.java | 36 + .../mybatis/config/DataSourceConfig.java | 34 + .../init/DataSourceAutoConfiguration.java | 68 ++ .../plugin/mybatis/init/DataSourceInit.java | 19 + .../main/resources/META-INF/spring.factories | 2 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../src/main/resources/mybatis-config.md | 65 ++ .../pom.xml | 22 + .../src/main/resources/file.md | 3 + .../lingniu-framework-plugin-xxljob/pom.xml | 35 + .../plugin/xxljob/config/XxlJobConfig.java | 64 ++ .../xxljob/init/XxlJobAutoConfiguration.java | 41 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../src/main/resources/xxl-job.md | 43 ++ .../lingniu-framework-plugin-core/pom.xml | 76 ++ .../plugin/core/base/CommonResult.java | 121 ++++ .../plugin/core/base/TerminalEnum.java | 38 + .../plugin/core/base/WebFilterOrderEnum.java | 35 + .../plugin/core/config/CommonConstant.java | 16 + .../core/context/ApplicationNameContext.java | 15 + .../context/SpringBeanApplicationContext.java | 60 ++ .../plugin/core/exception/ErrorCode.java | 30 + .../core/exception/ServerException.java | 60 ++ .../core/exception/ServiceException.java | 60 ++ .../enums/GlobalErrorCodeConstants.java | 39 + .../enums/ServiceErrorCodeRange.java | 11 + .../exception/util/ServiceExceptionUtil.java | 76 ++ ...FrameworkSystemInitContextInitializer.java | 43 ++ .../SpringApplicationContextCondition.java | 16 + ...SpringApplicationContextConfiguration.java | 25 + .../main/resources/META-INF/spring.factories | 6 + .../src/main/resources/core.md | 7 + .../lingniu-framework-plugin-util/pom.xml | 109 +++ .../plugin/util/cache/CacheUtils.java | 59 ++ .../plugin/util/collection/ArrayUtils.java | 54 ++ .../util/collection/CollectionUtils.java | 358 ++++++++++ .../plugin/util/collection/MapUtils.java | 58 ++ .../plugin/util/collection/SetUtils.java | 17 + .../plugin/util/config/ContextUtils.java | 63 ++ .../plugin/util/config/PropertyUtils.java | 43 ++ .../plugin/util/core/ArrayValuable.java | 13 + .../framework/plugin/util/core/KeyValue.java | 20 + .../plugin/util/date/DateIntervalEnum.java | 47 ++ .../framework/plugin/util/date/DateUtils.java | 151 ++++ .../plugin/util/date/LocalDateTimeUtils.java | 352 ++++++++++ .../framework/plugin/util/io/FileUtils.java | 59 ++ .../framework/plugin/util/io/IoUtils.java | 26 + .../framework/plugin/util/ip/IpUtil.java | 59 ++ .../framework/plugin/util/json/JsonUtil.java | 156 ++++ .../framework/plugin/util/json/JsonUtils.java | 231 ++++++ .../util/json/databind/NumberSerializer.java | 35 + .../TimestampLocalDateTimeDeserializer.java | 25 + .../TimestampLocalDateTimeSerializer.java | 82 +++ .../plugin/util/number/MoneyUtils.java | 129 ++++ .../plugin/util/number/NumberUtils.java | 76 ++ .../plugin/util/object/BeanUtils.java | 53 ++ .../plugin/util/object/ObjectUtils.java | 65 ++ .../plugin/util/servlet/ServletUtils.java | 121 ++++ .../util/spring/SpringExpressionUtils.java | 123 ++++ .../plugin/util/spring/SpringUtils.java | 22 + .../plugin/util/string/StrUtils.java | 78 ++ .../plugin/util/string/StringUtil.java | 72 ++ .../util/validation/ObjectEmptyUtils.java | 79 +++ .../src/main/resources/util.md | 1 + .../log/logback-spring.xml | 122 ++++ .../pom.xml | 78 ++ .../common/config/DefaultHttpProperties.java | 22 + .../common/config/FeignProperties.java | 18 + .../common/config/MircroServiceConfig.java | 47 ++ .../common/config/OkHttpProperties.java | 30 + .../common/core/ErrorCommonResultHandler.java | 13 + .../core/StringHttpMessageConverter.java | 122 ++++ .../core/decoder/LingniuErrorDecoder.java | 75 ++ .../core/decoder/LingniuFeignDecoder.java | 85 +++ .../CharlesRequestInterceptor.java | 83 +++ .../interceptor/FeignLoggerInterceptor.java | 19 + ...DefaultFeignLoadBalancedConfiguration.java | 80 +++ ...MessageFastJsonConverterConfiguration.java | 96 +++ .../init/LingniuCloudAutoConfiguration.java | 171 +++++ .../common/init/LingniuCloudInit.java | 31 + .../OkHttpFeignLoadBalancedConfiguration.java | 90 +++ .../main/resources/META-INF/spring.factories | 2 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../pom.xml | 55 ++ .../src/main/resources/nacos.md | 72 ++ .../pom.xml | 71 ++ .../plugin/prometheus/PrometheusCounter.java | 34 + .../plugin/prometheus/PrometheusMetrics.java | 19 + .../plugin/prometheus/PrometheusSummary.java | 34 + .../aspect/BasePrometheusAspect.java | 28 + ...utionSummaryMeterTagAnnotationHandler.java | 52 ++ .../aspect/PrometheusCounterAspect.java | 55 ++ .../aspect/PrometheusMetricsAspect.java | 65 ++ .../aspect/PrometheusSummaryAspect.java | 58 ++ .../prometheus/config/PrometheusConfig.java | 32 + .../init/PrometheusConfiguration.java | 118 ++++ .../prometheus/init/PrometheusInit.java | 116 +++ .../main/resources/META-INF/spring.factories | 2 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../src/main/resources/prometheus.md | 34 + .../pom.xml | 33 + .../src/main/resources/skywalking.md | 4 + .../lingniu-framework-plugin-rocketmq/pom.xml | 85 +++ .../plugin/rocketmq/RocketMqConsumer.java | 22 + .../plugin/rocketmq/RocketMqProducer.java | 13 + .../plugin/rocketmq/config/ConsumeMode.java | 15 + .../rocketmq/config/ConsumerProperties.java | 109 +++ .../rocketmq/config/ProducerProperties.java | 79 +++ .../rocketmq/config/RocketMqConfig.java | 27 + .../plugin/rocketmq/config/SelectorType.java | 19 + .../core/RocketMqBodyJacksonSerializer.java | 63 ++ .../rocketmq/core/RocketMqBodySerializer.java | 13 + .../DefaultRocketMqListenerContainer.java | 329 +++++++++ .../consumer/RocketMqConsumerMessage.java | 22 + .../consumer/RocketMqListenerContainer.java | 10 + .../consumer/listener/GeneralMsgListener.java | 55 ++ .../consumer/listener/MRocketMqListener.java | 13 + .../consumer/listener/RocketMqListener.java | 18 + .../GeneralRocketMqProducerInner.java | 164 +++++ .../core/producer/RocketMqProducerInner.java | 36 + .../core/producer/RocketMqSendMsgBody.java | 27 + .../core/producer/RocketMqTemplate.java | 326 +++++++++ .../core/producer/call/SendMessageOnFail.java | 8 + .../producer/call/SendMessageOnSuccess.java | 9 + .../call/TransactionListenerImpl.java | 48 ++ .../plugin/rocketmq/init/RocketMqInit.java | 33 + .../init/RocketMqStartAutoConfiguration.java | 210 ++++++ .../main/resources/META-INF/spring.factories | 3 + .../src/main/resources/mq-config.md | 118 ++++ .../web/lingniu-framework-plugin-web/pom.xml | 99 +++ .../web/ApplicationStartEventListener.java | 45 ++ .../plugin/web/apilog/ApiAccessLog.java | 49 ++ .../web/apilog/enums/OperateTypeEnum.java | 51 ++ .../web/apilog/filter/ApiAccessLogFilter.java | 241 +++++++ .../interceptor/ApiAccessLogInterceptor.java | 102 +++ .../apilog/logger/ApiAccessLogApiImpl.java | 26 + .../logger/ApiAccessLogServiceImpl.java | 40 ++ .../web/apilog/logger/ApiErrorLogApiImpl.java | 23 + .../apilog/logger/ApiErrorLogServiceImpl.java | 32 + .../logger/api/ApiAccessLogCommonApi.java | 29 + .../logger/api/ApiAccessLogService.java | 18 + .../logger/api/ApiErrorLogCommonApi.java | 27 + .../apilog/logger/api/ApiErrorLogService.java | 18 + .../model/ApiAccessLogCreateReqDTO.java | 79 +++ .../apilog/logger/model/ApiAccessLogDO.java | 99 +++ .../logger/model/ApiErrorLogCreateReqDTO.java | 85 +++ .../apilog/logger/model/ApiErrorLogDO.java | 118 ++++ .../web/bae/filter/ApiRequestFilter.java | 38 + .../bae/filter/CacheRequestBodyFilter.java | 41 ++ .../bae/filter/CacheRequestBodyWrapper.java | 76 ++ .../bae/handler/GlobalExceptionHandler.java | 383 ++++++++++ .../handler/GlobalResponseBodyHandler.java | 42 ++ .../interceptor/ResponseCheckInterceptor.java | 38 + .../web/bae/util/WebFrameworkUtils.java | 123 ++++ .../web/config/ApiEncryptProperties.java | 59 ++ .../plugin/web/config/ApiLogProperties.java | 26 + .../plugin/web/config/FrameworkWebConfig.java | 30 + .../web/config/StaticResourceProperties.java | 20 + .../plugin/web/encrypt/ApiEncrypt.java | 27 + .../filter/ApiDecryptRequestWrapper.java | 84 +++ .../web/encrypt/filter/ApiEncryptFilter.java | 155 ++++ .../filter/ApiEncryptResponseWrapper.java | 108 +++ .../web/init/ApiEncryptAutoConfiguration.java | 34 + .../web/init/ApiLogAutoConfiguration.java | 44 ++ .../web/init/EnableGlobalResponseBody.java | 20 + .../web/init/JacksonAutoConfiguration.java | 77 ++ .../init/ResponseDetectAutoconfiguration.java | 52 ++ .../ResponseInterceptorAutoconfiguration.java | 19 + .../plugin/web/init/WebAutoConfiguration.java | 71 ++ .../favicon/FaviconAutoConfiguration.java | 41 ++ .../web/init/favicon/FaviconFilter.java | 22 + ...ot.autoconfigure.AutoConfiguration.imports | 7 + .../src/main/resources/web.md | 57 ++ .../lingniu-framework-starters-nacos/pom.xml | 32 + .../lingniu-framework-starters-web/pom.xml | 62 ++ lingniu-framework-starters/pom.xml | 20 + pom.xml | 83 +++ 292 files changed, 17485 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/checkstyle-idea.xml create mode 100644 .idea/workspace.xml create mode 100644 .woodpecker.yml create mode 100644 README.md create mode 100644 demo/lingniu-framework-demo/pom.xml create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/DemoApplication.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloConfigChangeDemoListener.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloController.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjcetDemoConfiguration.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjectDemoConfig.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperController.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServer.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServerMapper.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/DemoData.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileController.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileTestUtil.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/User.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserDao.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserService.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/JetCacheController.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/KeyValueEntity.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/job/SampleXxlJob.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/CloudController.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/Contributor.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/DemoFeignClient.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallBack.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallbackFactory.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/monitor/PrometheusController.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/LockActionDemo.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/RedissonLockControllerDemo.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/RocketMqController.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestC1Consumer.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestMqProducer.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestTransMqProducer.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebController.java create mode 100644 demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebEntity.java create mode 100644 demo/lingniu-framework-demo/src/main/resources/application-db.yml create mode 100644 demo/lingniu-framework-demo/src/main/resources/application-jetcache.yml create mode 100644 demo/lingniu-framework-demo/src/main/resources/application-prometheus.yml create mode 100644 demo/lingniu-framework-demo/src/main/resources/application-redisson.yml create mode 100644 demo/lingniu-framework-demo/src/main/resources/application-rocketmq.yml create mode 100644 demo/lingniu-framework-demo/src/main/resources/application-web.yml create mode 100644 demo/lingniu-framework-demo/src/main/resources/application-xxljob.yml create mode 100644 demo/lingniu-framework-demo/src/main/resources/application.yml create mode 100644 demo/lingniu-framework-demo/src/main/resources/applicationTest-a.yml create mode 100644 demo/lingniu-framework-demo/src/main/resources/logback-spring.xml create mode 100644 demo/lingniu-framework-provider-demo/pom.xml create mode 100644 demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/ProviderDemoApplication.java create mode 100644 demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/Contributor.java create mode 100644 demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/GithubApiController.java create mode 100644 demo/lingniu-framework-provider-demo/src/main/resources/application-web.yml create mode 100644 demo/lingniu-framework-provider-demo/src/main/resources/application.yml create mode 100644 demo/lingniu-framework-provider-demo/src/main/resources/logback-spring.xml create mode 100644 doc/apollo安装文档.docx create mode 100644 doc/nacos安装文档.docx create mode 100644 framework.md create mode 100644 img_1.png create mode 100644 lingniu-framework-dependencies/pom.xml create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/pom.xml create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AbstractCacheAutoInit.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AutoConfigureBeans.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/BeanDependencyManager.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/CaffeineAutoConfiguration.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ConfigTree.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/EmbeddedCacheAutoInit.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ExternalCacheAutoInit.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheAutoConfiguration.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheCondition.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheProperties.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/LinkedHashMapAutoConfiguration.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/MockRemoteCacheAutoConfiguration.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/RedissonAutoConfiguration.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonBroadcastManager.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCache.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheBuilder.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheConfig.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/jetcache-config.md create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/pom.xml create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClientFactory.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClusterLockerService.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockAction.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockType.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonAbstractClientBuilder.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonClusterClientBuilder.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonStandaloneClientBuilder.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisHostAndPort.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisType.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonConfig.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonProperties.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/LocalVariableTableParameterNameDiscoverer.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonAutoConfiguration.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonBeanRegisterPostProcessor.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonDistributedLockAspectConfiguration.java create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/redisson-config.md create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/pom.xml create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/config/ApolloConfig.java create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloAnnotationProcessor.java create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloConfigChangeLogListener.java create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ClassPoolUtils.java create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FramewokConfigPropertySourcesProcessorHelper.java create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloAnnotationProcessor.java create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloConfigRegistrarHelper.java create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloAutoConfiguration.java create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloInit.java create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ConfigPropertySourcesProcessorHelper create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring.factories create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/apollo-config.md create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/pom.xml create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DBSqlLogInterceptor.java create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DataSourceEnum.java create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DruidAdRemoveFilter.java create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/config/DataSourceConfig.java create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceAutoConfiguration.java create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceInit.java create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring.factories create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/mybatis-config.md create mode 100644 lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/pom.xml create mode 100644 lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/src/main/resources/file.md create mode 100644 lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/pom.xml create mode 100644 lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/config/XxlJobConfig.java create mode 100644 lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/init/XxlJobAutoConfiguration.java create mode 100644 lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/xxl-job.md create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/pom.xml create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/CommonResult.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/TerminalEnum.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/WebFilterOrderEnum.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/config/CommonConstant.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/ApplicationNameContext.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/SpringBeanApplicationContext.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ErrorCode.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServerException.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServiceException.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/GlobalErrorCodeConstants.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/ServiceErrorCodeRange.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/util/ServiceExceptionUtil.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/FrameworkSystemInitContextInitializer.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextCondition.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextConfiguration.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/META-INF/spring.factories create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/core.md create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/pom.xml create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/cache/CacheUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/ArrayUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/CollectionUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/MapUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/SetUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/ContextUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/PropertyUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/ArrayValuable.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/KeyValue.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateIntervalEnum.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/LocalDateTimeUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/FileUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/IoUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/ip/IpUtil.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtil.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/NumberSerializer.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeDeserializer.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeSerializer.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/MoneyUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/NumberUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/BeanUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/ObjectUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/servlet/ServletUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringExpressionUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StrUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StringUtil.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/validation/ObjectEmptyUtils.java create mode 100644 lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/resources/util.md create mode 100644 lingniu-framework-plugin/log/logback-spring.xml create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/pom.xml create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/DefaultHttpProperties.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/FeignProperties.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/MircroServiceConfig.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/OkHttpProperties.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/ErrorCommonResultHandler.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/StringHttpMessageConverter.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuErrorDecoder.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuFeignDecoder.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/CharlesRequestInterceptor.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/FeignLoggerInterceptor.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/DefaultFeignLoadBalancedConfiguration.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/HttpMessageFastJsonConverterConfiguration.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudAutoConfiguration.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudInit.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/OkHttpFeignLoadBalancedConfiguration.java create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring.factories create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/pom.xml create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/nacos.md create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/pom.xml create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusCounter.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusMetrics.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusSummary.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/BasePrometheusAspect.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/DistributionSummaryMeterTagAnnotationHandler.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusCounterAspect.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusMetricsAspect.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusSummaryAspect.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/config/PrometheusConfig.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusConfiguration.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusInit.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring.factories create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/prometheus.md create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/pom.xml create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/src/main/resources/skywalking.md create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/pom.xml create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqConsumer.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqProducer.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumeMode.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumerProperties.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ProducerProperties.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/RocketMqConfig.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/SelectorType.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodyJacksonSerializer.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodySerializer.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/DefaultRocketMqListenerContainer.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqConsumerMessage.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqListenerContainer.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/GeneralMsgListener.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/MRocketMqListener.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/RocketMqListener.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/GeneralRocketMqProducerInner.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqProducerInner.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqSendMsgBody.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqTemplate.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnFail.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnSuccess.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/TransactionListenerImpl.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqInit.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqStartAutoConfiguration.java create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/META-INF/spring.factories create mode 100644 lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/mq-config.md create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/pom.xml create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/ApplicationStartEventListener.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/ApiAccessLog.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/enums/OperateTypeEnum.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/filter/ApiAccessLogFilter.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/interceptor/ApiAccessLogInterceptor.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogApiImpl.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogServiceImpl.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogApiImpl.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogServiceImpl.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogCommonApi.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogService.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogCommonApi.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogService.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogCreateReqDTO.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogDO.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogCreateReqDTO.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogDO.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/ApiRequestFilter.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyFilter.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyWrapper.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalExceptionHandler.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalResponseBodyHandler.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/interceptor/ResponseCheckInterceptor.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/util/WebFrameworkUtils.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiEncryptProperties.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiLogProperties.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/FrameworkWebConfig.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/StaticResourceProperties.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/ApiEncrypt.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiDecryptRequestWrapper.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptFilter.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptResponseWrapper.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiEncryptAutoConfiguration.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiLogAutoConfiguration.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/EnableGlobalResponseBody.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/JacksonAutoConfiguration.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseDetectAutoconfiguration.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseInterceptorAutoconfiguration.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/WebAutoConfiguration.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/favicon/FaviconAutoConfiguration.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/favicon/FaviconFilter.java create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/web.md create mode 100644 lingniu-framework-starters/lingniu-framework-starters-nacos/pom.xml create mode 100644 lingniu-framework-starters/lingniu-framework-starters-web/pom.xml create mode 100644 lingniu-framework-starters/pom.xml create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82490ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/vcs.xml +.idea/misc.xml +.idea/encodings.xml +.idea/CoolRequestCommonStatePersistent.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +### cache file ### +/apolloConfig/ \ No newline at end of file diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..1f822d8 --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,15 @@ + + + + 10.26.1 + JavaOnly + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..28086df --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "associatedIndex": 8 +} + + + + + { + "keyToString": { + "Application.DemoApplication.executor": "Debug", + "Application.ProviderDemoApplication.executor": "Debug", + "Maven.Unknown [package].executor": "Run", + "Maven.lingniu-framework-dependencies [install...].executor": "Run", + "Maven.lingniu-framework-dependencies [package].executor": "Run", + "Maven.lingniu-framework-parent [install...].executor": "Run", + "Maven.lingniu-framework-parent [package].executor": "Run", + "Maven.lingniu-framework-plugin-apollo [install...].executor": "Run", + "Maven.lingniu-framework-plugin-core [install...].executor": "Run", + "Maven.lingniu-framework-plugin-jetcache [install...].executor": "Run", + "Maven.lingniu-framework-plugin-mybatis [install...].executor": "Run", + "Maven.lingniu-framework-plugin-prometheus [install...].executor": "Run", + "Maven.lingniu-framework-plugin-redisson [install...].executor": "Run", + "Maven.lingniu-framework-plugin-rocketmq [install...].executor": "Run", + "Maven.lingniu-framework-plugin-web [install...].executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.git.unshallow": "true", + "git-widget-placeholder": "master", + "kotlin-language-version-configured": "true", + "last_opened_file_path": "/Users/likobe/lingniu/lingniu-framework-parent/lingniu-framework-plugin/microservice/dubbo", + "project.structure.last.edited": "Modules", + "project.structure.proportion": "0.15", + "project.structure.side.proportion": "0.31264368", + "settings.editor.selected.configurable": "通义灵码" + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1770123780486 + + + + + + + + + + + + file://$PROJECT_DIR$/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FramewokConfigPropertySourcesProcessorHelper.java + 21 + + + file://$PROJECT_DIR$/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperController.java + 46 + + + file://$PROJECT_DIR$/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/favicon/FaviconFilter.java + 15 + + + file://$PROJECT_DIR$/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqTemplate.java + 281 + + + file://$PROJECT_DIR$/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalExceptionHandler.java + 230 + + + + + + + cn.lingniu.demo.* + + + \ No newline at end of file diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..85b9512 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,135 @@ +clone: + git: + image: woodpeckerci/plugin-git:next + settings: + depth: false + +steps: + # lingniu-framework CI/CD 流程 - 发布到 Nexus + - name: maven-deploy-to-nexus + image: maven:3.6.3-jdk-8 + when: + event: + - push + - pull_request + - manual # 允许手动触发 + branch: + - master + - develop + environment: + # Nexus 认证信息(通过 Woodpecker secret 注入) + NEXUS_USERNAME: + from_secret: nexus_username + NEXUS_PASSWORD: + from_secret: nexus_password + commands: | + echo "========================================" + echo "Building lingniu-framework-parent" + echo "========================================" + + # 进入工作目录 + cd $CI_WORKSPACE + + # 创建 Maven settings.xml 配置文件 + mkdir -p ~/.m2 + cat > ~/.m2/settings.xml <<'EOF' + + + + + + lnh2e-releases + ${NEXUS_USERNAME} + ${NEXUS_PASSWORD} + + + lnh2e-snapshots + ${NEXUS_USERNAME} + ${NEXUS_PASSWORD} + + + + + + lnh2e-central + central + 羚牛私服中央仓库 + https://nexus.lnh2e.com/repository/maven-public/ + + + + + + lnh2e + + + lnh2e-releases + 羚牛私服发布仓库 + https://nexus.lnh2e.com/repository/maven-releases/ + + true + + + false + + + + lnh2e-snapshots + 羚牛私服快照仓库 + https://nexus.lnh2e.com/repository/maven-snapshots/ + + false + + + true + + + + + + lnh2e-plugins + 羚牛私服插件仓库 + https://nexus.lnh2e.com/repository/maven-public/ + + true + + + true + + + + + + + + lnh2e + + + + EOF + + echo "Maven settings.xml created" + + # 获取分支名 + BRANCH_NAME=$(echo $CI_COMMIT_BRANCH | tr / -) + echo "Branch name: $BRANCH_NAME" + + # 获取项目版本号 + PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "Project version: $PROJECT_VERSION" + + # 打印构建信息 + echo "========================================" + echo "Branch: $BRANCH_NAME" + echo "Version: $PROJECT_VERSION" + echo "========================================" + + # 构建并部署到 Nexus(跳过测试) + echo "Building and deploying to Nexus..." + mvn -B -e -U clean deploy -Dmaven.test.skip=true + + echo "========================================" + echo "Deploy completed successfully!" + echo "========================================" diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/demo/lingniu-framework-demo/pom.xml b/demo/lingniu-framework-demo/pom.xml new file mode 100644 index 0000000..4b58357 --- /dev/null +++ b/demo/lingniu-framework-demo/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + + + lingniu-framework-demo + jar + + + + cn.lingniu.framework + lingniu-framework-plugin-apollo + + + cn.lingniu.framework + lingniu-framework-plugin-redisson + + + cn.lingniu.framework + lingniu-framework-plugin-jetcache + + + + cn.lingniu.framework + lingniu-framework-plugin-xxljob + + + + cn.lingniu.framework + lingniu-framework-plugin-mybatis + + + + com.h2database + h2 + 2.1.214 + + + + cn.lingniu.framework + lingniu-framework-plugin-rocketmq + + + org.apache.tomcat + annotations-api + + + + + + + cn.lingniu.framework + lingniu-framework-plugin-easyexcel + + + + + cn.lingniu.framework + lingniu-framework-plugin-prometheus + + + + + cn.lingniu.framework + lingniu-framework-plugin-web + + + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-nacos + + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + com.alibaba + fastjson + + + ch.qos.logback + logback-classic + + + + + + + diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/DemoApplication.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/DemoApplication.java new file mode 100644 index 0000000..61b1d94 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/DemoApplication.java @@ -0,0 +1,18 @@ +package cn.lingniu.demo; + +import com.alicp.jetcache.anno.config.EnableMethodCache; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan(basePackages = {"cn.lingniu.*"}) +//jetcache 注解 +@EnableMethodCache(basePackages = "cn.lingniu") +@SpringBootApplication() +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloConfigChangeDemoListener.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloConfigChangeDemoListener.java new file mode 100644 index 0000000..d57e4a6 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloConfigChangeDemoListener.java @@ -0,0 +1,31 @@ +package cn.lingniu.demo.apollo; + +import com.ctrip.framework.apollo.model.ConfigChange; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +@Slf4j +public class ApolloConfigChangeDemoListener { + + + @ApolloConfigChangeListener() + public void onChange(ConfigChangeEvent changeEvent) { + for (String changedKey : changeEvent.changedKeys()) { + ConfigChange changeInfo = changeEvent.getChange(changedKey); + if(StringUtils.isEmpty(changeInfo)){continue;} + log.info("apollo 配置变更 \r\n\t命名空间:{} \r\n\t属性:{} \r\n\t原值:{}\r\n\t新值:{}\r\n\t变更类型:{}" + ,changeInfo.getNamespace() + ,changeInfo.getPropertyName() + ,changeInfo.getOldValue() + ,changeInfo.getNewValue() + ,changeInfo.getChangeType() + ); + } + } + + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloController.java new file mode 100644 index 0000000..a62a99b --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloController.java @@ -0,0 +1,26 @@ +package cn.lingniu.demo.apollo; + +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping(value = "/demo/apollo") +public class ApolloController { + + + @Autowired + private ObjectDemoConfig objectDemoConfig; + + @GetMapping("/apolloDemoConfig") + public ObjectDemoConfig apolloDemoConfig() { + System.out.println("apolloDemoConfig:" + JSONObject.toJSONString(objectDemoConfig)); + return objectDemoConfig; + } + + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjcetDemoConfiguration.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjcetDemoConfiguration.java new file mode 100644 index 0000000..1052819 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjcetDemoConfiguration.java @@ -0,0 +1,15 @@ +package cn.lingniu.demo.apollo; + +import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableApolloConfig +public class ObjcetDemoConfiguration { + + @Bean + public ObjectDemoConfig apolloDemoConfig() { + return new ObjectDemoConfig(); + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjectDemoConfig.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjectDemoConfig.java new file mode 100644 index 0000000..f697aa4 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjectDemoConfig.java @@ -0,0 +1,39 @@ +package cn.lingniu.demo.apollo; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +@Data +@ConfigurationProperties(prefix = "apollo.demo") +public class ObjectDemoConfig implements Serializable { + + private String stringDemo = ""; + + private Boolean booleanDemo = true; + + private List listDemo = Lists.newArrayList(); + + private Set setDemo = Sets.newHashSet(); + + private Map hashDemo = Maps.newHashMap(); + + private Map hashObjectDemo = Maps.newHashMap(); + + private ObjectDemo objectDemo = new ObjectDemo(); + + + @Data + public static class ObjectDemo { + private String testObjcet; + } + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperController.java new file mode 100644 index 0000000..03e51a2 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperController.java @@ -0,0 +1,49 @@ +package cn.lingniu.demo.db; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; + +@RestController +@RequestMapping("/demo/db") +@Slf4j +public class JobZookeeperController { + + @Autowired + JobZookeeperServerMapper jobZookeeperServerMapper; + + @GetMapping("/page") + public Page getJobZookeeperServerPage() { + Page jobZookeeperServerPage = jobZookeeperServerMapper.selectPage(Page.of(1, 1), + new LambdaQueryWrapper().select(JobZookeeperServer::getName, JobZookeeperServer::getAddress, JobZookeeperServer::getDigest, + JobZookeeperServer::getJmxEnable, JobZookeeperServer::getJmxPort, JobZookeeperServer::getMessage, JobZookeeperServer::getCreateTime, + JobZookeeperServer::getUpdateTime, JobZookeeperServer::getStatus, JobZookeeperServer::getContactPhone, JobZookeeperServer::getUserName)); + log.info("jobZookeeperServerPage:{}", jobZookeeperServerPage); + return jobZookeeperServerPage; + } + + @GetMapping("/add") + public JobZookeeperServer addJobZookeeperServer() { + JobZookeeperServer server = new JobZookeeperServer(); + server.setName("test"); + server.setAddress("127.0.0.1:2181"); + server.setDigest(""); + server.setJmxEnable(1); + server.setJmxPort(""); + server.setMessage(""); + server.setCreateTime(new Date()); + server.setUpdateTime(new Date()); + server.setStatus(1); + server.setContactPhone(""); + server.setUserName(""); + jobZookeeperServerMapper.insert(server); + log.info("Added new JobZookeeperServer: {}", server); + return server; + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServer.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServer.java new file mode 100644 index 0000000..5308261 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServer.java @@ -0,0 +1,60 @@ +package cn.lingniu.demo.db; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + + +/** + * h2 建表语句: + + CREATE TABLE job_zk_namespace ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + address VARCHAR(255), + digest VARCHAR(255), + jmx_enable INTEGER DEFAULT 1, + jmx_port VARCHAR(255), + message VARCHAR(255), + create_time TIMESTAMP, + update_time TIMESTAMP, + status INTEGER DEFAULT 1, + contact_phone VARCHAR(255), + user_name VARCHAR(255) + ); + + */ +@Data +@TableName("job_zk_namespace") +public class JobZookeeperServer implements Serializable { + + @TableId(type = IdType.AUTO) + private Long id; + + private String name; + + private String address; + + private String digest; + + private Integer jmxEnable = 1; + + private String jmxPort; + + private String message; + + private Date createTime; + + private Date updateTime; + + private Integer status = 1; + + private String contactPhone; + + private String userName; + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServerMapper.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServerMapper.java new file mode 100644 index 0000000..4356382 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServerMapper.java @@ -0,0 +1,8 @@ +package cn.lingniu.demo.db; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface JobZookeeperServerMapper extends BaseMapper { +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/DemoData.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/DemoData.java new file mode 100644 index 0000000..0de9a0c --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/DemoData.java @@ -0,0 +1,26 @@ +package cn.lingniu.demo.file; + +import com.alibaba.excel.annotation.ExcelIgnore; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +@Getter +@Setter +@EqualsAndHashCode +public class DemoData { + @ExcelProperty("字符串标题") + private String string; + @ExcelProperty("日期标题") + private Date date; + @ExcelProperty("数字标题") + private Double doubleData; + /** + * 忽略这个字段 + */ + @ExcelIgnore + private String ignore; +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileController.java new file mode 100644 index 0000000..f28e9d2 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileController.java @@ -0,0 +1,79 @@ +package cn.lingniu.demo.file; + + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.util.ListUtils; +import com.alibaba.excel.write.metadata.WriteSheet; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; +import java.util.List; + +/** + * todo https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write + * easyexcel 性能好,api简单,上手更简单 + */ +@Slf4j +@RestController +@RequestMapping("/demo/file") +public class FileController { + + @GetMapping("/test1") + public String test1() { + simpleWrite(); + return "success"; + } + + /** + * 最简单的写 + *

+ * 1. 创建excel对应的实体对象 参照{@link DemoData} + *

+ * 2. 直接写即可 + */ + public void simpleWrite() { + // 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入 + + // 写法1 JDK8+ + // since: 3.0.0-beta1 + String fileName = FileTestUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx"; + // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 + // 如果这里想使用03 则 传入excelType参数即可 + EasyExcel.write(fileName, DemoData.class) + .sheet("模板") + .doWrite(() -> { + // 分页查询数据 + return data(); + }); + + // 写法2 + fileName = FileTestUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx"; + // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 + // 如果这里想使用03 则 传入excelType参数即可 + EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data()); + + // 写法3 + fileName = FileTestUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx"; + // 这里 需要指定写用哪个class去写 + try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) { + WriteSheet writeSheet = EasyExcel.writerSheet("模板").build(); + excelWriter.write(data(), writeSheet); + } + } + + private List data() { + List list = ListUtils.newArrayList(); + for (int i = 0; i < 10; i++) { + DemoData data = new DemoData(); + data.setString("字符串" + i); + data.setDate(new Date()); + data.setDoubleData(0.56); + list.add(data); + } + return list; + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileTestUtil.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileTestUtil.java new file mode 100644 index 0000000..849b020 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileTestUtil.java @@ -0,0 +1,76 @@ +package cn.lingniu.demo.file; + +import org.springframework.util.CollectionUtils; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class FileTestUtil { + + public static InputStream getResourcesFileInputStream(String fileName) { + return Thread.currentThread().getContextClassLoader().getResourceAsStream("" + fileName); + } + + public static String getPath() { + return FileTestUtil.class.getResource("/").getPath(); + } + + public static TestPathBuild pathBuild() { + return new TestPathBuild(); + } + + public static File createNewFile(String pathName) { + File file = new File(getPath() + pathName); + if (file.exists()) { + file.delete(); + } else { + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + } + return file; + } + + public static File readFile(String pathName) { + return new File(getPath() + pathName); + } + + public static File readUserHomeFile(String pathName) { + return new File(System.getProperty("user.home") + File.separator + pathName); + } + + /** + * build to test file path + **/ + public static class TestPathBuild { + private TestPathBuild() { + subPath = new ArrayList<>(); + } + + private final List subPath; + + public TestPathBuild sub(String dirOrFile) { + subPath.add(dirOrFile); + return this; + } + + public String getPath() { + if (CollectionUtils.isEmpty(subPath)) { + return FileTestUtil.class.getResource("/").getPath(); + } + if (subPath.size() == 1) { + return FileTestUtil.class.getResource("/").getPath() + subPath.get(0); + } + StringBuilder path = new StringBuilder(FileTestUtil.class.getResource("/").getPath()); + path.append(subPath.get(0)); + for (int i = 1; i < subPath.size(); i++) { + path.append(File.separator).append(subPath.get(i)); + } + return path.toString(); + } + + } + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/User.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/User.java new file mode 100644 index 0000000..bb7b6ad --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/User.java @@ -0,0 +1,24 @@ +package cn.lingniu.demo.jetcache.annotationDemo; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class User implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private String name; + private String email; + + public User() { + } + + public User(Long id, String name, String email) { + this.name = name; + this.email = email; + this.id = id; + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserDao.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserDao.java new file mode 100644 index 0000000..1421ea7 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserDao.java @@ -0,0 +1,53 @@ +package cn.lingniu.demo.jetcache.annotationDemo; + +import org.springframework.stereotype.Repository; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Repository +public class UserDao { + + // 模拟数据库存储 + private final ConcurrentHashMap userDatabase = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + /** + * 根据ID获取用户信息,使用缓存 + * @param id 用户ID + * @return 用户信息 + */ + public User getUserById(Long id) { + return userDatabase.get(id); + } + + /** + * 创建新用户 + * @param user 用户信息 + * @return 创建后的用户信息 + */ + public User createUser(User user) { + Long id = idGenerator.getAndIncrement(); + user.setId(id); + userDatabase.put(id, user); + return user; + } + + /** + * 更新用户信息并更新缓存 + * @param user 更新后的用户信息 + * @return 更新后的用户信息 + */ + public User updateUser(User user) { + userDatabase.put(user.getId(), user); + return user; + } + + /** + * 删除用户并清除缓存 + * @param id 用户ID + */ + public void deleteUser(Long id) { + userDatabase.remove(id); + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserService.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserService.java new file mode 100644 index 0000000..1ba6099 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserService.java @@ -0,0 +1,59 @@ +package cn.lingniu.demo.jetcache.annotationDemo; + +import com.alicp.jetcache.anno.CacheInvalidate; +import com.alicp.jetcache.anno.CacheType; +import com.alicp.jetcache.anno.CacheUpdate; +import com.alicp.jetcache.anno.Cached; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + + @Autowired + private UserDao userDao; + + /** + * 根据ID获取用户信息,使用缓存 + * @param id 用户ID + * @return 用户信息 + */ + @Cached(area = "ca2", name = "user:", key = "#id", expire = 3600, localExpire = 600, cacheType = CacheType.BOTH) + public User getUserById(Long id) { + // 模拟数据库查询延迟 + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return userDao.getUserById(id); + } + + /** + * 更新用户信息并更新缓存 + * @param user 更新后的用户信息 + * @return 更新后的用户信息 + */ + @CacheUpdate(area = "ca2", name = "user:", key = "#user.id", value = "#user") + public User updateUser(User user) { + userDao.updateUser(user); + return user; + } + + /** + * 删除用户并清除缓存 + * @param id 用户ID + */ + @CacheInvalidate(area = "ca2", name = "user:", key = "#id") + public void deleteUser(Long id) { + userDao.deleteUser(id); + } + + + + public User createUser(User user) { + return userDao.createUser(user); + } + + +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/JetCacheController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/JetCacheController.java new file mode 100644 index 0000000..299af59 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/JetCacheController.java @@ -0,0 +1,70 @@ +package cn.lingniu.demo.jetcache.controller; + +import cn.lingniu.demo.jetcache.annotationDemo.User; +import cn.lingniu.demo.jetcache.annotationDemo.UserService; +import com.alicp.jetcache.Cache; +import com.alicp.jetcache.SimpleCacheManager; +import com.alicp.jetcache.anno.CachePenetrationProtect; +import com.alicp.jetcache.anno.CacheRefresh; +import com.alicp.jetcache.anno.CacheType; +import com.alicp.jetcache.anno.Cached; +import com.alicp.jetcache.template.QuickConfig; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import java.util.concurrent.TimeUnit; + +@Slf4j +@RestController +@RequestMapping("/demo/cache") +public class JetCacheController { + + @Resource + private SimpleCacheManager jcCacheManager; + @Resource + private UserService userService; //todo 可以直接使用 + + @GetMapping + public String cache( + @RequestParam String localArea, + @RequestParam String remoteArea) { + Cache localCache = jcCacheManager.getOrCreateCache(QuickConfig.newBuilder(localArea, "user:").cacheType(CacheType.LOCAL).build()); + localCache.PUT("key1", "value3"); + Object value = localCache.GET("key1").getValue(); + log.info("value: {}", value); + + Cache remoteCache = jcCacheManager.getOrCreateCache(QuickConfig.newBuilder(remoteArea, "user:").build()); + remoteCache.PUT("key1", "value4"); + Object value1 = remoteCache.GET("key1").getValue(); + log.info("value1: {}", value1); + + return "success"; + } + + @GetMapping("/anno") + @CachePenetrationProtect + @Cached(name = "orderCache:", key = "#orderId", expire = 10, cacheType = CacheType.BOTH) + @CacheRefresh(refresh = 10, timeUnit = TimeUnit.MINUTES) + public KeyValueEntity getOrderById(String orderId) { + KeyValueEntity keyValueEntity = new KeyValueEntity(); + keyValueEntity.setId(orderId); + keyValueEntity.setKey("key"); + keyValueEntity.setValue("value"); + return keyValueEntity; + } + + + + @GetMapping("/userTest") + public String userTest() { + userService.createUser(new User(1L, "create", "create" )); + User temp1 = userService.getUserById(1L); + userService.updateUser(new User(1L, "update", "update")); + User temp2 = userService.getUserById(1L); + userService.deleteUser(1L); + return "success"; + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/KeyValueEntity.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/KeyValueEntity.java new file mode 100644 index 0000000..dd35c90 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/KeyValueEntity.java @@ -0,0 +1,18 @@ +package cn.lingniu.demo.jetcache.controller; + +import lombok.Data; + +import java.io.Serializable; + +/** + * + */ +@Data +public class KeyValueEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + String id; + String key; + String value; +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/job/SampleXxlJob.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/job/SampleXxlJob.java new file mode 100644 index 0000000..a5bca66 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/job/SampleXxlJob.java @@ -0,0 +1,63 @@ +package cn.lingniu.demo.job; + +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.annotation.XxlJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * XxlJob开发示例(Bean模式) + *

+ * 开发步骤: + * 1、任务开发:在Spring Bean实例中,开发Job方法; + * 2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。 + * 3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志; + * 4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果; + * + */ +@Component +public class SampleXxlJob { + private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class); + + + /** + * 1、简单任务示例(Bean模式) + */ + @XxlJob("demoJobHandler") + public void demoJobHandler() throws Exception { + XxlJobHelper.log("XXL-JOB, Hello World."); + + for (int i = 0; i < 5; i++) { + XxlJobHelper.log("beat at:" + i); + TimeUnit.SECONDS.sleep(2); + } + // default success + } + + + /** + * 2、分片广播任务 + */ + @XxlJob("shardingJobHandler") + public void shardingJobHandler() throws Exception { + + // 分片参数 + int shardIndex = XxlJobHelper.getShardIndex(); + int shardTotal = XxlJobHelper.getShardTotal(); + + XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal); + + // 业务逻辑 + for (int i = 0; i < shardTotal; i++) { + if (i == shardIndex) { + XxlJobHelper.log("第 {} 片, 命中分片开始处理", i); + } else { + XxlJobHelper.log("第 {} 片, 忽略", i); + } + } + + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/CloudController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/CloudController.java new file mode 100644 index 0000000..2da8cd3 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/CloudController.java @@ -0,0 +1,30 @@ +package cn.lingniu.demo.microservice; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/demo/cloud") +public class CloudController { + + @Autowired + private DemoFeignClient demoFeignClient; + + @GetMapping("/contributors") + public CommonResult> contributors() { + return demoFeignClient.contributors("OpenFeign", "feign"); + } + + @GetMapping("/reposNotFound") + public CommonResult> reposNotFound() { + return demoFeignClient.reposNotFound("OpenFeign", "feign"); + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/Contributor.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/Contributor.java new file mode 100644 index 0000000..77be830 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/Contributor.java @@ -0,0 +1,9 @@ +package cn.lingniu.demo.microservice; + +import lombok.Data; + +@Data +public class Contributor { + String login; + int contributions; +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/DemoFeignClient.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/DemoFeignClient.java new file mode 100644 index 0000000..aa2020d --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/DemoFeignClient.java @@ -0,0 +1,25 @@ +package cn.lingniu.demo.microservice; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@FeignClient(name = "lingniu-framework-provider-demo" +//外部接口配置url: @FeignClient(name = "github-api", url = "https://api.github.com" +// 方式1 配置fallback +// , fallback = GithubApiClientFallBack.class +// 方式2 配置fallbackFactory +// , fallbackFactory = GithubApiClientFallbackFactory.class +) +public interface DemoFeignClient { + + @GetMapping("/repos/{owner}/{repo}/contributors") + CommonResult> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo); + + @GetMapping("/reposNotFound") + CommonResult> reposNotFound(@RequestParam("owner") String owner, @RequestParam("repo") String repo); +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallBack.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallBack.java new file mode 100644 index 0000000..eff427e --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallBack.java @@ -0,0 +1,24 @@ +package cn.lingniu.demo.microservice; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + + +@Slf4j +@Component +public class GithubApiClientFallBack implements DemoFeignClient { + + @Override + public CommonResult> contributors(String owner, String repo) { + throw new RuntimeException("contributors no fallback"); + } + + @Override + public CommonResult> reposNotFound(String owner, String repo) { + log.info("reposNotFoundWithFallBack"); + return CommonResult.success(null); + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallbackFactory.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallbackFactory.java new file mode 100644 index 0000000..78e7b7f --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallbackFactory.java @@ -0,0 +1,21 @@ +package cn.lingniu.demo.microservice; + +import cn.lingniu.framework.plugin.core.exception.ServerException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +/** + * + */ +@Slf4j +@Component +public class GithubApiClientFallbackFactory implements FallbackFactory { + @Override + public GithubApiClientFallBack create(Throwable cause) { + if (cause instanceof ServerException) { + return new GithubApiClientFallBack(); + } + return null; + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/monitor/PrometheusController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/monitor/PrometheusController.java new file mode 100644 index 0000000..2f4459a --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/monitor/PrometheusController.java @@ -0,0 +1,82 @@ +package cn.lingniu.demo.monitor; + + +import cn.lingniu.framework.plugin.prometheus.PrometheusCounter; +import cn.lingniu.framework.plugin.prometheus.PrometheusMetrics; +import cn.lingniu.framework.plugin.prometheus.PrometheusSummary; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Metrics; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * http://localhost:30290/actuator + * 访问 http://localhost:30290/actuator/metrics/metricName 查看指标 + */ +@RestController +@RequestMapping("/demo/prometheus") +public class PrometheusController { + + /** + * + Counter(计数器) + Counter 是最简单的指标类型,它是一个单调递增的计数器,只能增加或重置为 0。通常用于记录请求总数、任务完成数、错误数等累计数据 */ + @GetMapping("/prometheusCounter") + @PrometheusCounter( labels = {"prometheusCounter","test"}, memo = "prometheusCounter") + public String prometheusCounter() { + return "prometheusCounter"; + } + + + /** + DistributionSummary(分布摘要) + DistributionSummary 与 Histogram 类似,也用于测量数值的分布情况,但它专门用于跟踪事件的大小或持续时间等非时间性指标 + */ + @GetMapping("/prometheusSummary") + @PrometheusSummary(labels = {"prometheusSummary","test"}, memo = "prometheusSummary") + public String prometheusSummary() { + return "prometheusSummary"; + } + + + /** + Histogram(直方图) + Histogram 用于测量数据的分布情况,如请求延迟、响应大小等。它会统计样本数据的分布情况,并提供分位数计算功能。 + */ + @GetMapping("/prometheusMetrics") + @PrometheusMetrics(name = "prometheusMetrics") + public String prometheusMetrics() { + return "prometheusMetrics"; + } + + + /** + * 自定义埋点 + */ + @GetMapping("/selfMonitor") + public String selfMonitor() { + long startTime = System.currentTimeMillis(); + Counter counter = Metrics.counter("selfMonitorCount", "selfMonitor", "selfMonitorCount"); + counter.increment(); + + DistributionSummary distributionSummary = Metrics.summary("selfMonitorSummary", "selfMonitor", "selfMonitorSummary"); + distributionSummary.record(System.currentTimeMillis() - startTime); + + + return "selfMonitor"; + } + + + + /** + * throw 异常 测试 + */ + @GetMapping("/throw") + public String throwEx() { + throw new RuntimeException("test"); + } + + +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/LockActionDemo.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/LockActionDemo.java new file mode 100644 index 0000000..19d8730 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/LockActionDemo.java @@ -0,0 +1,88 @@ +package cn.lingniu.demo.redisson; + +import cn.lingniu.framework.plugin.redisson.RedissonLockAction; +import cn.lingniu.framework.plugin.redisson.RedissonLockType; +import org.springframework.core.annotation.Order; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class LockActionDemo { + + + @RedissonLockAction(redissonName = "r1", lockType = RedissonLockType.REENTRANT_LOCK, + waitTime = 3000L, leaseTime = 30000L, unit = TimeUnit.MILLISECONDS, + throwException = true) + public void testLockAction(String test) { + + } + + /** + * 使用 #orderId 参数作为锁的key + */ + @PostMapping("/process/{orderId}") + @RedissonLockAction( + redissonName = "r1", + key = "#orderId", + lockType = RedissonLockType.REENTRANT_LOCK, + waitTime = 3000L, leaseTime = 30000L, unit = TimeUnit.MILLISECONDS, + throwException = true + ) + public ResponseEntity processOrder(@PathVariable String orderId) { + // 业务逻辑 + return ResponseEntity.ok("Order processed: " + orderId); + } + + /** + * 使用请求参数 + */ + @GetMapping("/query") + @RedissonLockAction( + redissonName = "defaultRedisson", + key = "'query:' + #userId + ':' + #status", + throwException = true + ) + public ResponseEntity> queryOrders( + @RequestParam String userId, + @RequestParam String status) { + // 业务逻辑 + return ResponseEntity.ok(new ArrayList<>()); + } + + /** + * 使用多个参数组合生成key + */ + @PostMapping("/update") + @RedissonLockAction( + redissonName = "defaultRedisson", + key = "'order:' + #orderDTO.orderId + ':user:' + #orderDTO.userId", + waitTime = 5000L + ) + public ResponseEntity updateOrder(@RequestBody OrderDTO orderDTO) { + // 业务逻辑 + return ResponseEntity.ok("Order updated"); + } + + + + public class OrderDTO { + private String orderId; + private String userId; + private String status; + + // getters and setters + public String getOrderId() { return orderId; } + public void setOrderId(String orderId) { this.orderId = orderId; } + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/RedissonLockControllerDemo.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/RedissonLockControllerDemo.java new file mode 100644 index 0000000..b116b01 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/RedissonLockControllerDemo.java @@ -0,0 +1,99 @@ +package cn.lingniu.demo.redisson; + +import cn.lingniu.framework.plugin.redisson.RedissonClientFactory; +import cn.lingniu.framework.plugin.redisson.RedissonClusterLockerService; +import cn.lingniu.framework.plugin.redisson.RedissonLockAction; +import org.redisson.api.RBucket; +import org.redisson.api.RLock; +import org.redisson.api.RTopic; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import java.util.concurrent.TimeUnit; + +/** + * 包含分布式锁和集群操作 + */ +@RestController +@RequestMapping("/demo/redisson") +public class RedissonLockControllerDemo { + + @Autowired + RedissonClusterLockerService redissonClusterLockerService; + //指定数据源:r1 + private final RedissonClient redissonClient; + + public RedissonLockControllerDemo() { + this.redissonClient = RedissonClientFactory.getRedissonClient("r1"); + } + + @GetMapping("/lock") + public String acquireLock( + @RequestParam String lockName, + @RequestParam long leaseTime) { + RLock lock = redissonClient.getLock(lockName); + try { + boolean isLocked = lock.tryLock(5, leaseTime, TimeUnit.SECONDS); + if (isLocked) { + return "锁获取成功"; + } else { + return "锁获取失败"; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "锁获取异常"; + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + @GetMapping("/bucket/set") + public String setBucketValue( + @RequestParam String key, + @RequestParam String value + ) { + RBucket bucket = redissonClient.getBucket(key); + bucket.set(value); + return "键值设置成功"; + } + + @GetMapping("/bucket/get/{key}") + public String getBucketValue(@PathVariable String key) { + RBucket bucket = redissonClient.getBucket(key); + return "结果:key:" + key + ",value:=" + bucket.get(); + } + + @GetMapping("/topic/publish") + public String publishMessage( @RequestParam String topicName, + @RequestParam String message + ) { + RTopic topic = redissonClient.getTopic(topicName); + topic.publish(message); + return "消息发布成功"; + } + + @GetMapping("/lockAction") + @RedissonLockAction(redissonName = "r1") + public String redisson() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "lockAction"; + } + + @GetMapping("/redissonClusterLockerService") + public String redissonClusterLockerService() { + return redissonClusterLockerService.wrapLock(this, + x -> "r1", x -> "key1", + x -> "success"); + } + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/RocketMqController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/RocketMqController.java new file mode 100644 index 0000000..4acde78 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/RocketMqController.java @@ -0,0 +1,58 @@ +package cn.lingniu.demo.rocketmq; + +import cn.lingniu.demo.jetcache.controller.KeyValueEntity; +import cn.lingniu.framework.plugin.rocketmq.core.producer.RocketMqSendMsgBody; +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnFail; +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnSuccess; +import org.apache.rocketmq.client.producer.SendResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@RestController +@RequestMapping("/demo/rocketmq") +public class RocketMqController { + + @Autowired + TestMqProducer testProducer; + + @Autowired + TestTransMqProducer testTransProducer; + + @GetMapping("/test") + public String test() { + for (int i = 0; i < 10; i++) { + KeyValueEntity body = new KeyValueEntity(); + String id = UUID.randomUUID().toString(); + body.setId(id); + body.setKey("key" + id); + body.setValue("value" + id); + + RocketMqSendMsgBody message = new RocketMqSendMsgBody<>(); + message.setBody(body); + if (i > 5) { + message.setDelayLevel(1); + } + SendResult sendResult = testProducer.syncSend(message); + System.out.println(sendResult); + + testProducer.asyncSend(message, new SendMessageOnSuccess() { + @Override + public void call(KeyValueEntity message, SendResult result) { + System.out.println(result); + } + }, new SendMessageOnFail() { + @Override + public void call(KeyValueEntity message, Throwable ex) { + System.out.println(ex); + } + }); + + testTransProducer.syncSendInTransaction(message, null); + } + return "over"; + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestC1Consumer.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestC1Consumer.java new file mode 100644 index 0000000..82f7e8f --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestC1Consumer.java @@ -0,0 +1,21 @@ +package cn.lingniu.demo.rocketmq; + +import cn.lingniu.demo.jetcache.controller.KeyValueEntity; +import cn.lingniu.framework.plugin.rocketmq.RocketMqConsumer; +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.RocketMqListener; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.common.message.MessageExt; + +/** + * 消费测试 + */ +@Slf4j +@RocketMqConsumer("consumer1") +public class TestC1Consumer implements RocketMqListener { + + @Override + public void onMessage(KeyValueEntity message, MessageExt messageExt) { + log.info("message{}", message); + log.info("messageExt{}", messageExt); + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestMqProducer.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestMqProducer.java new file mode 100644 index 0000000..34ce2d3 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestMqProducer.java @@ -0,0 +1,12 @@ +package cn.lingniu.demo.rocketmq; + +import cn.lingniu.demo.jetcache.controller.KeyValueEntity; +import cn.lingniu.framework.plugin.rocketmq.RocketMqProducer; +import org.springframework.stereotype.Component; + +/** + * 普通生产者 + */ +@Component +public class TestMqProducer extends RocketMqProducer { +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestTransMqProducer.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestTransMqProducer.java new file mode 100644 index 0000000..62718ee --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestTransMqProducer.java @@ -0,0 +1,49 @@ +package cn.lingniu.demo.rocketmq; + +import cn.lingniu.demo.jetcache.controller.KeyValueEntity; +import cn.lingniu.framework.plugin.rocketmq.RocketMqProducer; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 事物消息 + */ +@Component +public class TestTransMqProducer extends RocketMqProducer implements TransactionListener { + + private AtomicInteger transactionIndex = new AtomicInteger(0); + + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message message, Object o) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + localTrans.put(message.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt messageExt) { + Integer status = localTrans.get(messageExt.getTransactionId()); + if (null != status) { + switch (status) { + case 0 : + return LocalTransactionState.UNKNOW; + case 1 : + return LocalTransactionState.COMMIT_MESSAGE; + case 2 : + return LocalTransactionState.ROLLBACK_MESSAGE; + default: + return LocalTransactionState.COMMIT_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebController.java new file mode 100644 index 0000000..b3f8c26 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebController.java @@ -0,0 +1,54 @@ +package cn.lingniu.demo.web; + + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.web.apilog.ApiAccessLog; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/demo/web") +public class WebController { + + + @GetMapping("/errorResult") + public WebEntity testRrrorResult() { + WebEntity webEntity = new WebEntity(); + webEntity.setId(1L); + webEntity.setName("error"); + webEntity.setEmail(""); + return webEntity; + } + + @GetMapping("/commonResult") + public CommonResult test() { + WebEntity webEntity = new WebEntity(); + webEntity.setId(1L); + webEntity.setName("lingniu"); + webEntity.setEmail(""); + return CommonResult.success(webEntity); + } + + + @ApiAccessLog(responseEnable = true) + @GetMapping("/apiLog") + public CommonResult apiLog() { + WebEntity webEntity = new WebEntity(); + webEntity.setId(1L); + webEntity.setName("lingniu"); + webEntity.setEmail(""); + return CommonResult.success(webEntity); + } + + + @ApiAccessLog(responseEnable = true) + @GetMapping("/apiEncrypt") + public CommonResult apiEncrypt() { + WebEntity webEntity = new WebEntity(); + webEntity.setId(1L); + webEntity.setName("lingniu"); + webEntity.setEmail(""); + return CommonResult.success(webEntity); + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebEntity.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebEntity.java new file mode 100644 index 0000000..e31f033 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebEntity.java @@ -0,0 +1,10 @@ +package cn.lingniu.demo.web; + +import lombok.Data; + +@Data +public class WebEntity { + private Long id; + private String name; + private String email; +} diff --git a/demo/lingniu-framework-demo/src/main/resources/application-db.yml b/demo/lingniu-framework-demo/src/main/resources/application-db.yml new file mode 100644 index 0000000..edf0b77 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-db.yml @@ -0,0 +1,12 @@ +framework: + lingniu: + # 框架数据源配置 + datasource: + # 开启框架 sql执行分析 + # dev,local,lpt,fat,test,uat,sit 这些环境可开启 + sqlLog: true + # druid 后台地址:http://localhost:8080/druid/login.html + # druid 账号,默认值为 SpringApplicationProperties#name + druidUsername: "your_druid_username" + # druid 密码,默认值为 SpringApplicationProperties#name + "123" + druidPassword: "your_druid_password" diff --git a/demo/lingniu-framework-demo/src/main/resources/application-jetcache.yml b/demo/lingniu-framework-demo/src/main/resources/application-jetcache.yml new file mode 100644 index 0000000..d1c4fe8 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-jetcache.yml @@ -0,0 +1,58 @@ +framework: + lingniu: + jetcache: + # 需要隐藏的包路径(数组),默认为空 + hiddenPackages: [ ] + # 统计信息输出间隔(分钟),默认为 0(不输出) + statIntervalMinutes: 10 + # 是否在缓存名称中包含区域(area)信息,默认为 true + areaInCacheName: true + # 是否开启缓存穿透保护,默认为 false + penetrationProtect: false + # 本地缓存配置 + local: + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + ca2: + # 本地缓存实现类类型,支持:linkedhashmap/caffeine + type: linkedhashmap + # 缓存key数量限制, 默认100 + limit: 100 + default: + # 本地缓存实现类类型,支持:linkedhashmap/caffeine + type: caffeine + # 缓存key数量限制, 默认100 + limit: 100 + # 过期时间,0为不失效 + expireAfterAccessInMillis: 0 + # key 转换器,用于序列化,目前支持:NONE、FASTJSON、JACKSON、FASTJSON2 + keyConvertor: FASTJSON2 + remote: + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + ca2: + # 远程缓存实现类类型,目前支持:redisson + type: redisson + # 远程缓存客户端名称,默认r1 -- todo 需要在application-redisson.yml中配置 + redissonClient: r1 + # key 前缀 + keyPrefix: "cacheKeyPrefix:" + # 连接超时时间ms + timeout: 2000 + # 重试次数 + maxAttempt: 5 + # 连接池配置 + poolConfig: + maxTotal: 8 + maxIdle: 8 + minIdle: 0 + # key 转换器,用于序列化,目前支持:NONE、FASTJSON、JACKSON、FASTJSON2 + keyConvertor: FASTJSON2 + # JAVA, KRYO,KRYO5,自定义spring beanName + valueEncoder: JAVA + # JAVA, KRYO,KRYO5,自定义spring beanName + valueDecoder: JAVA + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + default: + # 远程缓存实现类类型,目前支持:redisson + type: redisson + # 远程缓存客户端名称,默认r1 -- todo 需要在application-redisson.yml中配置 + redissonClient: r1 diff --git a/demo/lingniu-framework-demo/src/main/resources/application-prometheus.yml b/demo/lingniu-framework-demo/src/main/resources/application-prometheus.yml new file mode 100644 index 0000000..dc56806 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-prometheus.yml @@ -0,0 +1,6 @@ +framework: + lingniu: + prometheus: + enabled: true + port: 30290 + allowAssignPort: false diff --git a/demo/lingniu-framework-demo/src/main/resources/application-redisson.yml b/demo/lingniu-framework-demo/src/main/resources/application-redisson.yml new file mode 100644 index 0000000..35c4c2b --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-redisson.yml @@ -0,0 +1,44 @@ +framework: + lingniu: + redisson: + remote: + r1: + # 应用id需在cachecloud申请 + redisAddresses: 10.130.36.242:6387 + # 客户端名称,默认为应用名 + IP + clientName: "" + # 单节点 Redis 默认数据库,默认为 0 + database: 0 + # 连接池最小空闲连接数,默认为 12 + connectionMinimumIdleSize: 12 + # 连接池最大连接数,默认为 32 + connectionPoolSize: 32 + # 空闲连接超时时间(毫秒),默认为 10000 + idleConnectionTimeout: 10000 + # 连接超时时间(毫秒),默认为 2000 + connectTimeout: 2000 + # 操作超时时间(毫秒),默认为 2000 + timeout: 2000 + # 操作重试次数,默认为 4 + retryAttempts: 4 + # 集群模式下是否检查 Slots 覆盖,默认为 true + checkSlotsCoverage: true + # 读取模式,默认为 "SLAVE" + # 可选值: + # - SLAVE: 从节点读取 + # - MASTER: 主节点读取 + # - MASTER_SLAVE: 主从节点读取 + readMode: "SLAVE" + # 存储类型,默认为 "CLUSTER" + # 可选值: + # - CLUSTER: 集群模式 + # - SINGLE: 单节点模式 + # - SENTINEL: 哨兵模式 + # - REPLICATED: 复制模式 + storageType: "CLUSTER" + # 实例名称 + r2: + # 应用id需在cachecloud申请 + redisAddresses: 10.130.36.242:6387 + # 单节点 Redis 默认数据库,默认为 0 + database: 0 diff --git a/demo/lingniu-framework-demo/src/main/resources/application-rocketmq.yml b/demo/lingniu-framework-demo/src/main/resources/application-rocketmq.yml new file mode 100644 index 0000000..36b4fe5 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-rocketmq.yml @@ -0,0 +1,65 @@ + +framework: + lingniu: + # rocketmq所有配置示例 + rocketmq: + # 是否开启数据源加密 + encryptEnabled: false + # 消费者配置 + consumers: + # 消费者bean名称 + TestC1Consumer: + nameServer: mq_test_n1.tst.mid:9876 + # 消费组 + consumerGroup: consumer1 + # 消费对象名字 + consumerBeanName: TestC1Consumer + # Topic + topic: test1-yw-topic + # 订阅表达式,默认为 * + selectorExpress: "*" + # 消费者最小线程数 + consumeThreadMin: 20 + # 消费者最大线程数 + consumeThreadMax: 32 + # 批量消费数量 + consumeMessageBatchMaxSize: 1 + # 批量拉取消息数量 + pullBatchSize: 32 + # 消息的最大重试次数 + maxRetryTimes: 16 + + # 生产者配置 + producers: + # 生产者的bean名称 + TestMqProducer: + nameServer: mq_test_n1.tst.mid:9876 + # 生产者组 + group: test1-yw-topic-producer + # 生产端Topic + topic: test1-yw-topic + # 发送超时(ms),默认 3000 + sendMsgTimeout: 3000 + # 消息体压缩阀值(kb),默认 4096 + compressMsgBodyOverHowmuch: 4096 + # 同步发送消息失败,是否重新发送,默认 2 + retryTimesWhenSendFailed: 2 + # 异步发送消息失败,是否重新发送,默认 2 + retryTimesWhenSendAsyncFailed: 2 + # 重试另一个Broker当发送消息失败, 默认 false + retryAnotherBrokerWhenNotStoreOk: false + # 默认不启用延迟容错,通过统计每个队列的发送耗时情况来计算broker是否可用 + sendLatencyFaultEnable: false + TestTransMqProducer: + nameServer: mq_test_n1.tst.mid:9876 + group: test1-yw-trans-topic-producer + topic: test1-yw-trans-topic + # 是否事务型 + isTransactionMQ: true + # 事务消息线程配置 + transaction: + corePoolSize: 5 + # 本地事务执行结果查询线程池配置 + maximumPoolSize: 10 + keepAliveTime: 200 + queueCapacity: 2000 diff --git a/demo/lingniu-framework-demo/src/main/resources/application-web.yml b/demo/lingniu-framework-demo/src/main/resources/application-web.yml new file mode 100644 index 0000000..0ed62e3 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-web.yml @@ -0,0 +1,12 @@ +framework: + lingniu: + web: + apiLog: + #开关 + enable: true + apiEncrypt: + #开关 + enable: false + algorithm: "AES" + requestKey: "xxx" + responseKey: "xxx" diff --git a/demo/lingniu-framework-demo/src/main/resources/application-xxljob.yml b/demo/lingniu-framework-demo/src/main/resources/application-xxljob.yml new file mode 100644 index 0000000..bcedbe6 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-xxljob.yml @@ -0,0 +1,21 @@ +framework: + lingniu: + # XXL-JOB 配置 + xxljob: + # 调度中心配置 + admin: + # 调度中心部署根地址(多个地址用逗号分隔,为空则关闭自动注册) + adminAddresses: "http://10.130.119.181:8099/xxl-job-admin" + # 调度中心通讯TOKEN(非空时启用) + accessToken: "default_token" + + # 执行器配置 + executor: + # 执行器IP(默认为空表示自动获取IP) + ip: "" + # 执行器端口号(小于等于0则自动获取,默认9999) + port: 9999 + # 执行器日志文件保存天数(大于等于3时生效,-1表示关闭自动清理,默认7天) + logRetentionDays: 7 + # 执行器运行日志文件存储磁盘路径(需对该路径拥有读写权限,为空则使用默认路径) + logPath: logs/applogs/xxl-job/jobhandler \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/resources/application.yml b/demo/lingniu-framework-demo/src/main/resources/application.yml new file mode 100644 index 0000000..efbba01 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application.yml @@ -0,0 +1,48 @@ +spring: + application: + name: lingniu-framework-demo + profiles: + active: dev,redisson,jetcache,xxljob,db,rocketmq,prometheus,web + cloud: + nacos: + enabled: true #开启微服务自定义注册,自动获取当前框架信息,启动时间等到metadata中 + discovery: + server-addr: http://10.130.131.205:18848 #注册中心地址 + username: nacos #注册中心用户名 + password: nacos #注册中心密码 + + #todo db基本配置,连接池请参考:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter + datasource: + dynamic: + primary: master + datasource: + #如需连接mysql,需修改下面的配置 + master: + driver-class-name: org.h2.Driver + url: jdbc:h2:./testdb + username: sa + password: sa + slaver: + driver-class-name: org.h2.Driver + url: jdbc:h2:./testdb + username: sa + password: sa + # http://localhost:8080/h2-console 访问 h2 数据库 账号: sa/sa + h2: + console: + enabled: true + path: /h2-console + + +# apollo 配置 +framework: + lingniu: + apollo: + #namespaces必须配置,配置文件列表,以逗号分割 + namespaces: application,applicationTest.yml + # 是否开启 Apollo 配置,默认true + enabled: true + # appId 等于 spring.application.name,无需配置 + appId: test-ln + # meta 地址,框架自动获取,无需配置 + meta: http://10.130.119.181:8080 diff --git a/demo/lingniu-framework-demo/src/main/resources/applicationTest-a.yml b/demo/lingniu-framework-demo/src/main/resources/applicationTest-a.yml new file mode 100644 index 0000000..3eb9989 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/applicationTest-a.yml @@ -0,0 +1,22 @@ +apollo: + demo: + string-demo: "示例字符串" + boolean-demo: true + list-demo: + - "元素1" + - "元素2" + - "元素3" + set-demo: + - "集合元素1" + - "集合元素2" + hash-demo: + key1: "值1" + key2: "值2" + key3: "值3" + hash-object-demo: + obj1: + test-objcet: "对象示例1" + obj2: + test-objcet: "对象示例2" + object-demo: + test-objcet: "对象示例" \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/resources/logback-spring.xml b/demo/lingniu-framework-demo/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..36fed7b --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/logback-spring.xml @@ -0,0 +1,66 @@ + + + + + + + + logback + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + true + + + + ${log.path}/log-%d{yyyy-MM-dd-HH}.%i.log + + 30MB + + + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/lingniu-framework-provider-demo/pom.xml b/demo/lingniu-framework-provider-demo/pom.xml new file mode 100644 index 0000000..4a76911 --- /dev/null +++ b/demo/lingniu-framework-provider-demo/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + + + lingniu-framework-provider-demo + jar + + + + + + + cn.lingniu.framework + lingniu-framework-plugin-web + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-nacos + + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + com.alibaba + fastjson + + + ch.qos.logback + logback-classic + + + + + + + diff --git a/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/ProviderDemoApplication.java b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/ProviderDemoApplication.java new file mode 100644 index 0000000..e23b4a4 --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/ProviderDemoApplication.java @@ -0,0 +1,13 @@ +package cn.lingniu.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan(basePackages = {"cn.lingniu.*"}) +@SpringBootApplication +public class ProviderDemoApplication { + public static void main(String[] args) { + SpringApplication.run(ProviderDemoApplication.class, args); + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/Contributor.java b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/Contributor.java new file mode 100644 index 0000000..269265b --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/Contributor.java @@ -0,0 +1,11 @@ +package cn.lingniu.demo.web; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class Contributor { + String login; + int contributions; +} \ No newline at end of file diff --git a/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/GithubApiController.java b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/GithubApiController.java new file mode 100644 index 0000000..a249e6c --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/GithubApiController.java @@ -0,0 +1,23 @@ +package cn.lingniu.demo.web; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequestMapping +public class GithubApiController { + + @GetMapping("/repos/{owner}/{repo}/contributors") + public CommonResult> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo) { + // 模拟返回贡献者列表 + Contributor contributor1 = new Contributor("user1", 10); + Contributor contributor2 = new Contributor("user2", 5); + List response = new ArrayList<>(); + response.add(contributor1); + response.add(contributor2); + return CommonResult.success(response); + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-provider-demo/src/main/resources/application-web.yml b/demo/lingniu-framework-provider-demo/src/main/resources/application-web.yml new file mode 100644 index 0000000..17044d8 --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/resources/application-web.yml @@ -0,0 +1,9 @@ +framework: + lingniu: + web: + apiLog: + #开关 + enable: true + spring: + cloud: + isOkHttp: false \ No newline at end of file diff --git a/demo/lingniu-framework-provider-demo/src/main/resources/application.yml b/demo/lingniu-framework-provider-demo/src/main/resources/application.yml new file mode 100644 index 0000000..caf0205 --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/resources/application.yml @@ -0,0 +1,15 @@ +server: + port: 8081 +spring: + application: + name: lingniu-framework-provider-demo + profiles: + active: dev,web + cloud: + nacos: + enabled: true #开启微服务自定义注册,自动获取当前框架信息,启动时间等到metadata中 + discovery: + server-addr: http://10.130.131.205:18848 #注册中心地址 + username: nacos #注册中心用户名 + password: nacos #注册中心密码 + diff --git a/demo/lingniu-framework-provider-demo/src/main/resources/logback-spring.xml b/demo/lingniu-framework-provider-demo/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b78b63c --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/resources/logback-spring.xml @@ -0,0 +1,65 @@ + + + + + + + + logback + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + true + + + + ${log.path}/log-%d{yyyy-MM-dd-HH}.%i.log + + 30MB + + + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/apollo安装文档.docx b/doc/apollo安装文档.docx new file mode 100644 index 0000000000000000000000000000000000000000..1f9cfc540fc475dfb91d2147200263f991b1bf44 GIT binary patch literal 26773 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%0DuR~pz4lVYD#!?lYOqy7TnF$AR=8Y&6 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 WGEgsL2M|pf8W<`lYYwI_zW)dGhJ?}p literal 0 HcmV?d00001 diff --git a/doc/nacos安装文档.docx b/doc/nacos安装文档.docx new file mode 100644 index 0000000000000000000000000000000000000000..643cf29823309142e0e11fabb4dd2b929652c6a7 GIT binary patch literal 31819 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

LZ% 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

3E+R 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 F>px+qK>z>% literal 0 HcmV?d00001 diff --git a/lingniu-framework-dependencies/pom.xml b/lingniu-framework-dependencies/pom.xml new file mode 100644 index 0000000..fbe6029 --- /dev/null +++ b/lingniu-framework-dependencies/pom.xml @@ -0,0 +1,664 @@ + + + + cn.lingniu.framework + lingniu-framework-parent + 1.0.0-SNAPSHOT + + 4.0.0 + pom + lingniu-framework-dependencies + + + 17 + 1.0.0-SNAPSHOT + 2.4.0 + 3.12.0 + 1.70 + 2025.0.0.0 + 3.3.0 + 2.19.2 + 2.19.2 + 4.12.0 + 3.5.0 + 2025.0.0 + 13.6 + UTF-8 + 3.18.0 + 3.2.2 + 33.4.8-jre + 1.2.27 + 5.1.0 + 5.8.41 + + + + 3.1.0 + 3.6.0 + 3.3.1 + 3.11.0 + 3.6.0 + 3.1.1 + 2.10 + 3.3.0 + 3.1.2 + 3.1.1 + 3.6.0 + 3.4.0 + 3.3.0 + 3.5.0 + 3.3.1 + 3.5.0 + 3.12.1 + 3.3.0 + 3.1.2 + 3.4.0 + 2.16.0 + + + + 1.5.18 + 2.0.17 + 2.7.8 + 3.1.1 + 2.0.53 + 3.1.1 + 2.20.0 + 6.5.5.RELEASE + 6.0.0 + 2.24.3 + 4.0.3 + 5.1 + + 4.3.1 + 3.5.12 + 5.0.1 + 1.0.2 + 3.5.19 + 3.0.4 + 2.1.1 + 2.14.0 + 21.9.0.0 + + + + + + commons-io + commons-io + ${commons.io.version} + + + org.projectlombok + lombok + compile + + + org.springframework.boot + spring-boot-configuration-processor + true + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + cn.lingniu.framework + lingniu-framework-plugin-util + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-core + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-apollo + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-xxljob + ${framework.version} + + + + cn.lingniu.framework + lingniu-framework-plugin-mybatis + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-jetcache + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-redisson + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-rocketmq + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-skywalking + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-prometheus + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-web + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-nacos + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-easyexcel + ${framework.version} + + + com.ctrip.framework.apollo + apollo-client + ${apollo.version} + + + + + com.oracle + ojdbc14 + 10.2.0.4.0 + + + com.alibaba + easyexcel + ${easyexcel.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-to-slf4j + ${log4j.version} + + + + + com.github.jsqlparser + jsqlparser + ${jsqlparser.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + io.lettuce + lettuce-core + ${lettuce.version} + + + ojdbc + ojdbc6 + 11.2.0.4.0 + + + org.bouncycastle + bcprov-jdk15on + ${bcprov.version} + + + + redis.clients + jedis + ${jedis.version} + + + + + + com.squareup.okhttp3 + okhttp + ${okhttp3.version} + + + cn.hutool + hutool-json + ${hutool-version} + + + cn.hutool + hutool-core + ${hutool-version} + + + cn.hutool + hutool-http + ${hutool-version} + + + cn.hutool + hutool-all + ${hutool-version} + + + + + org.redisson + redisson + 3.17.5 + + + + + com.google.code.gson + gson + 2.9.1 + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.db4j + reflectasm + 1.11.4-2 + + + + + joda-time + joda-time + ${joda-time.version} + + + com.alibaba + druid-spring-boot-3-starter + ${druid.version} + + + + org.apache.rocketmq + rocketmq-tools + ${rocketmq.version} + + + + org.apache.rocketmq + rocketmq-client + ${rocketmq.version} + + + org.slf4j + slf4j-api + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + com.google.guava + guava + ${guava.version} + + + + org.apache.commons + commons-lang3 + ${apache.commons.version} + + + + commons-collections + commons-collections + ${apache.common.collections} + + + + org.hibernate.validator + hibernate-validator + 6.2.5.Final + compile + + + + + com.alicp.jetcache + jetcache-anno + ${jetcache.version} + + + + commons-io + commons-io + ${commons.io.version} + + + + com.xuxueli + xxl-job-core + ${xxl.job.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind.version} + compile + + + jackson-core + com.fasterxml.jackson.core + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} + compile + + + jackson-databind + com.fasterxml.jackson.core + + + jackson-core + com.fasterxml.jackson.core + + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + compile + + + jackson-databind + com.fasterxml.jackson.core + + + jackson-core + com.fasterxml.jackson.core + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.module + jackson-module-parameter-names + ${jackson.version} + compile + + + jackson-databind + com.fasterxml.jackson.core + + + jackson-core + com.fasterxml.jackson.core + + + + + + mysql + mysql-connector-java + 8.0.12 + + + + cglib + cglib + 3.1 + + + + + commons-net + commons-net + ${commons-net.version} + + + org.javassist + javassist + 3.24.1-GA + + + com.alibaba + fastjson + ${fastjson.version} + + + + + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + io.github.openfeign + feign-core + ${feign-core.version} + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + com.alibaba.nacos + nacos-client + ${nacos-client.version} + + + + + org.mybatis + mybatis + ${mybatis.version} + + + org.mybatis + mybatis-spring + ${mybatis-spring.version} + + + com.baomidou + dynamic-datasource-spring-boot3-starter + ${dynamic-datasource-starter.version} + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus-boot-starter.version} + + + com.baomidou + mybatis-plus-jsqlparser + ${mybatis-plus-boot-starter.version} + + + tk.mybatis + mapper-spring-boot-starter + ${mapper.starter.version} + + + org.mybatis + mybatis-typehandlers-jsr310 + ${mybatis-typehandlers-jsr310.version} + + + com.oracle.database.jdbc + ojdbc11 + ${oracle.jdbc.version} + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper.version} + + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + false + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + org.apache.maven.plugins + maven-install-plugin + ${maven-install-plugin.version} + + + org.apache.maven.plugins + maven-help-plugin + ${maven-help-plugin.version} + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + true + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + + + + \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/pom.xml b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/pom.xml new file mode 100644 index 0000000..5e70b81 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-jetcache + lingniu-framework-plugin-jetcache + http://maven.apache.org + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + org.redisson + redisson + + + + org.springframework.boot + spring-boot-starter-aop + + + + com.alicp.jetcache + jetcache-anno + + + com.alibaba + fastjson + + + + + + + com.alibaba + fastjson + + + com.google.guava + guava + + + + diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AbstractCacheAutoInit.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AbstractCacheAutoInit.java new file mode 100644 index 0000000..72f1fdc --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AbstractCacheAutoInit.java @@ -0,0 +1,95 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.AbstractCacheBuilder; +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.anno.KeyConvertor; +import com.alicp.jetcache.anno.support.ParserFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.Assert; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Created on 2016/11/29. + * + * @author huangli + */ +public abstract class AbstractCacheAutoInit implements InitializingBean { + + private static Logger logger = LoggerFactory.getLogger(AbstractCacheAutoInit.class); + + @Autowired + protected ConfigurableEnvironment environment; + + @Autowired + protected AutoConfigureBeans autoConfigureBeans; + + protected String[] typeNames; + + private volatile boolean inited = false; + + public AbstractCacheAutoInit(String... cacheTypes) { + Objects.requireNonNull(cacheTypes, "cacheTypes can't be null"); + Assert.isTrue(cacheTypes.length > 0, "cacheTypes length is 0"); + this.typeNames = cacheTypes; + } + + @Override + public void afterPropertiesSet() { + if (!inited) { + synchronized (this) { + if (!inited) { + process("framework.lingniu.jetcache.local.", autoConfigureBeans.getLocalCacheBuilders(), true); + process("framework.lingniu.jetcache.remote.", autoConfigureBeans.getRemoteCacheBuilders(), false); + inited = true; + } + } + } + } + + private void process(String prefix, Map cacheBuilders, boolean local) { + ConfigTree resolver = new ConfigTree(environment, prefix); + Map m = resolver.getProperties(); + Set cacheAreaNames = resolver.directChildrenKeys(); + for (String cacheArea : cacheAreaNames) { + final Object configType = m.get(cacheArea + ".type"); + boolean match = Arrays.stream(typeNames).anyMatch((tn) -> tn.equals(configType)); + if (!match) { + continue; + } + ConfigTree ct = resolver.subTree(cacheArea + "."); + logger.info("init cache area {} , type= {}", cacheArea, typeNames[0]); + CacheBuilder c = initCache(ct, local ? "local." + cacheArea : "remote." + cacheArea); + cacheBuilders.put(cacheArea, c); + } + } + + protected void parseGeneralConfig(CacheBuilder builder, ConfigTree ct) { + AbstractCacheBuilder acb = (AbstractCacheBuilder) builder; + acb.keyConvertor(new ParserFunction(ct.getProperty("keyConvertor", KeyConvertor.FASTJSON2))); + + String expireAfterWriteInMillis = ct.getProperty("expireAfterWriteInMillis"); + if (expireAfterWriteInMillis == null) { + // compatible with 2.1 + expireAfterWriteInMillis = ct.getProperty("defaultExpireInMillis"); + } + if (expireAfterWriteInMillis != null) { + acb.setExpireAfterWriteInMillis(Long.parseLong(expireAfterWriteInMillis)); + } + + String expireAfterAccessInMillis = ct.getProperty("expireAfterAccessInMillis"); + if (expireAfterAccessInMillis != null) { + acb.setExpireAfterAccessInMillis(Long.parseLong(expireAfterAccessInMillis)); + } + + } + + protected abstract CacheBuilder initCache(ConfigTree ct, String cacheAreaWithPrefix); +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AutoConfigureBeans.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AutoConfigureBeans.java new file mode 100644 index 0000000..81e6cf5 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AutoConfigureBeans.java @@ -0,0 +1,45 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Created on 2016/12/28. + * + * @author huangli + */ +public class AutoConfigureBeans { + + private Map localCacheBuilders = new HashMap<>(); + + private Map remoteCacheBuilders = new HashMap<>(); + + private Map customContainer = Collections.synchronizedMap(new HashMap<>()); + + public Map getLocalCacheBuilders() { + return localCacheBuilders; + } + + public void setLocalCacheBuilders(Map localCacheBuilders) { + this.localCacheBuilders = localCacheBuilders; + } + + public Map getRemoteCacheBuilders() { + return remoteCacheBuilders; + } + + public void setRemoteCacheBuilders(Map remoteCacheBuilders) { + this.remoteCacheBuilders = remoteCacheBuilders; + } + + public Map getCustomContainer() { + return customContainer; + } + + public void setCustomContainer(Map customContainer) { + this.customContainer = customContainer; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/BeanDependencyManager.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/BeanDependencyManager.java new file mode 100644 index 0000000..d063613 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/BeanDependencyManager.java @@ -0,0 +1,33 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; + +import java.util.Arrays; + +/** + * Created on 2017/5/5. + * + * @author huangli + */ +public class BeanDependencyManager implements BeanFactoryPostProcessor { + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + String[] autoInitBeanNames = beanFactory.getBeanNamesForType(AbstractCacheAutoInit.class, false, false); + if (autoInitBeanNames != null) { + BeanDefinition bd = beanFactory.getBeanDefinition(JetCacheAutoConfiguration.GLOBAL_CACHE_CONFIG_NAME); + String[] dependsOn = bd.getDependsOn(); + if (dependsOn == null) { + dependsOn = new String[0]; + } + int oldLen = dependsOn.length; + dependsOn = Arrays.copyOf(dependsOn, dependsOn.length + autoInitBeanNames.length); + System.arraycopy(autoInitBeanNames, 0, dependsOn, oldLen, autoInitBeanNames.length); + bd.setDependsOn(dependsOn); + } + } + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/CaffeineAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/CaffeineAutoConfiguration.java new file mode 100644 index 0000000..84497e1 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/CaffeineAutoConfiguration.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.embedded.CaffeineCacheBuilder; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Component; + +/** + * Created on 2016/12/2. + * + * @author huangli + */ +@Component +@Conditional(CaffeineAutoConfiguration.CaffeineCondition.class) +public class CaffeineAutoConfiguration extends EmbeddedCacheAutoInit { + public CaffeineAutoConfiguration() { + super("caffeine"); + } + + @Override + protected CacheBuilder initCache(ConfigTree ct, String cacheAreaWithPrefix) { + CaffeineCacheBuilder builder = CaffeineCacheBuilder.createCaffeineCacheBuilder(); + parseGeneralConfig(builder, ct); + return builder; + } + + public static class CaffeineCondition extends JetCacheCondition { + public CaffeineCondition() { + super("caffeine"); + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ConfigTree.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ConfigTree.java new file mode 100644 index 0000000..e0d6671 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ConfigTree.java @@ -0,0 +1,106 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Created on 2017/11/20. + * + * @author huangli + */ +public class ConfigTree { + private ConfigurableEnvironment environment; + private String prefix; + + public ConfigTree(ConfigurableEnvironment environment, String prefix) { + Assert.notNull(environment, "environment is required"); + Assert.notNull(prefix, "prefix is required"); + this.environment = environment; + this.prefix = prefix; + } + + public ConfigTree subTree(String prefix) { + return new ConfigTree(environment, fullPrefixOrKey(prefix)); + } + + private String fullPrefixOrKey(String prefixOrKey) { + return this.prefix + prefixOrKey; + } + + public Map getProperties() { + Map m = new HashMap<>(); + for (PropertySource source : environment.getPropertySources()) { + if (source instanceof EnumerablePropertySource) { + for (String name : ((EnumerablePropertySource) source) + .getPropertyNames()) { + if (name != null && name.startsWith(prefix)) { + String subKey = name.substring(prefix.length()); + m.put(subKey, environment.getProperty(name)); + } + } + } + } + return m; + } + + public boolean containsProperty(String key) { + key = fullPrefixOrKey(key); + return environment.containsProperty(key); + } + + public String getProperty(String key) { + key = fullPrefixOrKey(key); + return environment.getProperty(key); + } + + public String getProperty(String key, String defaultValue) { + if (containsProperty(key)) { + return getProperty(key); + } else { + return defaultValue; + } + } + + public boolean getProperty(String key, boolean defaultValue) { + if (containsProperty(key)) { + return Boolean.parseBoolean(getProperty(key)); + } else { + return defaultValue; + } + } + + public int getProperty(String key, int defaultValue) { + if (containsProperty(key)) { + return Integer.parseInt(getProperty(key)); + } else { + return defaultValue; + } + } + + public long getProperty(String key, long defaultValue) { + if (containsProperty(key)) { + return Long.parseLong(getProperty(key)); + } else { + return defaultValue; + } + } + + public String getPrefix() { + return prefix; + } + + public Set directChildrenKeys() { + Map m = getProperties(); + return m.keySet().stream().map( + s -> s.indexOf('.') >= 0 ? s.substring(0, s.indexOf('.')) : null) + .filter(s -> s != null) + .collect(Collectors.toSet()); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/EmbeddedCacheAutoInit.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/EmbeddedCacheAutoInit.java new file mode 100644 index 0000000..cd62f57 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/EmbeddedCacheAutoInit.java @@ -0,0 +1,25 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.anno.CacheConsts; +import com.alicp.jetcache.embedded.EmbeddedCacheBuilder; + +/** + * Created on 2016/12/2. + * + * @author huangli + */ +public abstract class EmbeddedCacheAutoInit extends AbstractCacheAutoInit { + + public EmbeddedCacheAutoInit(String... cacheTypes) { + super(cacheTypes); + } + + @Override + protected void parseGeneralConfig(CacheBuilder builder, ConfigTree ct) { + super.parseGeneralConfig(builder, ct); + EmbeddedCacheBuilder ecb = (EmbeddedCacheBuilder) builder; + + ecb.limit(Integer.parseInt(ct.getProperty("limit", String.valueOf(CacheConsts.DEFAULT_LOCAL_LIMIT)))); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ExternalCacheAutoInit.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ExternalCacheAutoInit.java new file mode 100644 index 0000000..302937c --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ExternalCacheAutoInit.java @@ -0,0 +1,36 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.anno.CacheConsts; +import com.alicp.jetcache.anno.support.ParserFunction; +import com.alicp.jetcache.external.ExternalCacheBuilder; + +/** + * Created on 2016/11/29. + * + * @author huangli + */ +public abstract class ExternalCacheAutoInit extends AbstractCacheAutoInit { + public ExternalCacheAutoInit(String... cacheTypes) { + super(cacheTypes); + } + + @Override + protected void parseGeneralConfig(CacheBuilder builder, ConfigTree ct) { + super.parseGeneralConfig(builder, ct); + ExternalCacheBuilder ecb = (ExternalCacheBuilder) builder; + ecb.setKeyPrefix(ct.getProperty("keyPrefix")); + ecb.setBroadcastChannel(parseBroadcastChannel(ct)); + ecb.setValueEncoder(new ParserFunction(ct.getProperty("valueEncoder", CacheConsts.DEFAULT_SERIAL_POLICY))); + ecb.setValueDecoder(new ParserFunction(ct.getProperty("valueDecoder", CacheConsts.DEFAULT_SERIAL_POLICY))); + } + + protected String parseBroadcastChannel(ConfigTree ct) { + String broadcastChannel = ct.getProperty("broadcastChannel"); + if (broadcastChannel != null && !"".equals(broadcastChannel.trim())) { + return broadcastChannel.trim(); + } else { + return null; + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheAutoConfiguration.java new file mode 100644 index 0000000..88120a9 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheAutoConfiguration.java @@ -0,0 +1,83 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheManager; +import com.alicp.jetcache.SimpleCacheManager; +import com.alicp.jetcache.anno.support.EncoderParser; +import com.alicp.jetcache.anno.support.GlobalCacheConfig; +import com.alicp.jetcache.anno.support.JetCacheBaseBeans; +import com.alicp.jetcache.anno.support.KeyConvertorParser; +import com.alicp.jetcache.anno.support.SpringConfigProvider; +import com.alicp.jetcache.support.StatInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import java.util.function.Consumer; + +/** + * Created on 2016/11/17. + * + * @author huangli + */ +@Configuration +@ConditionalOnClass(GlobalCacheConfig.class) +@ConditionalOnMissingBean(GlobalCacheConfig.class) +@EnableConfigurationProperties(JetCacheProperties.class) +@Import({CaffeineAutoConfiguration.class, + MockRemoteCacheAutoConfiguration.class, + LinkedHashMapAutoConfiguration.class, + RedissonAutoConfiguration.class}) +public class JetCacheAutoConfiguration { + + public static final String GLOBAL_CACHE_CONFIG_NAME = "globalCacheConfig"; + + @Bean + @ConditionalOnMissingBean + public SpringConfigProvider springConfigProvider( + @Autowired ApplicationContext applicationContext, + @Autowired GlobalCacheConfig globalCacheConfig, + @Autowired(required = false) EncoderParser encoderParser, + @Autowired(required = false) KeyConvertorParser keyConvertorParser, + @Autowired(required = false) Consumer metricsCallback) { + return new JetCacheBaseBeans().springConfigProvider(applicationContext, globalCacheConfig, + encoderParser, keyConvertorParser, metricsCallback); + } + + @Bean(name = "jcCacheManager") + @ConditionalOnMissingBean + public CacheManager cacheManager(@Autowired SpringConfigProvider springConfigProvider) { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCacheBuilderTemplate(springConfigProvider.getCacheBuilderTemplate()); + return cacheManager; + } + + @Bean + public AutoConfigureBeans autoConfigureBeans() { + return new AutoConfigureBeans(); + } + + @Bean + public static BeanDependencyManager beanDependencyManager() { + return new BeanDependencyManager(); + } + + @Bean(name = GLOBAL_CACHE_CONFIG_NAME) + public GlobalCacheConfig globalCacheConfig(AutoConfigureBeans autoConfigureBeans, JetCacheProperties props) { + GlobalCacheConfig _globalCacheConfig = new GlobalCacheConfig(); + _globalCacheConfig = new GlobalCacheConfig(); + _globalCacheConfig.setHiddenPackages(props.getHiddenPackages()); + _globalCacheConfig.setStatIntervalMinutes(props.getStatIntervalMinutes()); + _globalCacheConfig.setAreaInCacheName(props.isAreaInCacheName()); + _globalCacheConfig.setPenetrationProtect(props.isPenetrationProtect()); + _globalCacheConfig.setEnableMethodCache(props.isEnableMethodCache()); + _globalCacheConfig.setLocalCacheBuilders(autoConfigureBeans.getLocalCacheBuilders()); + _globalCacheConfig.setRemoteCacheBuilders(autoConfigureBeans.getRemoteCacheBuilders()); + return _globalCacheConfig; + } + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheCondition.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheCondition.java new file mode 100644 index 0000000..c154356 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheCondition.java @@ -0,0 +1,48 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.Assert; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Created on 2016/11/28. + * + * @author huangli + */ +public abstract class JetCacheCondition extends SpringBootCondition { + + private String[] cacheTypes; + + protected JetCacheCondition(String... cacheTypes) { + Objects.requireNonNull(cacheTypes, "cacheTypes can't be null"); + Assert.isTrue(cacheTypes.length > 0, "cacheTypes length is 0"); + this.cacheTypes = cacheTypes; + } + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { + ConfigTree ct = new ConfigTree((ConfigurableEnvironment) conditionContext.getEnvironment(), "framework.lingniu.jetcache."); + if (match(ct, "local.") || match(ct, "remote.")) { + return ConditionOutcome.match(); + } else { + return ConditionOutcome.noMatch("no match for " + cacheTypes[0]); + } + } + + private boolean match(ConfigTree ct, String prefix) { + Map m = ct.subTree(prefix).getProperties(); + Set cacheAreaNames = m.keySet().stream().map((s) -> s.substring(0, s.indexOf('.'))).collect(Collectors.toSet()); + final List cacheTypesList = Arrays.asList(cacheTypes); + return cacheAreaNames.stream().anyMatch((s) -> cacheTypesList.contains(m.get(s + ".type"))); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheProperties.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheProperties.java new file mode 100644 index 0000000..0a70c20 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheProperties.java @@ -0,0 +1,68 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Created on 2016/11/23. + * + * @author huangli + */ +@ConfigurationProperties(prefix = "jetcache") +public class JetCacheProperties { + + private String[] hiddenPackages; + private int statIntervalMinutes; + private boolean areaInCacheName = true; + private boolean penetrationProtect = false; + private boolean enableMethodCache = true; + + public JetCacheProperties() { + } + + public String[] getHiddenPackages() { + // keep same with GlobalCacheConfig + return hiddenPackages; + } + + public void setHiddenPackages(String[] hiddenPackages) { + // keep same with GlobalCacheConfig + this.hiddenPackages = hiddenPackages; + } + + public void setHidePackages(String[] hidePackages) { + // keep same with GlobalCacheConfig + this.hiddenPackages = hidePackages; + } + + public int getStatIntervalMinutes() { + return statIntervalMinutes; + } + + public void setStatIntervalMinutes(int statIntervalMinutes) { + this.statIntervalMinutes = statIntervalMinutes; + } + + public boolean isAreaInCacheName() { + return areaInCacheName; + } + + public void setAreaInCacheName(boolean areaInCacheName) { + this.areaInCacheName = areaInCacheName; + } + + public boolean isPenetrationProtect() { + return penetrationProtect; + } + + public void setPenetrationProtect(boolean penetrationProtect) { + this.penetrationProtect = penetrationProtect; + } + + public boolean isEnableMethodCache() { + return enableMethodCache; + } + + public void setEnableMethodCache(boolean enableMethodCache) { + this.enableMethodCache = enableMethodCache; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/LinkedHashMapAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/LinkedHashMapAutoConfiguration.java new file mode 100644 index 0000000..173eb26 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/LinkedHashMapAutoConfiguration.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.embedded.LinkedHashMapCacheBuilder; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Component; + +/** + * Created on 2016/12/2. + * + * @author huangli + */ +@Component +@Conditional(LinkedHashMapAutoConfiguration.LinkedHashMapCondition.class) +public class LinkedHashMapAutoConfiguration extends EmbeddedCacheAutoInit { + public LinkedHashMapAutoConfiguration() { + super("linkedhashmap"); + } + + @Override + protected CacheBuilder initCache(ConfigTree ct, String cacheAreaWithPrefix) { + LinkedHashMapCacheBuilder builder = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder(); + parseGeneralConfig(builder, ct); + return builder; + } + + public static class LinkedHashMapCondition extends JetCacheCondition { + public LinkedHashMapCondition() { + super("linkedhashmap"); + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/MockRemoteCacheAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/MockRemoteCacheAutoConfiguration.java new file mode 100644 index 0000000..431ccaa --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/MockRemoteCacheAutoConfiguration.java @@ -0,0 +1,40 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.anno.CacheConsts; +import com.alicp.jetcache.external.MockRemoteCacheBuilder; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Component; + +/** + * Created on 2016/12/2. + * + * @author huangli + */ +@Component +@Conditional(MockRemoteCacheAutoConfiguration.MockRemoteCacheCondition.class) +public class MockRemoteCacheAutoConfiguration extends ExternalCacheAutoInit { + public MockRemoteCacheAutoConfiguration() { + super("mock"); + } + + @Override + protected CacheBuilder initCache(ConfigTree ct, String cacheAreaWithPrefix) { + MockRemoteCacheBuilder builder = MockRemoteCacheBuilder.createMockRemoteCacheBuilder(); + parseGeneralConfig(builder, ct); + return builder; + } + + @Override + protected void parseGeneralConfig(CacheBuilder builder, ConfigTree ct) { + super.parseGeneralConfig(builder, ct); + MockRemoteCacheBuilder b = (MockRemoteCacheBuilder) builder; + b.limit(Integer.parseInt(ct.getProperty("limit", String.valueOf(CacheConsts.DEFAULT_LOCAL_LIMIT)))); + } + + public static class MockRemoteCacheCondition extends JetCacheCondition { + public MockRemoteCacheCondition() { + super("mock"); + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/RedissonAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/RedissonAutoConfiguration.java new file mode 100644 index 0000000..6bf4eab --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/RedissonAutoConfiguration.java @@ -0,0 +1,73 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import cn.lingniu.framework.plugin.jetcache.redisson.RedissonCacheBuilder; +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.CacheConfigException; +import com.alicp.jetcache.external.ExternalCacheBuilder; +import org.redisson.api.RedissonClient; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; +import java.util.Objects; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +@Configuration +@Conditional(RedissonAutoConfiguration.RedissonCondition.class) +public class RedissonAutoConfiguration { + private static final String CACHE_TYPE = "redisson"; + + public static class RedissonCondition extends JetCacheCondition { + public RedissonCondition() { + super(CACHE_TYPE); + } + } + + @Bean + public RedissonAutoInit redissonAutoInit() { + return new RedissonAutoInit(); + } + + public static class RedissonAutoInit extends ExternalCacheAutoInit implements ApplicationContextAware { + private ApplicationContext context; + + public RedissonAutoInit() { + super(CACHE_TYPE); + } + + @Override + protected CacheBuilder initCache(final ConfigTree ct, final String cacheAreaWithPrefix) { + final Map beans = this.context.getBeansOfType(RedissonClient.class); + if (beans.isEmpty()) { + throw new CacheConfigException("no RedissonClient in spring context"); + } + RedissonClient client = beans.values().iterator().next(); + if (beans.size() > 1) { + final String redissonClientName = ct.getProperty("redissonClient"); + if (Objects.isNull(redissonClientName) || redissonClientName.isEmpty()) { + throw new CacheConfigException("redissonClient is required, because there is multiple RedissonClient in Spring context"); + } + if (!beans.containsKey(redissonClientName)) { + throw new CacheConfigException("there is no RedissonClient named " + redissonClientName + " in Spring context"); + } + client = beans.get(redissonClientName); + } + final ExternalCacheBuilder builder = RedissonCacheBuilder.createBuilder().redissonClient(client); + parseGeneralConfig(builder, ct); + return builder; + } + + @Override + public void setApplicationContext(final ApplicationContext context) throws BeansException { + this.context = context; + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonBroadcastManager.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonBroadcastManager.java new file mode 100644 index 0000000..75c3456 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonBroadcastManager.java @@ -0,0 +1,70 @@ +package cn.lingniu.framework.plugin.jetcache.redisson; + +import com.alicp.jetcache.CacheManager; +import com.alicp.jetcache.CacheResult; +import com.alicp.jetcache.support.BroadcastManager; +import com.alicp.jetcache.support.CacheMessage; +import com.alicp.jetcache.support.SquashedLogger; +import org.redisson.api.RedissonClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +public class RedissonBroadcastManager extends BroadcastManager { + private static final Logger logger = LoggerFactory.getLogger(RedissonBroadcastManager.class); + private final RedissonCacheConfig config; + private final String channel; + private final RedissonClient client; + private volatile int subscribeId; + + public RedissonBroadcastManager(final CacheManager cacheManager, final RedissonCacheConfig config) { + super(cacheManager); + checkConfig(config); + this.config = config; + this.channel = config.getBroadcastChannel(); + this.client = config.getRedissonClient(); + } + + @Override + public synchronized void startSubscribe() { + if (this.subscribeId == 0 && Objects.nonNull(this.channel) && !this.channel.isEmpty()) { + this.subscribeId = this.client.getTopic(this.channel) + .addListener(byte[].class, (channel, msg) -> processNotification(msg, this.config.getValueDecoder())); + } + } + + + @Override + public synchronized void close() { + final int id; + if ((id = this.subscribeId) > 0 && Objects.nonNull(this.channel)) { + this.subscribeId = 0; + try { + this.client.getTopic(this.channel).removeListener(id); + } catch (Throwable e) { + logger.warn("unsubscribe {} fail", this.channel, e); + } + } + } + + @Override + public CacheResult publish(final CacheMessage cacheMessage) { + try { + if (Objects.nonNull(this.channel) && Objects.nonNull(cacheMessage)) { + final byte[] msg = this.config.getValueEncoder().apply(cacheMessage); + this.client.getTopic(this.channel).publish(msg); + return CacheResult.SUCCESS_WITHOUT_MSG; + } + return CacheResult.FAIL_WITHOUT_MSG; + } catch (Throwable e) { + SquashedLogger.getLogger(logger).error("jetcache publish error", e); + return new CacheResult(e); + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCache.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCache.java new file mode 100644 index 0000000..d1426cb --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCache.java @@ -0,0 +1,180 @@ +package cn.lingniu.framework.plugin.jetcache.redisson; + +import com.alicp.jetcache.CacheConfig; +import com.alicp.jetcache.CacheGetResult; +import com.alicp.jetcache.CacheResult; +import com.alicp.jetcache.CacheResultCode; +import com.alicp.jetcache.CacheValueHolder; +import com.alicp.jetcache.MultiGetResult; +import com.alicp.jetcache.external.AbstractExternalCache; +import org.redisson.api.RBatch; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +public class RedissonCache extends AbstractExternalCache { + private final RedissonClient client; + private final RedissonCacheConfig config; + + public RedissonCache(final RedissonCacheConfig config) { + super(config); + this.config = config; + this.client = config.getRedissonClient(); + } + + protected String getCacheKey(final K key) { + final byte[] newKey = buildKey(key); + return new String(newKey, StandardCharsets.UTF_8); + } + + @Override + public CacheConfig config() { + return this.config; + } + + @Override + public T unwrap(final Class clazz) { + throw new UnsupportedOperationException("RedissonCache does not support unwrap"); + } + + @Override + @SuppressWarnings({"unchecked"}) + protected CacheGetResult do_GET(final K key) { + try { + final RBucket> rb = this.client.getBucket(getCacheKey(key)); + final CacheValueHolder holder = rb.get(); + if (Objects.nonNull(holder)) { + final long now = System.currentTimeMillis(), expire = holder.getExpireTime(); + if (expire > 0 && now >= expire) { + return CacheGetResult.EXPIRED_WITHOUT_MSG; + } + return new CacheGetResult<>(CacheResultCode.SUCCESS, null, holder); + } + return CacheGetResult.NOT_EXISTS_WITHOUT_MSG; + } catch (Throwable e) { + logError("GET", key, e); + return new CacheGetResult<>(e); + } + } + + @Override + @SuppressWarnings({"unchecked"}) + protected MultiGetResult do_GET_ALL(final Set keys) { + try { + final Map> retMap = new HashMap<>(1 << 4); + if (Objects.nonNull(keys) && !keys.isEmpty()) { + final Map keyMap = new HashMap<>(keys.size()); + for (K k : keys) { + if (Objects.nonNull(k)) { + final String key = getCacheKey(k); + if (Objects.nonNull(key)) { + keyMap.put(k, key); + } + } + } + if (!keyMap.isEmpty()) { + final Map kvMap = this.client.getBuckets().get(keyMap.values().toArray(new String[0])); + final long now = System.currentTimeMillis(); + for (K k : keys) { + final String key = keyMap.get(k); + if (Objects.nonNull(key) && Objects.nonNull(kvMap)) { + final CacheValueHolder holder = (CacheValueHolder) kvMap.get(key); + if (Objects.nonNull(holder)) { + final long expire = holder.getExpireTime(); + final CacheGetResult ret = (expire > 0 && now >= expire) ? CacheGetResult.EXPIRED_WITHOUT_MSG : + new CacheGetResult<>(CacheResultCode.SUCCESS, null, holder); + retMap.put(k, ret); + continue; + } + } + retMap.put(k, CacheGetResult.NOT_EXISTS_WITHOUT_MSG); + } + } + } + return new MultiGetResult<>(CacheResultCode.SUCCESS, null, retMap); + } catch (Throwable e) { + logError("GET_ALL", "keys(" + (Objects.nonNull(keys) ? keys.size() : 0) + ")", e); + return new MultiGetResult<>(e); + } + } + + @Override + protected CacheResult do_PUT(final K key, final V value, final long expireAfterWrite, final TimeUnit timeUnit) { + try { + final CacheValueHolder holder = new CacheValueHolder<>(value, timeUnit.toMillis(expireAfterWrite)); + this.client.getBucket(getCacheKey(key)).set(holder, expireAfterWrite, timeUnit); + return CacheGetResult.SUCCESS_WITHOUT_MSG; + } catch (Throwable e) { + logError("PUT", key, e); + return new CacheResult(e); + } + } + + @Override + protected CacheResult do_PUT_ALL(final Map map, final long expireAfterWrite, final TimeUnit timeUnit) { + try { + if (Objects.nonNull(map) && !map.isEmpty()) { + final long expire = timeUnit.toMillis(expireAfterWrite); + final RBatch batch = this.client.createBatch(); + map.forEach((k, v) -> { + final CacheValueHolder holder = new CacheValueHolder<>(v, expire); + batch.getBucket(getCacheKey(k)).setAsync(holder, expireAfterWrite, timeUnit); + }); + batch.execute(); + } + return CacheResult.SUCCESS_WITHOUT_MSG; + } catch (Throwable e) { + logError("PUT_ALL", "map(" + map.size() + ")", e); + return new CacheResult(e); + } + } + + @Override + protected CacheResult do_REMOVE(final K key) { + try { + final boolean ret = this.client.getBucket(getCacheKey(key)).delete(); + return ret ? CacheResult.SUCCESS_WITHOUT_MSG : CacheResult.FAIL_WITHOUT_MSG; + } catch (Throwable e) { + logError("REMOVE", key, e); + return new CacheResult(e); + } + } + + @Override + protected CacheResult do_REMOVE_ALL(final Set keys) { + try { + if (Objects.nonNull(keys) && !keys.isEmpty()) { + final RBatch batch = this.client.createBatch(); + keys.forEach(key -> batch.getBucket(getCacheKey(key)).deleteAsync()); + batch.execute(); + } + return CacheResult.SUCCESS_WITHOUT_MSG; + } catch (Throwable e) { + logError("REMOVE_ALL", "keys(" + keys.size() + ")", e); + return new CacheResult(e); + } + } + + @Override + protected CacheResult do_PUT_IF_ABSENT(final K key, final V value, final long expireAfterWrite, final TimeUnit timeUnit) { + try { + final CacheValueHolder holder = new CacheValueHolder<>(value, timeUnit.toMillis(expireAfterWrite)); + final boolean success = this.client.getBucket(getCacheKey(key)).trySet(holder, expireAfterWrite, timeUnit); + return success ? CacheResult.SUCCESS_WITHOUT_MSG : CacheResult.EXISTS_WITHOUT_MSG; + } catch (Throwable e) { + logError("PUT_IF_ABSENT", key, e); + return new CacheResult(e); + } + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheBuilder.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheBuilder.java new file mode 100644 index 0000000..338408f --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheBuilder.java @@ -0,0 +1,52 @@ +package cn.lingniu.framework.plugin.jetcache.redisson; + +import com.alicp.jetcache.CacheManager; +import com.alicp.jetcache.external.ExternalCacheBuilder; +import com.alicp.jetcache.support.BroadcastManager; +import org.redisson.api.RedissonClient; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +public class RedissonCacheBuilder> extends ExternalCacheBuilder { + + public static class RedissonDataCacheBuilderImpl extends RedissonCacheBuilder { + + } + + public static RedissonDataCacheBuilderImpl createBuilder() { + return new RedissonDataCacheBuilderImpl(); + } + + @SuppressWarnings({"all"}) + protected RedissonCacheBuilder() { + buildFunc(config -> new RedissonCache((RedissonCacheConfig) config)); + } + + @Override + @SuppressWarnings({"all"}) + public RedissonCacheConfig getConfig() { + if (this.config == null) { + this.config = new RedissonCacheConfig(); + } + return (RedissonCacheConfig) this.config; + } + + public T redissonClient(final RedissonClient client) { + this.getConfig().setRedissonClient(client); + return self(); + } + + @Override + public boolean supportBroadcast() { + return true; + } + + @Override + public BroadcastManager createBroadcastManager(final CacheManager cacheManager) { + final RedissonCacheConfig c = (RedissonCacheConfig) this.getConfig().clone(); + return new RedissonBroadcastManager(cacheManager, c); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheConfig.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheConfig.java new file mode 100644 index 0000000..a6e4809 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheConfig.java @@ -0,0 +1,21 @@ +package cn.lingniu.framework.plugin.jetcache.redisson; + +import com.alicp.jetcache.external.ExternalCacheConfig; +import org.redisson.api.RedissonClient; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +public class RedissonCacheConfig extends ExternalCacheConfig { + private RedissonClient redissonClient; + + public RedissonClient getRedissonClient() { + return redissonClient; + } + + public void setRedissonClient(final RedissonClient redissonClient) { + this.redissonClient = redissonClient; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..b98cf20 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.jetcache.autoconfigure.JetCacheAutoConfiguration diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/jetcache-config.md b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/jetcache-config.md new file mode 100644 index 0000000..3513f6a --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/jetcache-config.md @@ -0,0 +1,122 @@ +# 【重要】jetcache详细资料---参考官网 + +## 概述 (Overview) + +1. 基于 JetCache 封装的企业级二级缓存解决方案,提供本地 JVM 本地缓存和远程 Redis 缓存的组合缓存能力 +2. 核心能力 + * 二级缓存机制: + + - 一级缓存:本地 JVM 缓存(基于 Caffeine等) + - 二级缓存:远程 Redis 缓存(redisson 模式) + +* 多样化缓存配置: + - 本地缓存:支持 Caffeine 类型,可配置缓存数量限制和过期时间 + - 远程缓存:支持 redisson 类型 + - 多实例支持:可配置多个缓存分组(如 default、employee 等) +* 注解式缓存操作: + - @Cached:标记方法结果需要缓存,支持缓存名称、过期时间等配置 + - @CacheRefresh:支持缓存自动刷新机制 + - @EnableMethodCache:启用 JetCache Bean 扫描 +* 灵活的缓存策略: + - 支持不同的 Key 生成策略 + - 可配置缓存过期时间和时间单位 + - 支持不同的缓存类型(本地、远程、双向) + +3. 适用场景:该组件特别适用于需要高性能缓存访问的企业级应用,通过本地+远程的二级缓存架构,在保证缓存访问速度的同时,确保缓存数据的共享和一致性。通过注解方式简化了缓存的使用。 + * 高并发读场景:需要减少数据库访问压力,提升系统响应速度的业务场景 + * 高可用访问:需要自动感知 Redis 集群变化以及故障自动切换能力的应用场景 + * 复杂查询结果缓存:对于计算复杂或关联查询较多的数据结果进行缓存 + * 数据一致性要求较高:需要通过二级缓存机制保证数据访问性能和一致性的场景 + * 需要缓存自动刷新:对于有一定时效性要求但不需要实时更新的数据 + +## 如何配置--更多参数参考:更多配置请参考官网jetcache + +* 此处为了统一中间件配置前缀framework.lingniu.jetcache 和更好的个性化扩展, 其他jetcache所有配置方式未改变 +* 请先依赖:lingniu-framework-plugin-redisson + redissonClient 就是 RedissonConfig配置的key名称 + +```yaml +framework: + lingniu: + jetcache: + # 需要隐藏的包路径(数组),默认为空 + hiddenPackages: [ ] + # 统计信息输出间隔(分钟),默认为 0(不输出) + statIntervalMinutes: 10 + # 是否在缓存名称中包含区域(area)信息,默认为 true + areaInCacheName: true + # 是否开启缓存穿透保护,默认为 false + penetrationProtect: false + # 本地缓存配置 + local: + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + ca2: + # 本地缓存实现类类型,支持:linkedhashmap/caffeine + type: linkedhashmap + # 缓存key数量限制, 默认100 + limit: 100 + default: + # 本地缓存实现类类型,支持:linkedhashmap/caffeine + type: caffeine + # 缓存key数量限制, 默认100 + limit: 100 + # 过期时间,0为不失效 + expireAfterAccessInMillis: 0 + # key 转换器,用于序列化,目前支持:NONE、FASTJSON、JACKSON、FASTJSON2 + keyConvertor: FASTJSON2 + remote: + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + ca2: + # 远程缓存实现类类型,目前支持:redisson + type: redisson + # 远程缓存客户端名称,默认r1 -- todo 需要在application-redisson.yml中配置 + redissonClient: r1 + # key 前缀 + keyPrefix: "cacheKeyPrefix:" + # 连接超时时间ms + timeout: 2000 + # 重试次数 + maxAttempt: 5 + # 连接池配置 + poolConfig: + maxTotal: 8 + maxIdle: 8 + minIdle: 0 + # key 转换器,用于序列化,目前支持:NONE、FASTJSON、JACKSON、FASTJSON2 + keyConvertor: FASTJSON2 + # JAVA, KRYO,KRYO5,自定义spring beanName + valueEncoder: JAVA + # JAVA, KRYO,KRYO5,自定义spring beanName + valueDecoder: JAVA + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + default: + # 远程缓存实现类类型,目前支持:redisson + type: redisson + # 远程缓存客户端名称,默认r1 -- todo 需要在application-redisson.yml中配置 + redissonClient: r1 + +``` + +## 如何使用- + +```java +//启动 扫描类添加注解 +@EnableMethodCache(basePackages = "cn.lingniu.*") +@SpringBootApplication +public class CacheApplication { + public static void main(String[] args) { + SpringApplication.run(CacheApplication.class, args); + } +} +// 多种使用方式参考 +// 1. 注解方式 +@CachePenetrationProtect +@Cached(name = "orderCache:", key = "#orderId", expire = 3600, cacheType = CacheType.BOTH) +@CacheRefresh(refresh = 10, timeUnit = TimeUnit.MINUTES) +public DataResult getOrderById(String orderId) { + ... +} +// 2. SimpleCacheManager方式 +Cache localCache = simpleCacheManager.getOrCreateCache(QuickConfig.newBuilder(localArea, "user:").cacheType(CacheType.LOCAL).build()); +``` + diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/pom.xml b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/pom.xml new file mode 100644 index 0000000..ec1f026 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-redisson + lingniu-framework-plugin-redisson + http://maven.apache.org + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + org.redisson + redisson + + + org.springframework.boot + spring-boot-starter-aop + + + com.alibaba + fastjson + + + diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClientFactory.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClientFactory.java new file mode 100644 index 0000000..2bf65d4 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClientFactory.java @@ -0,0 +1,28 @@ +package cn.lingniu.framework.plugin.redisson; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import org.redisson.api.RedissonClient; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class RedissonClientFactory { + + private static volatile Map redisMap = new ConcurrentHashMap<>(); + + public static void putRedissonClient(String beanName, RedissonClient redissonClient) { + redisMap.put(beanName, redissonClient); + } + + public static RedissonClient getRedissonClient(String beanName) { + if (ObjectEmptyUtils.isEmpty(beanName) || !redisMap.containsKey(beanName)) { + throw new IllegalStateException("(redisson)bean-对应的RedissonClient未注册:" + beanName); + } + return redisMap.get(beanName); + } + + public static Map getRedisMap() { + return redisMap; + } + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClusterLockerService.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClusterLockerService.java new file mode 100644 index 0000000..27ea85b --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClusterLockerService.java @@ -0,0 +1,30 @@ +package cn.lingniu.framework.plugin.redisson; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * 锁 + **/ +@SuppressWarnings("all") +public interface RedissonClusterLockerService { + + default public R wrapLock(T source, Function fun, + Function keyGetter, Function wrapFun) { + return wrapLock(source, fun, keyGetter, wrapFun, 15L, TimeUnit.SECONDS); + } + + R wrapLock(T source, Function fun, + Function keyGetter, Function wrapFun, + Long leaseTime, TimeUnit leaseTimeUnit); + + default public R wrapLock(T source, Function keyGetter, Function wrapFun) { + return wrapLock(source, (t) -> { + String key = keyGetter.apply(source); + return delivery(key); + }, keyGetter, wrapFun); + } + + String delivery(String key); + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockAction.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockAction.java new file mode 100644 index 0000000..7f96f9c --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockAction.java @@ -0,0 +1,51 @@ +package cn.lingniu.framework.plugin.redisson; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * 支持注解获取锁 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface RedissonLockAction { + + /** + * RedissonConfig 中 remote 的 key 名字 + */ + String redissonName(); + /** + * 锁的资源,key。支持spring El表达式 + */ + @AliasFor("key") + String value() default "'default'"; + @AliasFor("value") + String key() default "'default'"; + /** + * 锁类型 + */ + RedissonLockType lockType() default RedissonLockType.REENTRANT_LOCK; + /** + * 获取锁等待时间,默认3秒 + */ + long waitTime() default 3000L; + /** + * 锁自动释放时间,默认10秒 + */ + long leaseTime() default 10000L; + /** + * 时间单位 + */ + TimeUnit unit() default TimeUnit.MILLISECONDS; + /** + * 锁失败是否Throw异常 + */ + boolean throwException() default false; +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockType.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockType.java new file mode 100644 index 0000000..5e72cbd --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockType.java @@ -0,0 +1,23 @@ +package cn.lingniu.framework.plugin.redisson; + +/** + * 锁类型 + */ +public enum RedissonLockType { + /** + * 读锁 + */ + READ_LOCK, + /** + * 写锁 + */ + WRITE_LOCK, + /** + * 可重入锁 + */ + REENTRANT_LOCK, + /** + * 公平锁 + */ + FAIR_LOCK; +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonAbstractClientBuilder.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonAbstractClientBuilder.java new file mode 100644 index 0000000..367f95e --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonAbstractClientBuilder.java @@ -0,0 +1,58 @@ +package cn.lingniu.framework.plugin.redisson.builder; + +import cn.lingniu.framework.plugin.redisson.config.RedisHostAndPort; +import cn.lingniu.framework.plugin.redisson.config.RedissonProperties; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.redisson.config.Config; + +import java.util.HashSet; +import java.util.Set; + + +@Slf4j +public abstract class RedissonAbstractClientBuilder { + + @Getter(AccessLevel.PUBLIC) + private RedissonProperties redissonProperties; + + protected RedissonAbstractClientBuilder(RedissonProperties redissonProperties) { + this.redissonProperties = redissonProperties; + } + + protected abstract void config(Config redssionConfig, Set redisHostAndPorts); + + public Config build() { + Config redissionConfig = new Config(); + Set redisHostAndPorts = parseRedisHostAndPort(redissonProperties.getRedisAddresses()); + config(redissionConfig, redisHostAndPorts); + return redissionConfig; + } + + public Set parseRedisHostAndPort(String redisAddress) { + Set redisHostAndPorts = new HashSet<>(); + // 分离地址部分和密码部分 + String[] parts = redisAddress.split(";"); + String addressPart = parts[0]; + String password = parts.length > 1 ? parts[1] : null; + // 解析host:port格式的地址 + String[] hosts = addressPart.split(","); + for (String hostPort : hosts) { + hostPort = hostPort.trim(); + if (hostPort.contains(":")) { + int colonIndex = hostPort.indexOf(":"); + String host = hostPort.substring(0, colonIndex); + int port = Integer.parseInt(hostPort.substring(colonIndex + 1)); + RedisHostAndPort redisHostAndPort = new RedisHostAndPort(); + redisHostAndPort.setHost(host); + redisHostAndPort.setPort(port); + if (password != null) { + redisHostAndPort.setPkey(password); + } + redisHostAndPorts.add(redisHostAndPort); + } + } + return redisHostAndPorts; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonClusterClientBuilder.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonClusterClientBuilder.java new file mode 100644 index 0000000..4f2b169 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonClusterClientBuilder.java @@ -0,0 +1,47 @@ +package cn.lingniu.framework.plugin.redisson.builder; + + +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.redisson.config.RedisHostAndPort; +import cn.lingniu.framework.plugin.redisson.config.RedissonProperties; +import cn.lingniu.framework.plugin.util.ip.IpUtil; +import org.apache.commons.lang3.StringUtils; +import org.redisson.config.ClusterServersConfig; +import org.redisson.config.Config; +import org.redisson.config.ReadMode; + +import java.util.Set; + +/** + * @description: 集群类型 + **/ +public class RedissonClusterClientBuilder extends RedissonAbstractClientBuilder { + + public RedissonClusterClientBuilder(RedissonProperties properties) { + super(properties); + } + + @Override + protected void config(Config redssionConfig, Set redisHostAndPorts) { + ClusterServersConfig clusterServersConfig = redssionConfig.useClusterServers(); + if (StringUtils.isEmpty(getRedissonProperties().getClientName())) { + clusterServersConfig.setClientName(ApplicationNameContext.getApplicationName() + ":" + IpUtil.getIp()); + } + clusterServersConfig.setMasterConnectionMinimumIdleSize(getRedissonProperties().getConnectionMinimumIdleSize()); + clusterServersConfig.setSlaveConnectionMinimumIdleSize(getRedissonProperties().getConnectionMinimumIdleSize()); + clusterServersConfig.setMasterConnectionPoolSize(getRedissonProperties().getConnectionPoolSize()); + clusterServersConfig.setSlaveConnectionPoolSize(getRedissonProperties().getConnectionPoolSize()); + clusterServersConfig.setIdleConnectionTimeout(getRedissonProperties().getIdleConnectionTimeout()); + clusterServersConfig.setConnectTimeout(getRedissonProperties().getConnectTimeout()); + clusterServersConfig.setTimeout(getRedissonProperties().getTimeout()); + clusterServersConfig.setRetryAttempts(getRedissonProperties().getRetryAttempts()); + RedisHostAndPort redisHostAndPort = redisHostAndPorts.stream().findAny().get(); + clusterServersConfig.setPassword(redisHostAndPort.getPkey()); + clusterServersConfig.setCheckSlotsCoverage(getRedissonProperties().getCheckSlotsCoverage()); + clusterServersConfig.setReadMode(ReadMode.valueOf(getRedissonProperties().getReadMode())); + redisHostAndPorts.stream().forEach(h -> { + clusterServersConfig.addNodeAddress(String.format("redis://%s/", h.getHost() + ":" + h.getPort())); + }); + + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonStandaloneClientBuilder.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonStandaloneClientBuilder.java new file mode 100644 index 0000000..15d122f --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonStandaloneClientBuilder.java @@ -0,0 +1,41 @@ +package cn.lingniu.framework.plugin.redisson.builder; + +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.redisson.config.RedisHostAndPort; +import cn.lingniu.framework.plugin.redisson.config.RedissonProperties; +import cn.lingniu.framework.plugin.util.ip.IpUtil; +import org.apache.commons.lang3.StringUtils; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; + +import java.util.Set; + + +/** + * @description: 单节点类型 + **/ +public class RedissonStandaloneClientBuilder extends RedissonAbstractClientBuilder { + + + public RedissonStandaloneClientBuilder(RedissonProperties properties) { + super(properties); + } + + @Override + protected void config(Config redssionConfig, Set redisHostAndPorts) { + SingleServerConfig singleServerConfig = redssionConfig.useSingleServer(); + if (StringUtils.isEmpty(getRedissonProperties().getClientName())) { + singleServerConfig.setClientName(ApplicationNameContext.getApplicationName() + ":" + IpUtil.getIp()); + } + singleServerConfig.setDatabase(getRedissonProperties().getDatabase()); + singleServerConfig.setConnectionMinimumIdleSize(getRedissonProperties().getConnectionMinimumIdleSize()); + singleServerConfig.setConnectionPoolSize(getRedissonProperties().getConnectionPoolSize()); + singleServerConfig.setIdleConnectionTimeout(getRedissonProperties().getIdleConnectionTimeout()); + singleServerConfig.setConnectTimeout(getRedissonProperties().getConnectTimeout()); + singleServerConfig.setTimeout(getRedissonProperties().getTimeout()); + singleServerConfig.setRetryAttempts(getRedissonProperties().getRetryAttempts()); + RedisHostAndPort redisHostAndPort = redisHostAndPorts.stream().findAny().get(); + singleServerConfig.setPassword(redisHostAndPort.getPkey()); + singleServerConfig.setAddress(String.format("redis://%s/", redisHostAndPort.getHost() + ":" + redisHostAndPort.getPort())); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisHostAndPort.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisHostAndPort.java new file mode 100644 index 0000000..e1ca3d4 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisHostAndPort.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.redisson.config; + +import lombok.Data; + +@Data +public class RedisHostAndPort { + + private String host; + + private int port; + + private String pkey; +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisType.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisType.java new file mode 100644 index 0000000..2eb3fd6 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisType.java @@ -0,0 +1,23 @@ +package cn.lingniu.framework.plugin.redisson.config; + +import lombok.Getter; + +/** + * 集群类型 + **/ +public enum RedisType { + STANDALONE("standalone", "单节点类型"), + CLUSTER("cluster", "集群类型"), + SENTINEL("sentinel", "哨兵类型"); + + @Getter + private String type; + + @Getter + private String memo; + + RedisType(String type, String memo) { + this.type = type; + this.memo = memo; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonConfig.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonConfig.java new file mode 100644 index 0000000..2cbea8f --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonConfig.java @@ -0,0 +1,21 @@ +package cn.lingniu.framework.plugin.redisson.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.HashMap; +import java.util.Map; + +/** + * redisson 配置 + */ +@ConfigurationProperties(prefix = RedissonConfig.PREFIX) +@Data +public class RedissonConfig { + + public static final String PREFIX = "framework.lingniu.redisson"; + + + private Map remote = new HashMap<>(); + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonProperties.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonProperties.java new file mode 100644 index 0000000..bfa8d7e --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonProperties.java @@ -0,0 +1,42 @@ +package cn.lingniu.framework.plugin.redisson.config; + +import lombok.Data; + + +/** + * 详细配置 + */ +@Data +public class RedissonProperties { + + /** + * redis连接串信息 host:port,host:port, 多个用,分割,最后一个是;是密码,可以不配置 + */ + private String redisAddresses; + + /** + * readMode must be one of SLAVE, MASTER, MASTER_SLAVE + */ + private String readMode = "MASTER"; + /** + * 类型 + */ + private RedisType storageType = RedisType.CLUSTER; + private String clientName = ""; + + private int database = 0; + + private int connectionMinimumIdleSize = 12; + + private int connectionPoolSize = 32; + + private int idleConnectionTimeout = 10000; + + private int connectTimeout = 2000; + + private int timeout = 2000; + + private int retryAttempts = 4; + //cluster: Slots检查 + private Boolean checkSlotsCoverage = true; +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/LocalVariableTableParameterNameDiscoverer.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/LocalVariableTableParameterNameDiscoverer.java new file mode 100644 index 0000000..39a5a23 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/LocalVariableTableParameterNameDiscoverer.java @@ -0,0 +1,240 @@ +package cn.lingniu.framework.plugin.redisson.init; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.asm.ClassReader; +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.asm.Type; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class LocalVariableTableParameterNameDiscoverer implements ParameterNameDiscoverer { + + private static final Log logger = LogFactory.getLog(LocalVariableTableParameterNameDiscoverer.class); + + // marker object for classes that do not have any debug info + private static final Map NO_DEBUG_INFO_MAP = Collections.emptyMap(); + + // the cache uses a nested index (value is a map) to keep the top level cache relatively small in size + private final Map, Map> parameterNamesCache = new ConcurrentHashMap<>(32); + + + @Override + @Nullable + public String[] getParameterNames(Method method) { + Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); + Class declaringClass = originalMethod.getDeclaringClass(); + Map map = this.parameterNamesCache.get(declaringClass); + if (map == null) { + map = inspectClass(declaringClass); + this.parameterNamesCache.put(declaringClass, map); + } + if (map != NO_DEBUG_INFO_MAP) { + return map.get(originalMethod); + } + return null; + } + + @Override + @Nullable + public String[] getParameterNames(Constructor ctor) { + Class declaringClass = ctor.getDeclaringClass(); + Map map = this.parameterNamesCache.get(declaringClass); + if (map == null) { + map = inspectClass(declaringClass); + this.parameterNamesCache.put(declaringClass, map); + } + if (map != NO_DEBUG_INFO_MAP) { + return map.get(ctor); + } + return null; + } + + /** + * Inspects the target class. Exceptions will be logged and a maker map returned + * to indicate the lack of debug information. + */ + private Map inspectClass(Class clazz) { + InputStream is = clazz.getResourceAsStream(ClassUtils.getClassFileName(clazz)); + if (is == null) { + // We couldn't load the class file, which is not fatal as it + // simply means this method of discovering parameter names won't work. + if (logger.isDebugEnabled()) { + logger.debug("Cannot find '.class' file for class [" + clazz + + "] - unable to determine constructor/method parameter names"); + } + return NO_DEBUG_INFO_MAP; + } + try { + ClassReader classReader = new ClassReader(is); + Map map = new ConcurrentHashMap<>(32); + classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0); + return map; + } catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Exception thrown while reading '.class' file for class [" + clazz + + "] - unable to determine constructor/method parameter names", ex); + } + } catch (IllegalArgumentException ex) { + if (logger.isDebugEnabled()) { + logger.debug("ASM ClassReader failed to parse class file [" + clazz + + "], probably due to a new Java class file version that isn't supported yet " + + "- unable to determine constructor/method parameter names", ex); + } + } finally { + try { + is.close(); + } catch (IOException ex) { + // ignore + } + } + return NO_DEBUG_INFO_MAP; + } + + + /** + * Helper class that inspects all methods (constructor included) and then + * attempts to find the parameter names for that member. + */ + private static class ParameterNameDiscoveringVisitor extends ClassVisitor { + + private static final String STATIC_CLASS_INIT = ""; + + private final Class clazz; + + private final Map memberMap; + + public ParameterNameDiscoveringVisitor(Class clazz, Map memberMap) { + super(SpringAsmInfo.ASM_VERSION); + this.clazz = clazz; + this.memberMap = memberMap; + } + + @Override + @Nullable + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + // exclude synthetic + bridged && static class initialization + if (!isSyntheticOrBridged(access) && !STATIC_CLASS_INIT.equals(name)) { + return new LocalVariableTableVisitor(this.clazz, this.memberMap, name, desc, isStatic(access)); + } + return null; + } + + private static boolean isSyntheticOrBridged(int access) { + return (((access & Opcodes.ACC_SYNTHETIC) | (access & Opcodes.ACC_BRIDGE)) > 0); + } + + private static boolean isStatic(int access) { + return ((access & Opcodes.ACC_STATIC) > 0); + } + } + + + private static class LocalVariableTableVisitor extends MethodVisitor { + + private static final String CONSTRUCTOR = ""; + + private final Class clazz; + + private final Map memberMap; + + private final String name; + + private final Type[] args; + + private final String[] parameterNames; + + private final boolean isStatic; + + private boolean hasLvtInfo = false; + + /* + * The nth entry contains the slot index of the LVT table entry holding the + * argument name for the nth parameter. + */ + private final int[] lvtSlotIndex; + + public LocalVariableTableVisitor(Class clazz, Map map, String name, String desc, boolean isStatic) { + super(SpringAsmInfo.ASM_VERSION); + this.clazz = clazz; + this.memberMap = map; + this.name = name; + this.args = Type.getArgumentTypes(desc); + this.parameterNames = new String[this.args.length]; + this.isStatic = isStatic; + this.lvtSlotIndex = computeLvtSlotIndices(isStatic, this.args); + } + + @Override + public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) { + this.hasLvtInfo = true; + for (int i = 0; i < this.lvtSlotIndex.length; i++) { + if (this.lvtSlotIndex[i] == index) { + this.parameterNames[i] = name; + } + } + } + + @Override + public void visitEnd() { + if (this.hasLvtInfo || (this.isStatic && this.parameterNames.length == 0)) { + // visitLocalVariable will never be called for static no args methods + // which doesn't use any local variables. + // This means that hasLvtInfo could be false for that kind of methods + // even if the class has local variable info. + this.memberMap.put(resolveMember(), this.parameterNames); + } + } + + private Member resolveMember() { + ClassLoader loader = this.clazz.getClassLoader(); + Class[] argTypes = new Class[this.args.length]; + for (int i = 0; i < this.args.length; i++) { + argTypes[i] = ClassUtils.resolveClassName(this.args[i].getClassName(), loader); + } + try { + if (CONSTRUCTOR.equals(this.name)) { + return this.clazz.getDeclaredConstructor(argTypes); + } + return this.clazz.getDeclaredMethod(this.name, argTypes); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException("Method [" + this.name + + "] was discovered in the .class file but cannot be resolved in the class object", ex); + } + } + + private static int[] computeLvtSlotIndices(boolean isStatic, Type[] paramTypes) { + int[] lvtIndex = new int[paramTypes.length]; + int nextIndex = (isStatic ? 0 : 1); + for (int i = 0; i < paramTypes.length; i++) { + lvtIndex[i] = nextIndex; + if (isWideType(paramTypes[i])) { + nextIndex += 2; + } else { + nextIndex++; + } + } + return lvtIndex; + } + + private static boolean isWideType(Type aType) { + // float is not a wide type + return (aType == Type.LONG_TYPE || aType == Type.DOUBLE_TYPE); + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonAutoConfiguration.java new file mode 100644 index 0000000..1160fbf --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonAutoConfiguration.java @@ -0,0 +1,53 @@ +package cn.lingniu.framework.plugin.redisson.init; + +import cn.lingniu.framework.plugin.redisson.RedissonClusterLockerService; +import cn.lingniu.framework.plugin.redisson.RedissonClientFactory; +import cn.lingniu.framework.plugin.redisson.config.RedissonConfig; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import java.util.ArrayList; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * 初始化-RedissonClusterLockerService + */ +@Slf4j +@EnableConfigurationProperties({RedissonConfig.class}) +public class RedissonAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public RedissonClusterLockerService redissonClusterLockerService() { + return new RedissonClusterLockerService() { + @Override + public R wrapLock(T source, Function fun, Function keyGetter, Function wrapFun, Long leaseTime, TimeUnit leaseTimeUnit) { + RedissonClient client = Optional.ofNullable(RedissonClientFactory.getRedissonClient(fun.apply(source))).orElseThrow(() -> new IllegalStateException("Redisson client not found for: " + fun.apply(source))); + RLock lock = client.getLock(keyGetter.apply(source)); + try { + lock.lock(leaseTime, leaseTimeUnit); + return wrapFun.apply(source); + } finally { + lock.unlock(); + } + } + + @Override + public String delivery(String key) { + return new ArrayList<>(RedissonClientFactory.getRedisMap().keySet()).toArray(new String[]{})[Math.abs(key.hashCode() % RedissonClientFactory.getRedisMap().size())]; + } + }; + } + + @Bean + RedissonBeanRegisterPostProcessor redissonBeanRegister() { + return new RedissonBeanRegisterPostProcessor(); + } + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonBeanRegisterPostProcessor.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonBeanRegisterPostProcessor.java new file mode 100644 index 0000000..46da7c1 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonBeanRegisterPostProcessor.java @@ -0,0 +1,75 @@ +package cn.lingniu.framework.plugin.redisson.init; + +import cn.lingniu.framework.plugin.redisson.RedissonClientFactory; +import cn.lingniu.framework.plugin.redisson.builder.RedissonClusterClientBuilder; +import cn.lingniu.framework.plugin.redisson.builder.RedissonStandaloneClientBuilder; +import cn.lingniu.framework.plugin.redisson.config.RedisType; +import cn.lingniu.framework.plugin.redisson.config.RedissonConfig; +import cn.lingniu.framework.plugin.redisson.config.RedissonProperties; +import lombok.extern.slf4j.Slf4j; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; + +import java.util.Map; +import java.util.Optional; + +/** + * 初始化 + */ +@Slf4j +public class RedissonBeanRegisterPostProcessor implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware { + + private ConfigurableApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = (ConfigurableApplicationContext) applicationContext; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + RedissonConfig config = Binder.get(applicationContext.getEnvironment()).bind(RedissonConfig.PREFIX, RedissonConfig.class).orElse(new RedissonConfig()); + Map remote = config.getRemote(); + Optional.ofNullable(remote).ifPresent(b -> b.forEach((k, v) -> { + if (applicationContext.containsBean(k)) { + throw new IllegalStateException("redisson的实例对象已注册,请查看配置:" + k); + } + registerContainer(registry, k, v); + })); + } + + private void registerContainer(BeanDefinitionRegistry registry, String beanName, RedissonProperties redissonProperties) { + Config redissonConfig = null; + if (RedisType.STANDALONE.equals(redissonProperties.getStorageType())) { + redissonConfig = new RedissonStandaloneClientBuilder(redissonProperties).build(); + } else if (RedisType.CLUSTER.equals(redissonProperties.getStorageType())) { + redissonConfig = new RedissonClusterClientBuilder(redissonProperties).build(); + } else { + throw new IllegalArgumentException("不支持的Redisson存储类型:" + redissonProperties.getStorageType()); + } + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Redisson.class); + builder.addConstructorArgValue(redissonConfig); + registry.registerBeanDefinition(beanName, builder.getBeanDefinition()); + // 从ApplicationContext中获取已注册的Bean实例 + RedissonClient jedisCluster = applicationContext.getBean(beanName, RedissonClient.class); + RedissonClientFactory.putRedissonClient(beanName, jedisCluster); + if (!registry.containsBeanDefinition("redissonDistributedLockAspectConfiguration")) { + registry.registerBeanDefinition("redissonDistributedLockAspectConfiguration", + BeanDefinitionBuilder.rootBeanDefinition(RedissonDistributedLockAspectConfiguration.class).getBeanDefinition()); + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonDistributedLockAspectConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonDistributedLockAspectConfiguration.java new file mode 100644 index 0000000..454ce0f --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonDistributedLockAspectConfiguration.java @@ -0,0 +1,89 @@ +package cn.lingniu.framework.plugin.redisson.init; + +import cn.lingniu.framework.plugin.redisson.RedissonLockAction; +import cn.lingniu.framework.plugin.redisson.RedissonClientFactory; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * Aspect 实现 + */ +@Slf4j +@Aspect +public class RedissonDistributedLockAspectConfiguration { + + private ExpressionParser parser = new SpelExpressionParser(); + + private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); + + @Pointcut("@annotation(cn.lingniu.framework.plugin.redisson.RedissonLockAction)") + public void lockPoint() { + } + + @Around("lockPoint()") + public Object around(ProceedingJoinPoint pjp) throws Throwable { + Method method = ((MethodSignature) pjp.getSignature()).getMethod(); + RedissonLockAction redissonLockAction = method.getAnnotation(RedissonLockAction.class); + Object[] args = pjp.getArgs(); + String key = redissonLockAction.value(); + key = parse(key, method, args); + + RLock lock = getLock(key, redissonLockAction); + if (!lock.tryLock(redissonLockAction.waitTime(), redissonLockAction.leaseTime(), redissonLockAction.unit())) { + log.warn("RedissonLockAction-获取锁失败: [{}]", key); + if (redissonLockAction.throwException()) + throw new RuntimeException(String.format("RedissonLockAction-获取锁失败:[%s]", key)); + return null; + } + log.info("RedissonLockAction-获取锁成功: [{}]", key); + try { + return pjp.proceed(); + } catch (Exception e) { + log.error(String.format("RedissonLockAction-方法锁执行异常:[%s]", key), e); + throw e; + } finally { + lock.unlock(); + log.info("RedissonLockAction-释放锁成功: [{}]", key); + } + } + + /** + * 解析spring EL表达式 + */ + private String parse(String key, Method method, Object[] args) { + String[] params = discoverer.getParameterNames(method); + EvaluationContext context = new StandardEvaluationContext(); + for (int i = 0; i < params.length; i++) { + context.setVariable(params[i], args[i]); + } + return parser.parseExpression(key).getValue(context, String.class); + } + + private RLock getLock(String key, RedissonLockAction redissonLockAction) { + RedissonClient redissonClient = RedissonClientFactory.getRedissonClient(redissonLockAction.redissonName()); + switch (redissonLockAction.lockType()) { + case REENTRANT_LOCK: + return redissonClient.getLock(key); + case FAIR_LOCK: + return redissonClient.getFairLock(key); + case READ_LOCK: + return redissonClient.getReadWriteLock(key).readLock(); + case WRITE_LOCK: + return redissonClient.getReadWriteLock(key).writeLock(); + default: + throw new RuntimeException("RedissonLockAction-不支持的锁类型:" + redissonLockAction.lockType().name()); + } + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..c39dfd6 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.redisson.init.RedissonAutoConfiguration diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/redisson-config.md b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/redisson-config.md new file mode 100644 index 0000000..22c4ff0 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/redisson-config.md @@ -0,0 +1,121 @@ +# 【重要】redisson详细资料-redis搭建---参考官网 + +## 概述 (Overview) + +1. 基于 Redisson 封装的 Redis 集群操作和分布式锁组件,专门用于处理 Redis 集群环境下的分布式锁场景 +2. 核心能力 + * 高性能键值存储:基于RocksDB的高效读写能力,支持快速的数据存取操作。 + * 集群自适应感知:客户端能够感知集群变化并自动刷新配置 + * Redisson 操作:基于 Redisson 提供 Redis 集群操作能力 + * 分布式锁支持:支持多种类型的分布式锁: + ■ 可重入锁(REENTRANT_LOCK) + ■ 公平锁(FAIR_LOCK) + ■ 读锁(READ_LOCK) + ■ 写锁(WRITE_LOCK) + * 注解式锁操作:通过 @LockAction 注解简化分布式锁的使用 + * 灵活配置:支持锁等待时间、自动释放时间、时间单位等参数配置 + * 异常处理:支持锁获取失败时的异常抛出或返回 null 两种处理方式 + * 支持多集群操作redis,也可扩展队列消费 +3. 适用场景:该组件特别适用于需要在 Redis 集群环境下使用分布式锁的企业级应用,通过注解方式简化了分布式锁的使用,提供了多种锁类型以满足不同业务场景的需求 + * 分布式系统:需要在分布式环境下保证数据一致性的场景 + * 高并发业务:需要防止并发操作导致数据不一致的业务场景 + * 集群环境:基于 Redis 集群部署的应用系统 + * 资源竞争控制:需要对共享资源进行并发访问控制的场景 + * 定时任务:分布式环境下需要确保任务单实例执行的定时任务 + * 库存扣减:电商系统中需要防止超卖的库存扣减场景 + * 幂等性保证:需要保证接口幂等性的业务操作。 + +## 如何配置--更多参数参考:RedissonConfig/RedissonProperties + +```yaml +framework: + lingniu: + redisson: + remote: + r1: + # 应用id需在cachecloud申请 + redisAddresses: xxxx:6387 + # 客户端名称,默认为应用名 + IP + clientName: "" + # 单节点 Redis 默认数据库,默认为 0 + database: 0 + # 连接池最小空闲连接数,默认为 12 + connectionMinimumIdleSize: 12 + # 连接池最大连接数,默认为 32 + connectionPoolSize: 32 + # 空闲连接超时时间(毫秒),默认为 10000 + idleConnectionTimeout: 10000 + # 连接超时时间(毫秒),默认为 2000 + connectTimeout: 2000 + # 操作超时时间(毫秒),默认为 2000 + timeout: 2000 + # 操作重试次数,默认为 4 + retryAttempts: 4 + # 集群模式下是否检查 Slots 覆盖,默认为 true + checkSlotsCoverage: true + # 读取模式,默认为 "SLAVE" + # 可选值: + # - SLAVE: 从节点读取 + # - MASTER: 主节点读取 + # - MASTER_SLAVE: 主从节点读取 + readMode: "SLAVE" + # 存储类型,默认为 "CLUSTER" + # 可选值: + # - CLUSTER: 集群模式 + # - SINGLE: 单节点模式 + # - SENTINEL: 哨兵模式 + # - REPLICATED: 复制模式 + storageType: "CLUSTER" + # 实例名称 + r2: + # 应用id需在cachecloud申请 + redisAddresses: xxx:6387 + # 单节点 Redis 默认数据库,默认为 0 + database: 0 + +``` + +## 如何使用--正常操作redis + +```java +@Autowired +private RedissonClient test; +// 或者 +RedissonFactory.getRedissonClient("test").api +``` + +## 如何使用--分布式锁 + +```java +/** + * 使用 #orderId 参数作为锁的key + */ +@PostMapping("/process/{orderId}") +@LockAction( + redissonName = "r1", + key = "#orderId", + lockType = LockType.REENTRANT_LOCK, + waitTime = 3000L, leaseTime = 30000L, unit = TimeUnit.MILLISECONDS, + throwEx = true +) +public ResponseEntity processOrder(@PathVariable String orderId) { + // 业务逻辑 + return ResponseEntity.ok("Order processed: " + orderId); +} + +/** + * 使用请求参数 + */ +@GetMapping("/query") +@LockAction( + redissonName = "defaultRedisson", + key = "'query:' + #userId + ':' + #status", + throwEx = true +) +public ResponseEntity> queryOrders( + @RequestParam String userId, + @RequestParam String status) { + // 业务逻辑 + return ResponseEntity.ok(new ArrayList<>()); +} +``` \ No newline at end of file diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/pom.xml b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/pom.xml new file mode 100644 index 0000000..d7f04fa --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-apollo + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + com.ctrip.framework.apollo + apollo-client + + + org.javassist + javassist + + + org.springframework.cloud + spring-cloud-context + + + org.springframework.boot + spring-boot-autoconfigure + provided + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + provided + true + + + + + diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/config/ApolloConfig.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/config/ApolloConfig.java new file mode 100644 index 0000000..42c6c11 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/config/ApolloConfig.java @@ -0,0 +1,29 @@ +package cn.lingniu.framework.plugin.apollo.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = ApolloConfig.PRE_FIX) +public class ApolloConfig { + + public final static String PRE_FIX = "framework.lingniu.apollo"; + + /** + * 是否开始Apollo配置 + */ + private Boolean enabled = true; + /** + * Meta地址 + */ + private String meta; + /** + * 配置文件列表 以,分割 + */ + private String namespaces = ""; + /** + * 无需配置 app.id 等于 spring.application.name + */ + private String appId; + +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloAnnotationProcessor.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloAnnotationProcessor.java new file mode 100644 index 0000000..60ea82c --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloAnnotationProcessor.java @@ -0,0 +1,79 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigChangeListener; +import com.ctrip.framework.apollo.ConfigService; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfig; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; +import com.ctrip.framework.apollo.spring.annotation.ApolloProcessor; +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Set; + +/** + * Apollo Annotation Processor for Spring Application + * + * @author Jason Song(song_s@ctrip.com) + */ +public class ApolloAnnotationProcessor extends ApolloProcessor { + + @Override + protected void processField(Object bean, String beanName, Field field) { + ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class); + if (annotation == null) { + return; + } + + Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()), + "Invalid type: %s for field: %s, should be Config", field.getType(), field); + + String namespace = System.getProperty("apollo.bootstrap.namespaces", annotation.value()); + Config config = ConfigService.getConfig(namespace); + + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, bean, config); + } + + @Override + protected void processMethod(final Object bean, String beanName, final Method method) { + ApolloConfigChangeListener annotation = AnnotationUtils + .findAnnotation(method, ApolloConfigChangeListener.class); + if (annotation == null) { + return; + } + Class[] parameterTypes = method.getParameterTypes(); + Preconditions.checkArgument(parameterTypes.length == 1, + "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length, + method); + Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]), + "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0], + method); + + ReflectionUtils.makeAccessible(method); + + String namespaceProperties = System.getProperty("apollo.bootstrap.namespaces", String.join(",", annotation.value())); + String[] namespaces = namespaceProperties.split(","); + String[] annotatedInterestedKeys = annotation.interestedKeys(); + String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes(); + ConfigChangeListener configChangeListener = changeEvent -> ReflectionUtils.invokeMethod(method, bean, changeEvent); + + Set interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null; + Set interestedKeyPrefixes = annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes) : null; + + for (String namespace : namespaces) { + Config config = ConfigService.getConfig(namespace); + + if (interestedKeys == null && interestedKeyPrefixes == null) { + config.addChangeListener(configChangeListener); + } else { + config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes); + } + } + } +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloConfigChangeLogListener.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloConfigChangeLogListener.java new file mode 100644 index 0000000..0e824c6 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloConfigChangeLogListener.java @@ -0,0 +1,58 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import com.ctrip.framework.apollo.model.ConfigChange; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.context.scope.refresh.RefreshScope; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + + +@Component +@Slf4j +public class ApolloConfigChangeLogListener implements ApplicationContextAware { + + @Resource + RefreshScope refreshScope; + + private ApplicationContext applicationContext; + + @ApolloConfigChangeListener + public void onChange(ConfigChangeEvent changeEvent) { + if (log.isInfoEnabled()) { + for (String changedKey : changeEvent.changedKeys()) { + ConfigChange changeInfo = changeEvent.getChange(changedKey); + if (ObjectEmptyUtils.isEmpty(changeInfo)) { + continue; + } + log.info("【apollo 配置变更】 - namespace: {}, property: {}, oldValue: {}, newValue: {}, changeType: {}", + changeInfo.getNamespace(), changeInfo.getPropertyName(), + changeInfo.getOldValue(), changeInfo.getNewValue(), changeInfo.getChangeType()); + } + } + applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); + refreshProperties(changeEvent); + } + + public void refreshProperties(ConfigChangeEvent changeEvent) { + try { + this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); + if (ObjectEmptyUtils.isNotEmpty(refreshScope)) { + refreshScope.refreshAll(); + } + } catch (Exception e) { + log.error("Failed to refresh properties after Apollo config change", e); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ClassPoolUtils.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ClassPoolUtils.java new file mode 100644 index 0000000..b175302 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ClassPoolUtils.java @@ -0,0 +1,26 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import javassist.ClassPool; +import javassist.LoaderClassPath; + + +public class ClassPoolUtils { + + private static volatile ClassPool instance; + + private ClassPoolUtils() { + } + + public static ClassPool getInstance() { + if (instance == null) { + synchronized (ClassPoolUtils.class) { + if (instance == null) { + instance = ClassPool.getDefault(); + instance.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader())); + } + } + } + return instance; + } + +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FramewokConfigPropertySourcesProcessorHelper.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FramewokConfigPropertySourcesProcessorHelper.java new file mode 100644 index 0000000..83b1534 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FramewokConfigPropertySourcesProcessorHelper.java @@ -0,0 +1,47 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import com.ctrip.framework.apollo.core.spi.Ordered; +import com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor; +import com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener; +import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor; +import com.ctrip.framework.apollo.spring.spi.ConfigPropertySourcesProcessorHelper; +import com.ctrip.framework.apollo.spring.util.BeanRegistrationUtil; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; + +import java.util.HashMap; +import java.util.Map; + +public class FramewokConfigPropertySourcesProcessorHelper implements ConfigPropertySourcesProcessorHelper { + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + Map propertySourcesPlaceholderPropertyValues = new HashMap<>(); + // to make sure the default PropertySourcesPlaceholderConfigurer's priority is higher than PropertyPlaceholderConfigurer + propertySourcesPlaceholderPropertyValues.put("order", 0); + + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class, + propertySourcesPlaceholderPropertyValues); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, AutoUpdateConfigChangeListener.class);// + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, FrameworkApolloAnnotationProcessor.class); //扩展 Apollo 注解处理器 + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class); + + processSpringValueDefinition(registry); + } + + /** + * For Spring 3.x versions, the BeanDefinitionRegistryPostProcessor would not be instantiated if + * it is added in postProcessBeanDefinitionRegistry phase, so we have to manually call the + * postProcessBeanDefinitionRegistry method of SpringValueDefinitionProcessor here... + */ + private void processSpringValueDefinition(BeanDefinitionRegistry registry) { + SpringValueDefinitionProcessor springValueDefinitionProcessor = new SpringValueDefinitionProcessor(); + springValueDefinitionProcessor.postProcessBeanDefinitionRegistry(registry); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10 ; // 优先级放高 + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloAnnotationProcessor.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloAnnotationProcessor.java new file mode 100644 index 0000000..6194ab6 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloAnnotationProcessor.java @@ -0,0 +1,265 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigChangeListener; +import com.ctrip.framework.apollo.ConfigService; +import com.ctrip.framework.apollo.build.ApolloInjector; +import com.ctrip.framework.apollo.core.utils.StringUtils; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.spring.annotation.ApolloAnnotationProcessor; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfig; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; +import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue; +import com.ctrip.framework.apollo.spring.annotation.ApolloProcessor; +import com.ctrip.framework.apollo.spring.property.PlaceholderHelper; +import com.ctrip.framework.apollo.spring.property.SpringValue; +import com.ctrip.framework.apollo.spring.property.SpringValueRegistry; +import com.ctrip.framework.apollo.spring.util.SpringInjector; +import com.ctrip.framework.apollo.util.ConfigUtil; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Apollo Annotation Processor for Spring Application + */ +public class FrameworkApolloAnnotationProcessor extends ApolloProcessor implements BeanFactoryAware, + EnvironmentAware { + + private static final Logger logger = LoggerFactory.getLogger(ApolloAnnotationProcessor.class); + + private static final String NAMESPACE_DELIMITER = ","; + + private static final Splitter NAMESPACE_SPLITTER = Splitter.on(NAMESPACE_DELIMITER) + .omitEmptyStrings().trimResults(); + private static final Map DATEPATTERN_GSON_MAP = new ConcurrentHashMap<>(); + + private final ConfigUtil configUtil; // 配置工具类 + private final PlaceholderHelper placeholderHelper; + private final SpringValueRegistry springValueRegistry; + + /** + * resolve the expression. + */ + private ConfigurableBeanFactory configurableBeanFactory; + + private Environment environment; + + public FrameworkApolloAnnotationProcessor() { + configUtil = ApolloInjector.getInstance(ConfigUtil.class); + placeholderHelper = SpringInjector.getInstance(PlaceholderHelper.class); + springValueRegistry = SpringInjector.getInstance(SpringValueRegistry.class); + } + + @Override + protected void processField(Object bean, String beanName, Field field) { + this.processApolloConfig(bean, field); + this.processApolloJsonValue(bean, beanName, field); + } + // 处理Bean方法上的Apollo注解 + @Override + protected void processMethod(final Object bean, String beanName, final Method method) { + this.processApolloConfigChangeListener(bean, method); + this.processApolloJsonValue(bean, beanName, method); + } + // field-具体注解处理方法--处理@ApolloConfig字段注解 + private void processApolloConfig(Object bean, Field field) { + ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class); + if (annotation == null) { + return; + } + Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()), + "Invalid type: %s for field: %s, should be Config", field.getType(), field); + final String appId = StringUtils.defaultIfBlank(annotation.appId(), configUtil.getAppId()); + final String namespace = annotation.value(); + final String resolvedAppId = this.environment.resolveRequiredPlaceholders(appId); + final String resolvedNamespace = this.environment.resolveRequiredPlaceholders(namespace); + Config config = ConfigService.getConfig(resolvedAppId, resolvedNamespace); + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, bean, config); + } + private void processApolloConfigChangeListener(final Object bean, final Method method) { + ApolloConfigChangeListener annotation = AnnotationUtils + .findAnnotation(method, ApolloConfigChangeListener.class); + if (annotation == null) { + return; + } + Class[] parameterTypes = method.getParameterTypes(); + Preconditions.checkArgument(parameterTypes.length == 1, + "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length, + method); + Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]), + "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0], + method); + + ReflectionUtils.makeAccessible(method); + String appId = StringUtils.defaultIfBlank(annotation.appId(), configUtil.getAppId()); + String namespaceProperties =System.getProperty("apollo.bootstrap.namespaces",String.join( "," ,annotation.value())); //todo 默认处理所有-bootstrap.namespaces + String[] namespaces = namespaceProperties.split(","); + String[] annotatedInterestedKeys = annotation.interestedKeys(); + String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes(); + ConfigChangeListener configChangeListener = changeEvent -> ReflectionUtils.invokeMethod(method, bean, changeEvent); + + Set interestedKeys = + annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null; + Set interestedKeyPrefixes = + annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes) + : null; + + Set resolvedNamespaces = processResolveNamespaceValue(namespaces); + + for (String namespace : resolvedNamespaces) { + Config config = ConfigService.getConfig(appId, namespace); + + if (interestedKeys == null && interestedKeyPrefixes == null) { + config.addChangeListener(configChangeListener); + } else { + config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes); + } + } + } + + /** + * Evaluate and resolve namespaces from env/properties. + * Split delimited namespaces + * @param namespaces + * @return resolved namespaces + */ + private Set processResolveNamespaceValue(String[] namespaces) { + + Set resolvedNamespaces = new HashSet<>(); + + for (String namespace : namespaces) { + final String resolvedNamespace = this.environment.resolveRequiredPlaceholders(namespace); + + if (resolvedNamespace.contains(NAMESPACE_DELIMITER)) { + resolvedNamespaces.addAll(NAMESPACE_SPLITTER.splitToList(resolvedNamespace)); + } else { + resolvedNamespaces.add(resolvedNamespace); + } + } + + return resolvedNamespaces; + } + + private void processApolloJsonValue(Object bean, String beanName, Field field) { + ApolloJsonValue apolloJsonValue = AnnotationUtils.getAnnotation(field, ApolloJsonValue.class); + if (apolloJsonValue == null) { + return; // 处理方法上的@ApolloJsonValue注解 + } + + String placeholder = apolloJsonValue.value(); + String datePattern = apolloJsonValue.datePattern(); + Object propertyValue = this.resolvePropertyValue(beanName, placeholder); + if (propertyValue == null) { + return; + } + + boolean accessible = field.isAccessible(); + field.setAccessible(true); + ReflectionUtils + .setField(field, bean, parseJsonValue((String) propertyValue, field.getGenericType(), datePattern)); + field.setAccessible(accessible); + + if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) { + Set keys = placeholderHelper.extractPlaceholderKeys(placeholder); + for (String key : keys) { + SpringValue springValue = new SpringValue(key, placeholder, bean, beanName, field, true); + springValueRegistry.register(this.configurableBeanFactory, key, springValue); + logger.debug("Monitoring {}", springValue); + } + } + } + + private void processApolloJsonValue(Object bean, String beanName, Method method) { + ApolloJsonValue apolloJsonValue = AnnotationUtils.getAnnotation(method, ApolloJsonValue.class); + if (apolloJsonValue == null) { + return; + } + + String placeHolder = apolloJsonValue.value(); + String datePattern = apolloJsonValue.datePattern(); + Object propertyValue = this.resolvePropertyValue(beanName, placeHolder); + if (propertyValue == null) { + return; + } + + Type[] types = method.getGenericParameterTypes(); + Preconditions.checkArgument(types.length == 1, + "Ignore @ApolloJsonValue setter {}.{}, expecting 1 parameter, actual {} parameters", + bean.getClass().getName(), method.getName(), method.getParameterTypes().length); + + boolean accessible = method.isAccessible(); + method.setAccessible(true); // 解析占位符获取JSON值 + ReflectionUtils.invokeMethod(method, bean, parseJsonValue((String) propertyValue, types[0], datePattern)); + method.setAccessible(accessible); + + if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) { + Set keys = placeholderHelper.extractPlaceholderKeys(placeHolder); + for (String key : keys) { + SpringValue springValue = new SpringValue(key, placeHolder, bean, beanName, method, true); + springValueRegistry.register(this.configurableBeanFactory, key, springValue); + logger.debug("Monitoring {}", springValue); + } + } + } + + private Object resolvePropertyValue(String beanName, String placeHolder) { + Object propertyValue = placeholderHelper + .resolvePropertyValue(this.configurableBeanFactory, beanName, placeHolder); + + // propertyValue will never be null, as @ApolloJsonValue will not allow that + if (!(propertyValue instanceof String)) { + return null; + } + + return propertyValue; + } + + private Object parseJsonValue(String json, Type targetType, String datePattern) { + try { + return DATEPATTERN_GSON_MAP.computeIfAbsent(datePattern, this::buildGson).fromJson(json, targetType); + } catch (Throwable ex) { + logger.error("Parsing json '{}' to type {} failed!", json, targetType, ex); + throw ex; + } + } + + private Gson buildGson(String datePattern) { + if (StringUtils.isBlank(datePattern)) { + return new Gson(); + } + return new GsonBuilder().setDateFormat(datePattern).create(); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.configurableBeanFactory = (ConfigurableBeanFactory) beanFactory; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloConfigRegistrarHelper.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloConfigRegistrarHelper.java new file mode 100644 index 0000000..74368f9 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloConfigRegistrarHelper.java @@ -0,0 +1,118 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import com.ctrip.framework.apollo.build.ApolloInjector; +import com.ctrip.framework.apollo.core.spi.Ordered; +import com.ctrip.framework.apollo.core.utils.StringUtils; +import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; +import com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor; +import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor; +import com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener; +import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor; +import com.ctrip.framework.apollo.spring.spi.DefaultApolloConfigRegistrarHelper; +import com.ctrip.framework.apollo.spring.util.BeanRegistrationUtil; +import com.ctrip.framework.apollo.util.ConfigUtil; +import com.google.common.collect.Lists; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; + +import java.util.HashMap; +import java.util.Map; + +public class FrameworkApolloConfigRegistrarHelper implements com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper, Ordered { + private static final Logger logger = LoggerFactory.getLogger( + DefaultApolloConfigRegistrarHelper.class); + + private final ConfigUtil configUtil = ApolloInjector.getInstance(ConfigUtil.class); + + private Environment environment; + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + AnnotationAttributes attributes = AnnotationAttributes + .fromMap(importingClassMetadata.getAnnotationAttributes(EnableApolloConfig.class.getName())); + final String[] namespaces = System.getProperty("apollo.bootstrap.namespaces","application").split(","); //todo 默认处理所有-bootstrap.namespaces + final int order = attributes.getNumber("order"); + + // put main appId + PropertySourcesProcessor.addNamespaces(configUtil.getAppId(), Lists.newArrayList(this.resolveNamespaces(namespaces)), order); + + // put multiple appId into + AnnotationAttributes[] multipleConfigs = attributes.getAnnotationArray("multipleConfigs"); + if (multipleConfigs != null) { + for (AnnotationAttributes multipleConfig : multipleConfigs) { + String appId = multipleConfig.getString("appId"); + String[] multipleNamespaces = this.resolveNamespaces(multipleConfig.getStringArray("namespaces")); + String secret = resolveSecret(multipleConfig.getString("secret")); + int multipleOrder = multipleConfig.getNumber("order"); + + // put multiple secret into system property + if (!StringUtils.isBlank(secret)) { + System.setProperty("apollo.accesskey." + appId + ".secret", secret); + } + PropertySourcesProcessor.addNamespaces(appId, Lists.newArrayList(multipleNamespaces), multipleOrder); + } + } + + Map propertySourcesPlaceholderPropertyValues = new HashMap<>(); + // to make sure the default PropertySourcesPlaceholderConfigurer's priority is higher than PropertyPlaceholderConfigurer + propertySourcesPlaceholderPropertyValues.put("order", 0); + + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class, + propertySourcesPlaceholderPropertyValues); // 属性占位符配置器 + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, AutoUpdateConfigChangeListener.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, FrameworkApolloAnnotationProcessor.class); //todo 扩展的Apollo 注解处理器 + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueDefinitionProcessor.class); + } + + private String[] resolveNamespaces(String[] namespaces) { + // no support for Spring version prior to 3.2.x, see https://github.com/apolloconfig/apollo/issues/4178 + if (this.environment == null) { + logNamespacePlaceholderNotSupportedMessage(namespaces); + return namespaces; + } + String[] resolvedNamespaces = new String[namespaces.length]; + for (int i = 0; i < namespaces.length; i++) { + // throw IllegalArgumentException if given text is null or if any placeholders are unresolvable + resolvedNamespaces[i] = this.environment.resolveRequiredPlaceholders(namespaces[i]); + } + return resolvedNamespaces; + } + + private String resolveSecret(String secret){ + if (this.environment == null) { + if (secret != null && secret.contains("${")) { + logger.warn("secret placeholder {} is not supported for Spring version prior to 3.2.x", secret); + } + return secret; + } + return this.environment.resolveRequiredPlaceholders(secret); + } + + private void logNamespacePlaceholderNotSupportedMessage(String[] namespaces) { + for (String namespace : namespaces) { + if (namespace.contains("${")) { + logger.warn("Namespace placeholder {} is not supported for Spring version prior to 3.2.x," + + " see https://github.com/apolloconfig/apollo/issues/4178 for more details.", + namespace); + break; + } + } + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + @Override + public void setEnvironment(Environment environment) { + + } +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloAutoConfiguration.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloAutoConfiguration.java new file mode 100644 index 0000000..db278ea --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloAutoConfiguration.java @@ -0,0 +1,26 @@ +package cn.lingniu.framework.plugin.apollo.init; + +import cn.lingniu.framework.plugin.apollo.extend.ApolloConfigChangeLogListener; +import cn.lingniu.framework.plugin.apollo.config.ApolloConfig; +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + + +@Slf4j +@Configuration +@EnableApolloConfig +@ConditionalOnProperty(prefix = ApolloConfig.PRE_FIX, name = "enabled", havingValue = "true") +@Import({ApolloConfigChangeLogListener.class}) +public class ApolloAutoConfiguration implements InitializingBean { + + @Override + public void afterPropertiesSet() { + log.info("Apollo configuration enabled(启动), meta URL: {}", PropertyUtils.getProperty("apollo.meta")); + } + +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloInit.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloInit.java new file mode 100644 index 0000000..8988937 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloInit.java @@ -0,0 +1,82 @@ +package cn.lingniu.framework.plugin.apollo.init; + + +import cn.lingniu.framework.plugin.apollo.config.ApolloConfig; +import cn.lingniu.framework.plugin.apollo.extend.ClassPoolUtils; +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.io.File; + + +/** + * apollo 初始化 + */ +@Slf4j +@Order(Integer.MIN_VALUE + 100) +public class ApolloInit implements ApplicationContextInitializer { + + + private String applicationName; + + private final static String AppId = "app.id"; + private final static String ApolloMeta = "apollo.meta"; + private final static String ApolloBootstrapEnabled = "apollo.bootstrap.enabled"; + private final static String ApolloBootstrapEagerLoadEnabled = "apollo.bootstrap.eagerLoad.enabled"; + private final static String UserDir = "user.dir"; + + public final static String NameSpaces = "framework.lingniu.apollo.namespaces"; + public final static String FrameworkMeta = "framework.lingniu.apollo.meta"; + public final static String FrameworkEnabled = "framework.lingniu.apollo.enabled"; + + + private final static String APOLLO_NAME = ApolloConfig.PRE_FIX + ".name"; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + applicationName = environment.getProperty(APOLLO_NAME, environment.getProperty(CommonConstant.SPRING_APP_NAME_KEY)); + String profile = environment.getProperty(CommonConstant.ACTIVE_PROFILES_PROPERTY); + this.initApolloConfig(environment, applicationName, profile); + } + + + void initApolloConfig(ConfigurableEnvironment environment, String appName, String profile) { + //优先原始配置 + if (!ObjectEmptyUtils.isEmpty(environment.getProperty(ApolloBootstrapEnabled)) + && !ObjectEmptyUtils.isEmpty(environment.getProperty(ApolloMeta)) + && !ObjectEmptyUtils.isEmpty(environment.getProperty(AppId))) { + return; + } + if (ObjectEmptyUtils.isEmpty(environment.getProperty(FrameworkEnabled)) || environment.getProperty(FrameworkEnabled).equalsIgnoreCase("false")) { + setDefaultProperty(ApolloBootstrapEnabled, "false"); + setDefaultProperty(ApolloBootstrapEagerLoadEnabled, "false"); + return; + } + PropertyUtils.setDefaultInitProperty("app.id", "test-ln"); +// PropertyUtils.setDefaultInitProperty("app.id", appName); + setDefaultProperty("apollo.meta", environment.getProperty(FrameworkMeta)); + setDefaultProperty("apollo.bootstrap.enabled", "true"); + setDefaultProperty("apollo.bootstrap.namespaces", environment.getProperty(NameSpaces, "application")); + setDefaultProperty("apollo.bootstrap.eagerLoad.enabled", "true"); + setDefaultProperty("spring.boot.enableautoconfiguration", "true"); + setDefaultProperty("env",profile.toUpperCase()); + setDefaultProperty("apollo.cache-dir", System.getProperty(UserDir) + File.separator + "apolloConfig" + File.separator); + + } + + void setDefaultProperty(String key, String defaultPropertyValue) { + PropertyUtils.setDefaultInitProperty(key, defaultPropertyValue); + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper new file mode 100644 index 0000000..6e69ca4 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.apollo.extend.FrameworkApolloConfigRegistrarHelper diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ConfigPropertySourcesProcessorHelper b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ConfigPropertySourcesProcessorHelper new file mode 100644 index 0000000..18b0ce2 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ConfigPropertySourcesProcessorHelper @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.apollo.extend.FramewokConfigPropertySourcesProcessorHelper diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d435288 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.context.ApplicationContextInitializer=\ +cn.lingniu.framework.plugin.apollo.init.ApolloInit \ No newline at end of file diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..95b4993 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.apollo.init.ApolloAutoConfiguration diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/apollo-config.md b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/apollo-config.md new file mode 100644 index 0000000..5e7dedc --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/apollo-config.md @@ -0,0 +1,37 @@ +# 【重要】apllo服务搭建、和详细demo使用教程---参考官网 + +## 概述 (Overview) + +1. 定位:基于 Apollo 封装的分布式配置管理中心组件 +2. 核心能力 + * 集中化配置管理:统一管理不同环境、不同集群的配置 + * 实时推送更新:配置变更后实时推送到应用端,无需重启服务 + * 多环境支持:支持开发、测试、生产等多套环境配置隔离 +3. 适用场景 + * 微服务架构下的配置管理 + * 多环境、多集群的应用配置管理 + * 需要动态调整配置参数的业务场 + * 对配置变更实时性要求较高的系统 + +## 如何配置--参考:ApolloConfig类 + +```yaml +framework: + lingniu: + apollo: + #namespaces必须配置,配置文件列表,以逗号分割 + namespaces: application,applicationTest + # 是否开启 Apollo 配置,默认true + enabled: true + # appId 等于 spring.application.name,无需配置 + appId: lingniu-framework-demo + # meta 地址,框架自动获取,无需配置 + meta: http://xxx:8180 +``` + +## 核心参数描述 + +- app.id:此参数未配置,默认采用:spring.application.name +- framework.lingniu.apollo.meta: 为apollo的eurka注册地址 +- framework.lingniu.apollo.enabled:是否启用 +- framework.lingniu.apollo.namespaces 可配置多个配置文件 \ No newline at end of file diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/pom.xml b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/pom.xml new file mode 100644 index 0000000..e343af6 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-mybatis + lingniu-framework-plugin-mybatis + http://maven.apache.org + + UTF-8 + 4.2.0 + + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-autoconfigure + + + org.bouncycastle + bcprov-jdk15on + + + org.springframework.boot + spring-boot-starter-jdbc + provided + + + org.springframework.boot + spring-boot-starter-aop + provided + + + + org.mybatis + mybatis-typehandlers-jsr310 + + + mysql + mysql-connector-java + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${dynamic-datasource-starter.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus-boot-starter.version} + + + + jakarta.servlet + jakarta.servlet-api + + + com.alibaba + druid-spring-boot-3-starter + + + com.github.pagehelper + pagehelper-spring-boot-starter + + + tk.mybatis + mapper-spring-boot-starter + ${tk.mapper-spring-boot.version} + + + mybatis-spring + org.mybatis + + + + + com.google.guava + guava + + + + diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DBSqlLogInterceptor.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DBSqlLogInterceptor.java new file mode 100644 index 0000000..55c4bc6 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DBSqlLogInterceptor.java @@ -0,0 +1,183 @@ +package cn.lingniu.framework.plugin.mybatis.base; + +import cn.lingniu.framework.plugin.mybatis.config.DataSourceConfig; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.SystemClock; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Plugin; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; +import org.apache.ibatis.session.ResultHandler; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +/** + * 输出SQL 语句及其执行时间 + */ +@Slf4j +@Intercepts({ + @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), + @Signature(type = StatementHandler.class, method = "update", args = Statement.class), + @Signature(type = StatementHandler.class, method = "batch", args = Statement.class) +}) +@RequiredArgsConstructor +public class DBSqlLogInterceptor implements Interceptor { + private static final String DRUID_POOLED_PREPARED_STATEMENT = "com.alibaba.druid.pool.DruidPooledPreparedStatement"; + private static final String T4C_PREPARED_STATEMENT = "oracle.jdbc.driver.T4CPreparedStatement"; + private static final String ORACLE_PREPARED_STATEMENT_WRAPPER = "oracle.jdbc.driver.OraclePreparedStatementWrapper"; + private Method oracleGetOriginalSqlMethod; + private Method druidGetSqlMethod; + private final DataSourceConfig dataSourceConfig; + + @Override + public Object intercept(Invocation invocation) throws Throwable { + if (!dataSourceConfig.isSqlLog()) { + return invocation.proceed(); + } + Statement statement; + Object firstArg = invocation.getArgs()[0]; + if (Proxy.isProxyClass(firstArg.getClass())) { + statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement"); + } else { + statement = (Statement) firstArg; + } + MetaObject stmtMetaObj = SystemMetaObject.forObject(statement); + try { + statement = (Statement) stmtMetaObj.getValue("stmt.statement"); + } catch (Exception e) { + // do nothing + } + if (stmtMetaObj.hasGetter("delegate")) {//Hikari + try { + statement = (Statement) stmtMetaObj.getValue("delegate"); + } catch (Exception ignored) { + // do nothing + } + } + String originalSql = null; + String stmtClassName = statement.getClass().getName(); + if (DRUID_POOLED_PREPARED_STATEMENT.equals(stmtClassName)) { + try { + if (druidGetSqlMethod == null) { + Class clazz = Class.forName(DRUID_POOLED_PREPARED_STATEMENT); + druidGetSqlMethod = clazz.getMethod("getSql"); + } + Object stmtSql = druidGetSqlMethod.invoke(statement); + if (stmtSql instanceof String) { + originalSql = (String) stmtSql; + } + } catch (Exception e) { + e.printStackTrace(); + } + } else if (T4C_PREPARED_STATEMENT.equals(stmtClassName) + || ORACLE_PREPARED_STATEMENT_WRAPPER.equals(stmtClassName)) { + try { + if (oracleGetOriginalSqlMethod != null) { + Object stmtSql = oracleGetOriginalSqlMethod.invoke(statement); + if (stmtSql instanceof String) { + originalSql = (String) stmtSql; + } + } else { + Class clazz = Class.forName(stmtClassName); + oracleGetOriginalSqlMethod = getMethodRegular(clazz, "getOriginalSql"); + if (oracleGetOriginalSqlMethod != null) { + //OraclePreparedStatementWrapper is not a public class, need set this. + oracleGetOriginalSqlMethod.setAccessible(true); + if (null != oracleGetOriginalSqlMethod) { + Object stmtSql = oracleGetOriginalSqlMethod.invoke(statement); + if (stmtSql instanceof String) { + originalSql = (String) stmtSql; + } + } + } + } + } catch (Exception e) { + //ignore + } + } + if (originalSql == null) { + originalSql = statement.toString(); + } + originalSql = originalSql.replaceAll("[\\s]+", StringPool.SPACE); + int index = indexOfSqlStart(originalSql); + if (index > 0) { + originalSql = originalSql.substring(index); + } + long start = SystemClock.now(); + Object result = invocation.proceed(); + long timing = SystemClock.now() - start; + Object target = PluginUtils.realTarget(invocation.getTarget()); + MetaObject metaObject = SystemMetaObject.forObject(target); + MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); + // 打印 sql + String sqlLogger = "\nsql日志执行日志:开始==============" + + "\nExecute ID :{}" + + "\nExecute SQL :{}" + + "\nExecute Time:{} ms" + + "\nsql日志执行日志:结束 ==============\n"; + if (timing > dataSourceConfig.getSqlErrorTimeOut()){ + log.error(sqlLogger, ms.getId(), originalSql, timing); + } else if (log.isInfoEnabled()) { + log.info(sqlLogger, ms.getId(), originalSql, timing); + } + return result; + } + + @Override + public Object plugin(Object target) { + if (target instanceof StatementHandler) { + return Plugin.wrap(target, this); + } + return target; + } + + @Override + public void setProperties(Properties properties) { + } + + private Method getMethodRegular(Class clazz, String methodName) { + if (Object.class.equals(clazz)) { + return null; + } + for (Method method : clazz.getDeclaredMethods()) { + if (method.getName().equals(methodName)) { + return method; + } + } + return getMethodRegular(clazz.getSuperclass(), methodName); + } + + private int indexOfSqlStart(String sql) { + String upperCaseSql = sql.toUpperCase(); + Set set = new HashSet<>(); + set.add(upperCaseSql.indexOf("SELECT ")); + set.add(upperCaseSql.indexOf("UPDATE ")); + set.add(upperCaseSql.indexOf("INSERT ")); + set.add(upperCaseSql.indexOf("DELETE ")); + set.remove(-1); + if (CollectionUtils.isEmpty(set)) { + return -1; + } + List list = new ArrayList<>(set); + list.sort(Comparator.naturalOrder()); + return list.get(0); + } + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DataSourceEnum.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DataSourceEnum.java new file mode 100644 index 0000000..bf42e2d --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DataSourceEnum.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.mybatis.base; + +/** + * 对应于多数据源中不同数据源配置 + *

+ * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。 + * 注意,默认是 {@link #MASTER} 数据源 + *

+ * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html + */ +public interface DataSourceEnum { + + /** + * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解 + */ + String MASTER = "master"; + /** + * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解 + */ + String SLAVE = "slave"; + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DruidAdRemoveFilter.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DruidAdRemoveFilter.java new file mode 100644 index 0000000..e406459 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DruidAdRemoveFilter.java @@ -0,0 +1,36 @@ +package cn.lingniu.framework.plugin.mybatis.base; + +import com.alibaba.druid.util.Utils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; + +/** + * Druid 底部广告过滤器 + * + */ +public class DruidAdRemoveFilter extends OncePerRequestFilter { + + /** + * common.js 的路径 + */ + private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取 common.js + String text = Utils.readFromResource(COMMON_JS_ILE_PATH); + // 正则替换 banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/config/DataSourceConfig.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/config/DataSourceConfig.java new file mode 100644 index 0000000..5a6f8c9 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/config/DataSourceConfig.java @@ -0,0 +1,34 @@ +package cn.lingniu.framework.plugin.mybatis.config; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 数据源配置--mybatis, mybatis-plus, dynamic-datasource, tk-mapper 配置参数整合 + **/ +@Data +@ConfigurationProperties(prefix = DataSourceConfig.PRE_FIX) +public class DataSourceConfig { + + public static final String PRE_FIX = "framework.lingniu.datasource"; + + /** + * sql分析日志打印, 默认关闭 + */ + private boolean sqlLog = false; + /** + * druid 账号, 默认 SpringApplicationConfig#name + */ + private String druidUsername; + /** + * druid 密码, 默认 SpringApplicationConfig#name + 123 + */ + private String druidPassword; + + /** + * 超过3s 的sql 打印error日志 + */ + private Long sqlErrorTimeOut = 3000l; + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceAutoConfiguration.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceAutoConfiguration.java new file mode 100644 index 0000000..e656eb1 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceAutoConfiguration.java @@ -0,0 +1,68 @@ +package cn.lingniu.framework.plugin.mybatis.init; + +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.mybatis.base.DBSqlLogInterceptor; +import cn.lingniu.framework.plugin.mybatis.base.DruidAdRemoveFilter; +import cn.lingniu.framework.plugin.mybatis.config.DataSourceConfig; + +import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.support.jakarta.ResourceServlet; +import com.alibaba.druid.support.jakarta.StatViewServlet; +import com.alibaba.druid.support.jakarta.WebStatFilter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Optional; + +@Configuration +@EnableConfigurationProperties({DataSourceConfig.class}) +public class DataSourceAutoConfiguration { + + @Bean + public DBSqlLogInterceptor sqlLogInterceptor(DataSourceConfig dataSourceConfig) { + return new DBSqlLogInterceptor(dataSourceConfig); + } + + @Bean(name = "druidStatView") + public ServletRegistrationBean druidStatView(DataSourceConfig dataSourceConfig) { + ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); + registrationBean.addInitParameter(ResourceServlet.PARAM_NAME_ALLOW, ""); + registrationBean.addInitParameter(StatViewServlet.PARAM_NAME_DENY, ""); + registrationBean.addInitParameter(StatViewServlet.PARAM_NAME_USERNAME, Optional.ofNullable(dataSourceConfig.getDruidUsername()).orElseGet(()-> ApplicationNameContext.getApplicationName())); + registrationBean.addInitParameter(StatViewServlet.PARAM_NAME_PASSWORD, Optional.ofNullable(dataSourceConfig.getDruidPassword()).orElseGet(() -> ApplicationNameContext.getApplicationName() + "123")); + registrationBean.addInitParameter(StatViewServlet.PARAM_NAME_RESET_ENABLE, "false"); + return registrationBean; + } + + @Bean(name = "druidWebStatFilter") + public FilterRegistrationBean druidWebStatFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new WebStatFilter()); + registrationBean.addUrlPatterns("/*"); + registrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"); + return registrationBean; + } + + + /** + * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 + */ + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true") + public FilterRegistrationBean druidAdRemoveFilterFilter(DruidStatProperties properties) { + // 获取 druid web 监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取 common.js 的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + // 创建 DruidAdRemoveFilter Bean + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new DruidAdRemoveFilter()); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceInit.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceInit.java new file mode 100644 index 0000000..4ca6c8b --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceInit.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.mybatis.init; + +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.Order; + +/** + * 数据源属性初始化--处理监控开关 + */ +@Order(Integer.MIN_VALUE + 100) +public class DataSourceInit implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + PropertyUtils.setDefaultInitProperty("management.health.db.enabled", "false"); + } + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ff6d697 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.context.ApplicationContextInitializer=\ +cn.lingniu.framework.plugin.mybatis.init.DataSourceInit \ No newline at end of file diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..407a603 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.mybatis.init.DataSourceAutoConfiguration diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/mybatis-config.md b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/mybatis-config.md new file mode 100644 index 0000000..4692f8c --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/mybatis-config.md @@ -0,0 +1,65 @@ +# 【重要】基于 Mybatis-Plus + dynamic-datasource + tk 详细使用参考官网---参考官网 + +## 概述 (Overview) + +1. 基于 Mybatis-Plus + dynamic-datasource + tk 封装的数据源交互工具 +2. 核心能力 + * 数据源CRUD操作,支持mysql, oracle, clickhouse 等 + * 多数据源支持 + * 分页查询 + * Sql语句执行分析 + * 默认集成druid +3. 适用场景: + * 依赖外部数据源场景 + * 关系型数据库场景 + +## 如何配置--更多参数参考:RedissonConfig/RedissonProperties + +```yaml +spring: + application: + name: lingniu-framework-demo + profiles: + active: dev,redisson,jetcache,xxljob,db + #todo db配置 + datasource: + dynamic: + primary: master + datasource: + #如需连接mysql,需修改下面的配置 + master: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: sa + password: sa + slaver: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: sa + password: sa + # http://localhost:8080/h2-console 访问 h2 数据库 账号: sa/sa + h2: + console: + enabled: true + path: /h2-console +framework: + lingniu: + # 框架数据源配置 + datasource: + # 开启框架 sql执行分析 + sqlLog: true + # druid 后台地址:http://localhost:8080/druid/login.html + # druid 账号,默认值为 SpringApplicationProperties#name + druidUsername: "your_druid_username" + # druid 密码,默认值为 SpringApplicationProperties#name + "123" + druidPassword: "your_druid_password" +``` + +## druid 配置请参考:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter + +## 如何使用--Mybatis基本功能 + +1. 基本功能使用可参考Mybatis-Plus: https://www.baomidou.com +2. 默认配置下 在 src/main/resources/mapper/ 文件夹下定义 xml 文件 +3. 定义 java 接口 继承 com.baomidou.mybatisplus.core.mapper.BaseMapper +4. 使用@DS 注解实现数据源切换:@DS("master") diff --git a/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/pom.xml b/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/pom.xml new file mode 100644 index 0000000..0e97038 --- /dev/null +++ b/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-easyexcel + ${project.artifactId} + + + + com.alibaba + easyexcel + + + + diff --git a/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/src/main/resources/file.md b/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/src/main/resources/file.md new file mode 100644 index 0000000..f49b36b --- /dev/null +++ b/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/src/main/resources/file.md @@ -0,0 +1,3 @@ +# easyexcel * easyexcel 性能好,api简单,上手更简单 + +# https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/pom.xml b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/pom.xml new file mode 100644 index 0000000..0c14354 --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-xxljob + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + org.springframework.boot + spring-boot-autoconfigure + + + com.xuxueli + xxl-job-core + + + com.alibaba + fastjson + + + + + diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/config/XxlJobConfig.java b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/config/XxlJobConfig.java new file mode 100644 index 0000000..cdff222 --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/config/XxlJobConfig.java @@ -0,0 +1,64 @@ +package cn.lingniu.framework.plugin.xxljob.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(XxlJobConfig.PRE_FIX) +public class XxlJobConfig { + + public final static String PRE_FIX = "framework.lingniu.xxljob"; + + /** + * xxl job admin properties. + */ + private AdminProperties admin = new AdminProperties(); + /** + * xxl job executor properties. + */ + private ExecutorProperties executor = new ExecutorProperties(); + + + /** + * xxl-job admin properties. + */ + @Data + static public class AdminProperties { + /** + * xxl job admin address. + */ + private String adminAddresses = "http://xxx:8080/xxl-job-admin"; + /** + * xxl job admin registry access token. + */ + private String accessToken; + } + + /** + * xxl-job executor properties. + */ + @Data + static public class ExecutorProperties { + /** + * xxl job registry name. [等于 spring.application.name] ApplicationNameContext.getApplicationName() + */ + private String appName = "xxl-job-executor"; + /** + * xxl job registry ip. + */ + private String ip; + /** + * xxl job registry port. + */ + private Integer port = 9999; + /** + * xxl job log files path. todo 注意权限问题 + */ + private String logPath = "logs/applogs/xxl-job/jobhandler"; + /** + * xxl job log files retention days. + */ + private Integer logRetentionDays = 7; + } + +} diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/init/XxlJobAutoConfiguration.java b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/init/XxlJobAutoConfiguration.java new file mode 100644 index 0000000..783daca --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/init/XxlJobAutoConfiguration.java @@ -0,0 +1,41 @@ +package cn.lingniu.framework.plugin.xxljob.init; + +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.util.json.JsonUtil; +import cn.lingniu.framework.plugin.xxljob.config.XxlJobConfig; +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Slf4j +@EnableConfigurationProperties({XxlJobConfig.class}) +public class XxlJobAutoConfiguration { + + @Bean(initMethod = "start", destroyMethod = "destroy") + public XxlJobSpringExecutor xxlJobSpringExecutor(XxlJobConfig prop) { + validateXxlJobConfig(prop); + + XxlJobSpringExecutor executor = new XxlJobSpringExecutor(); + executor.setAdminAddresses(prop.getAdmin().getAdminAddresses()); + executor.setAccessToken(prop.getAdmin().getAccessToken()); + + executor.setPort(prop.getExecutor().getPort()); + executor.setLogRetentionDays(prop.getExecutor().getLogRetentionDays()); + executor.setLogPath(prop.getExecutor().getLogPath()); + executor.setIp(prop.getExecutor().getIp()); + executor.setAppname(ApplicationNameContext.getApplicationName()); + + log.info("XxlJob configuration initialized: {}", JsonUtil.bean2Json(prop)); + return executor; + } + + private void validateXxlJobConfig(XxlJobConfig prop) { + if (prop.getAdmin() == null || prop.getExecutor() == null) { + throw new IllegalArgumentException("XxlJob configuration cannot be null"); + } + } + +} diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..2812eac --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.xxljob.init.XxlJobAutoConfiguration \ No newline at end of file diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/xxl-job.md b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/xxl-job.md new file mode 100644 index 0000000..5ec0eed --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/xxl-job.md @@ -0,0 +1,43 @@ +# 【重要】xxl-job服务搭建、和详细demo使用教程---参考官网 + +## 概述 (Overview) + +1. 定位: 基于 xxljob 封装的分布式定时任务开发工具 +2. 核心能力: + * 分布式任务调度:基于 XXL-JOB 实现任务的分布式执行和分片处理。 + * 动态调度:支持任务的动态添加、修改和删除,无需重启服务。 + * 故障转移:自动检测任务执行失败并重新分配,确保任务高可用。 + * 监控与管理:提供任务执行日志、状态监控和告警功能。 + * 插件化集成:通过插件机制与主框架无缝对接,支持动态加载和卸载。 +3. 适用场景: + * 定时任务:如报表生成、数据清理等周期性任务。 + * 分布式计算:需要分片处理大量数据的场景。 + * 高可用需求:确保任务在节点故障时自动恢复。 + * 动态调整:需要灵活调整任务执行策略的场景。 + +## 如何配置--参考:XxlJobConfig + +```yaml + +framework: + lingniu: + # XXL-JOB 配置 + xxljob: + # 调度中心配置 + admin: + # 调度中心部署根地址(多个地址用逗号分隔,为空则关闭自动注册) + adminAddresses: "http://xxx:8099/xxl-job-admin" + # 调度中心通讯TOKEN(非空时启用) + accessToken: "default_token" + + # 执行器配置 + executor: + # 执行器IP(默认为空表示自动获取IP) + ip: "" + # 执行器端口号(小于等于0则自动获取,默认9999) + port: 9999 + # 执行器日志文件保存天数(大于等于3时生效,-1表示关闭自动清理,默认7天) + logRetentionDays: 7 + # 执行器运行日志文件存储磁盘路径(需对该路径拥有读写权限,为空则使用默认路径) + logPath: logs/applogs/xxl-job/jobhandler +``` \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/pom.xml b/lingniu-framework-plugin/lingniu-framework-plugin-core/pom.xml new file mode 100644 index 0000000..fdd3382 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-core + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + joda-time + joda-time + + + org.db4j + reflectasm + + + org.springframework.boot + spring-boot + compile + + + com.fasterxml.jackson.core + jackson-databind + compile + + + org.slf4j + slf4j-api + + + + org.apache.commons + commons-lang3 + + + commons-collections + commons-collections + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + compile + true + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + compile + + + jakarta.servlet + jakarta.servlet-api + + + org.springframework.boot + spring-boot-autoconfigure + compile + true + + + + + diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/CommonResult.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/CommonResult.java new file mode 100644 index 0000000..314e6a7 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/CommonResult.java @@ -0,0 +1,121 @@ +package cn.lingniu.framework.plugin.core.base; + +import cn.hutool.core.lang.Assert; +import cn.lingniu.framework.plugin.core.exception.ErrorCode; +import cn.lingniu.framework.plugin.core.exception.ServiceException; +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import cn.lingniu.framework.plugin.core.exception.util.ServiceExceptionUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + * + * @see ErrorCode#getCode() + */ + private Integer code; + /** + * 错误提示,用户可阅读 + * + * @see ErrorCode#getMsg() () + */ + private String msg; + /** + * 返回数据 + */ + private T data; + + /** + * 将传入的 result 对象,转换成另外一个泛型结果的对象 + * + * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 + * + * @param result 传入的 result 对象 + * @param 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + public static CommonResult error(ErrorCode errorCode, Object... params) { + Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = errorCode.getCode(); + result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params); + return result; + } + + public static CommonResult error(ErrorCode errorCode) { + return error(errorCode.getCode(), errorCode.getMsg()); + } + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); + result.data = data; + result.msg = ""; + return result; + } + + public static boolean isSuccess(Integer code) { + return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isSuccess() { + return isSuccess(code); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isError() { + return !isSuccess(); + } + + // ========= 和 Exception 异常体系集成 ========= + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + */ + public void checkError() throws ServiceException { + if (isSuccess()) { + return; + } + // 业务异常 + throw new ServiceException(code, msg); + } + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + * 如果没有,则返回 {@link #data} 数据 + */ + @JsonIgnore // 避免 jackson 序列化 + public T getCheckedData() { + checkError(); + return data; + } + + public static CommonResult error(ServiceException serviceException) { + return error(serviceException.getCode(), serviceException.getMessage()); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/TerminalEnum.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/TerminalEnum.java new file mode 100644 index 0000000..85c3519 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/TerminalEnum.java @@ -0,0 +1,38 @@ +package cn.lingniu.framework.plugin.core.base; + +import cn.lingniu.framework.plugin.util.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 终端的枚举 + */ +@RequiredArgsConstructor +@Getter +public enum TerminalEnum implements ArrayValuable { + + UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它 + WECHAT_MINI_PROGRAM(10, "微信小程序"), + WECHAT_WAP(11, "微信公众号"), + H5(20, "H5 网页"), + APP(31, "手机 App"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(TerminalEnum::getTerminal).toArray(Integer[]::new); + + /** + * 终端 + */ + private final Integer terminal; + /** + * 终端名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/WebFilterOrderEnum.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/WebFilterOrderEnum.java new file mode 100644 index 0000000..767702b --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/WebFilterOrderEnum.java @@ -0,0 +1,35 @@ +package cn.lingniu.framework.plugin.core.base; + +/** + * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 + * + * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下 + * + */ +public interface WebFilterOrderEnum { + + int CORS_FILTER = Integer.MIN_VALUE; + + int TRACE_FILTER = CORS_FILTER + 1; + + int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; + + int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1; + + // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 + + int FAVICON_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 + + int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 + + int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面 + + // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类 + + int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面 + + int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面 + + int DEMO_FILTER = Integer.MAX_VALUE; + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/config/CommonConstant.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/config/CommonConstant.java new file mode 100644 index 0000000..c3400dd --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/config/CommonConstant.java @@ -0,0 +1,16 @@ +package cn.lingniu.framework.plugin.core.config; + +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@UtilityClass +public class CommonConstant { + //Spring 应用名 + public static final String SPRING_APP_NAME_KEY = "spring.application.name"; + //active profiles + public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active"; + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/ApplicationNameContext.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/ApplicationNameContext.java new file mode 100644 index 0000000..6e8dfec --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/ApplicationNameContext.java @@ -0,0 +1,15 @@ +package cn.lingniu.framework.plugin.core.context; + +public abstract class ApplicationNameContext { + + private static String applicationName; + + public static String getApplicationName() { + return applicationName; + } + + public static void setApplicationName(String applicationName) { + ApplicationNameContext.applicationName = applicationName; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/SpringBeanApplicationContext.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/SpringBeanApplicationContext.java new file mode 100644 index 0000000..f11cda7 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/SpringBeanApplicationContext.java @@ -0,0 +1,60 @@ +package cn.lingniu.framework.plugin.core.context; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider;import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.SpringBootVersion; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import java.util.Map; + +/** + * spring bean 的所有bean存储 + */ +@Slf4j +public class SpringBeanApplicationContext implements ApplicationContextAware { + + private static ApplicationContext applicationContext = null; + + private static final String SPRING_BOOT_VERSION = SpringBootVersion.getVersion(); + + + private static String applicationName; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringBeanApplicationContext.applicationContext = applicationContext; + } + + public static Object getBean(String beanName) throws BeansException { + return applicationContext.getBean(beanName); + } + + public static T getBean(Class clazz) throws BeansException { + return applicationContext.getBean(clazz); + } + + public static String getSpringBootVersion() { + return SPRING_BOOT_VERSION; + } + + public static ObjectProvider getBeanProvider(Class clazz) throws BeansException { + return applicationContext.getBeanProvider(clazz); + } + + public static T getBean(String beanName, Class clazz) throws BeansException { + return applicationContext.getBean(beanName, clazz); + } + + public static Map getBeans(Class clazz) throws BeansException { + return applicationContext.getBeansOfType(clazz); + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ErrorCode.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ErrorCode.java new file mode 100644 index 0000000..566b512 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ErrorCode.java @@ -0,0 +1,30 @@ +package cn.lingniu.framework.plugin.core.exception; + +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import cn.lingniu.framework.plugin.core.exception.enums.ServiceErrorCodeRange; +import lombok.Data; + +/** + * 错误码对象 + * + * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} + * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} + */ +@Data +public class ErrorCode { + + /** + * 错误码 + */ + private final Integer code; + /** + * 错误提示 + */ + private final String msg; + + public ErrorCode(Integer code, String message) { + this.code = code; + this.msg = message; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServerException.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServerException.java new file mode 100644 index 0000000..5fb7d1c --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServerException.java @@ -0,0 +1,60 @@ +package cn.lingniu.framework.plugin.core.exception; + +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 服务器异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServerException extends RuntimeException { + + /** + * 全局错误码 + * + * @see GlobalErrorCodeConstants + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServerException() { + } + + public ServerException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServerException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServerException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServerException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServiceException.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServiceException.java new file mode 100644 index 0000000..ed49df3 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServiceException.java @@ -0,0 +1,60 @@ +package cn.lingniu.framework.plugin.core.exception; + +import cn.lingniu.framework.plugin.core.exception.enums.ServiceErrorCodeRange; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务逻辑异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServiceException extends RuntimeException { + + /** + * 业务错误码 + * + * @see ServiceErrorCodeRange + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() { + } + + public ServiceException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServiceException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServiceException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServiceException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/GlobalErrorCodeConstants.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/GlobalErrorCodeConstants.java new file mode 100644 index 0000000..d0c2b82 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/GlobalErrorCodeConstants.java @@ -0,0 +1,39 @@ +package cn.lingniu.framework.plugin.core.exception.enums; + + +import cn.lingniu.framework.plugin.core.exception.ErrorCode; + +/** + * 全局错误码枚举 + * 0-999 系统异常编码保留 + * + * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status + * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 + * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 + */ +public interface GlobalErrorCodeConstants { + + ErrorCode SUCCESS = new ErrorCode(0, "成功"); + + // ========== 客户端错误段 ========== + + ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); + ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); + ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); + ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); + ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); + ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 + ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); + ErrorCode TIME_OUT_ERROR = new ErrorCode(430, "请求超时!"); + + // ========== 服务端错误段 ========== + + ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); + ErrorCode BAD_GATE_WAY_ERROR = new ErrorCode(502, "服务端Bad Gateway异常"); + + // ========== 自定义错误段 ========== + + ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); + ErrorCode FEIGN_DECODE_ERROR = new ErrorCode(900, "Feign异常解码错误"); + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/ServiceErrorCodeRange.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/ServiceErrorCodeRange.java new file mode 100644 index 0000000..b2f7515 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/ServiceErrorCodeRange.java @@ -0,0 +1,11 @@ +package cn.lingniu.framework.plugin.core.exception.enums; + +/** + * todo 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 + * 抽象类,业务可自定义继承 + */ +public abstract class ServiceErrorCodeRange { + + + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/util/ServiceExceptionUtil.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/util/ServiceExceptionUtil.java new file mode 100644 index 0000000..e268ba6 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/util/ServiceExceptionUtil.java @@ -0,0 +1,76 @@ +package cn.lingniu.framework.plugin.core.exception.util; + +import cn.lingniu.framework.plugin.core.exception.ErrorCode; +import cn.lingniu.framework.plugin.core.exception.ServiceException; +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +/** + * {@link ServiceException} 工具类 + * + * 目的在于,格式化异常信息提示。 + * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 + */ +@Slf4j +public class ServiceExceptionUtil { + + // ========== 和 ServiceException 的集成 ========== + + public static ServiceException exception(ErrorCode errorCode) { + return exception0(errorCode.getCode(), errorCode.getMsg()); + } + + public static ServiceException exception(ErrorCode errorCode, Object... params) { + return exception0(errorCode.getCode(), errorCode.getMsg(), params); + } + + public static ServiceException exception0(Integer code, String messagePattern, Object... params) { + String message = doFormat(code, messagePattern, params); + return new ServiceException(code, message); + } + + public static ServiceException invalidParamException(String messagePattern, Object... params) { + return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params); + } + + // ========== 格式化方法 ========== + + /** + * 将错误编号对应的消息使用 params 进行格式化。 + * + * @param code 错误编号 + * @param messagePattern 消息模版 + * @param params 参数 + * @return 格式化后的提示 + */ + @VisibleForTesting + public static String doFormat(int code, String messagePattern, Object... params) { + StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); + int i = 0; + int j; + int l; + for (l = 0; l < params.length; l++) { + j = messagePattern.indexOf("{}", i); + if (j == -1) { + log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + if (i == 0) { + return messagePattern; + } else { + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + } else { + sbuf.append(messagePattern, i, j); + sbuf.append(params[l]); + i = j + 2; + } + } + if (messagePattern.indexOf("{}", i) != -1) { + log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + } + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/FrameworkSystemInitContextInitializer.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/FrameworkSystemInitContextInitializer.java new file mode 100644 index 0000000..ca3e892 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/FrameworkSystemInitContextInitializer.java @@ -0,0 +1,43 @@ +package cn.lingniu.framework.plugin.core.init; + +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * 优先级最高 + */ +@Configuration +@Slf4j +public class FrameworkSystemInitContextInitializer implements EnvironmentPostProcessor, Ordered { + + private static boolean initialized = false; + + private final static String APPLICATION_NAME = CommonConstant.SPRING_APP_NAME_KEY; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (initialized) { + return; + } + initialized = true; + String applicationName = environment.getProperty(APPLICATION_NAME, ""); + if (ObjectEmptyUtils.isEmpty(applicationName)) { + throw new IllegalArgumentException("[框架异常][" + CommonConstant.SPRING_APP_NAME_KEY + "] is not set-必须配置!"); + } else { + ApplicationNameContext.setApplicationName(applicationName); + } + } + + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE - 100000; + } +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextCondition.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextCondition.java new file mode 100644 index 0000000..02361b7 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextCondition.java @@ -0,0 +1,16 @@ +package cn.lingniu.framework.plugin.core.init.applicationContext; + +import cn.lingniu.framework.plugin.core.context.SpringBeanApplicationContext; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + + +public class SpringApplicationContextCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return SpringBeanApplicationContext.getApplicationContext() == null; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextConfiguration.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextConfiguration.java new file mode 100644 index 0000000..3534c90 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextConfiguration.java @@ -0,0 +1,25 @@ +package cn.lingniu.framework.plugin.core.init.applicationContext; + +import cn.lingniu.framework.plugin.core.context.SpringBeanApplicationContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +@Configuration +public class SpringApplicationContextConfiguration implements EnvironmentAware { + + @Bean + @Conditional(SpringApplicationContextCondition.class) + public SpringBeanApplicationContext springApplicationContext(ApplicationContext applicationContext) { + SpringBeanApplicationContext context = new SpringBeanApplicationContext(); + context.setApplicationContext(applicationContext); + return context; + } + + @Override + public void setEnvironment(Environment event) { + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..13044ad --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ +cn.lingniu.framework.plugin.core.init.FrameworkSystemInitContextInitializer + + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.core.init.applicationContext.SpringApplicationContextConfiguration \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/core.md b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/core.md new file mode 100644 index 0000000..9391c8c --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/core.md @@ -0,0 +1,7 @@ +# 框架核心模块 + +## ApplicationNameContext 初始化 + +## 异常定义 + +## 等 \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/pom.xml b/lingniu-framework-plugin/lingniu-framework-plugin-util/pom.xml new file mode 100644 index 0000000..8128317 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/pom.xml @@ -0,0 +1,109 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-util + ${project.artifactId} + + + + + org.springframework.boot + spring-boot + + + org.springframework + spring-webmvc + compile + + + org.aspectj + aspectjweaver + provided + + + org.springframework + spring-core + + + org.springframework + spring-aop + + + org.springframework.boot + spring-boot-autoconfigure + compile + true + + + com.google.guava + guava + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.apache.commons + commons-lang3 + + + commons-codec + commons-codec + + + cn.hutool + hutool-core + + + org.hibernate.validator + hibernate-validator + + + joda-time + joda-time + + + org.slf4j + slf4j-api + + + jakarta.servlet + jakarta.servlet-api + + + cn.hutool + hutool-all + + + + diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/cache/CacheUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/cache/CacheUtils.java new file mode 100644 index 0000000..2654297 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/cache/CacheUtils.java @@ -0,0 +1,59 @@ +package cn.lingniu.framework.plugin.util.cache; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import java.time.Duration; +import java.util.concurrent.Executors; + +/** + * Cache 工具类 + */ +public class CacheUtils { + + /** + * 异步刷新的 LoadingCache 最大缓存数量 + * + * @see 本地缓存 CacheUtils 工具类建议 + */ + private static final Integer CACHE_MAX_SIZE = 10000; + + /** + * 构建异步刷新的 LoadingCache 对象 + * + * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法 + * + * 或者简单理解: + * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法 + * 2、和“全局”、“系统”相关的,使用当前缓存方法 + * + * @param duration 过期时间 + * @param loader CacheLoader 对象 + * @return LoadingCache 对象 + */ + public static LoadingCache buildAsyncReloadingCache(Duration duration, CacheLoader loader) { + return CacheBuilder.newBuilder() + .maximumSize(CACHE_MAX_SIZE) + // 只阻塞当前数据加载线程,其他线程返回旧值 + .refreshAfterWrite(duration) + // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 + .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); + } + + /** + * 构建同步刷新的 LoadingCache 对象 + * + * @param duration 过期时间 + * @param loader CacheLoader 对象 + * @return LoadingCache 对象 + */ + public static LoadingCache buildCache(Duration duration, CacheLoader loader) { + return CacheBuilder.newBuilder() + .maximumSize(CACHE_MAX_SIZE) + // 只阻塞当前数据加载线程,其他线程返回旧值 + .refreshAfterWrite(duration) + .build(loader); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/ArrayUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/ArrayUtils.java new file mode 100644 index 0000000..afe83b1 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/ArrayUtils.java @@ -0,0 +1,54 @@ +package cn.lingniu.framework.plugin.util.collection; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.util.ArrayUtil; + +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Array 工具类 + */ +public class ArrayUtils { + + /** + * 将 object 和 newElements 合并成一个数组 + * + * @param object 对象 + * @param newElements 数组 + * @param 泛型 + * @return 结果数组 + */ + @SafeVarargs + public static Consumer[] append(Consumer object, Consumer... newElements) { + if (object == null) { + return newElements; + } + Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); + result[0] = object; + System.arraycopy(newElements, 0, result, 1, newElements.length); + return result; + } + + public static V[] toArray(Collection from, Function mapper) { + return toArray(CollectionUtils.convertList(from, mapper)); + } + + @SuppressWarnings("unchecked") + public static T[] toArray(Collection from) { + if (CollectionUtil.isEmpty(from)) { + return (T[]) (new Object[0]); + } + return ArrayUtil.toArray(from, (Class) IterUtil.getElementType(from.iterator())); + } + + public static T get(T[] array, int index) { + if (null == array || index >= array.length) { + return null; + } + return array[index]; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/CollectionUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/CollectionUtils.java new file mode 100644 index 0000000..ae6a29b --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/CollectionUtils.java @@ -0,0 +1,358 @@ +package cn.lingniu.framework.plugin.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static cn.hutool.core.convert.Convert.toCollection; +import static java.util.Arrays.asList; + +/** + * Collection 工具类 + */ +public class CollectionUtils { + + public static boolean containsAny(Object source, Object... targets) { + return asList(targets).contains(source); + } + + public static boolean isAnyEmpty(Collection... collections) { + return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); + } + + public static boolean anyMatch(Collection from, Predicate predicate) { + return from.stream().anyMatch(predicate); + } + + public static List filterList(Collection from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(predicate).collect(Collectors.toList()); + } + + public static List distinct(Collection from, Function keyMapper) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return distinct(from, keyMapper, (t1, t2) -> t1); + } + + public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); + } + + public static List convertList(T[] from, Function func) { + if (ArrayUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return convertList(Arrays.asList(from), func); + } + + public static List convertList(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertList(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertListByFlatMap(Collection from, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertListByFlatMap(Collection from, + Function mapper, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List mergeValuesFromMap(Map> map) { + return map.values() + .stream() + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + public static Set convertSet(Collection from) { + return convertSet(from, v -> v); + } + + public static Set convertSet(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Set convertSet(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Map convertMapByFilter(Collection from, Predicate filter, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v)); + } + + public static Set convertSetByFlatMap(Collection from, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Set convertSetByFlatMap(Collection from, + Function mapper, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Map convertMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, Function.identity()); + } + + public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, Function.identity(), supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream() + .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); + } + + // 暂时没想好名字,先以 2 结尾噶 + public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); + } + + public static Map convertImmutableMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return Collections.emptyMap(); + } + ImmutableMap.Builder builder = ImmutableMap.builder(); + from.forEach(item -> builder.put(keyFunc.apply(item), item)); + return builder.build(); + } + + /** + * 对比老、新两个列表,找出新增、修改、删除的数据 + * + * @param oldList 老列表 + * @param newList 新列表 + * @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同 + * 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据 + * @return [新增列表、修改列表、删除列表] + */ + public static List> diffList(Collection oldList, Collection newList, + BiFunction sameFunc) { + List createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除 + List updateList = new ArrayList<>(); + List deleteList = new ArrayList<>(); + + // 通过以 oldList 为主遍历,找出 updateList 和 deleteList + for (T oldObj : oldList) { + // 1. 寻找是否有匹配的 + T foundObj = null; + for (Iterator iterator = createList.iterator(); iterator.hasNext(); ) { + T newObj = iterator.next(); + // 1.1 不匹配,则直接跳过 + if (!sameFunc.apply(oldObj, newObj)) { + continue; + } + // 1.2 匹配,则移除,并结束寻找 + iterator.remove(); + foundObj = newObj; + break; + } + // 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中 + if (foundObj != null) { + updateList.add(foundObj); + } else { + deleteList.add(oldObj); + } + } + return asList(createList, updateList, deleteList); + } + + public static boolean containsAny(Collection source, Collection candidates) { + return org.springframework.util.CollectionUtils.containsAny(source, candidates); + } + + public static T getFirst(List from) { + return !CollectionUtil.isEmpty(from) ? from.get(0) : null; + } + + public static T findFirst(Collection from, Predicate predicate) { + return findFirst(from, predicate, Function.identity()); + } + + public static U findFirst(Collection from, Predicate predicate, Function func) { + if (CollUtil.isEmpty(from)) { + return null; + } + return from.stream().filter(predicate).findFirst().map(func).orElse(null); + } + + public static > V getMaxValue(Collection from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert !from.isEmpty(); // 断言,避免告警 + T t = from.stream().max(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > V getMinValue(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + T t = from.stream().min(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > T getMinObject(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + return from.stream().min(Comparator.comparing(valueFunc)).get(); + } + + public static > V getSumValue(Collection from, Function valueFunc, + BinaryOperator accumulator) { + return getSumValue(from, valueFunc, accumulator, null); + } + + public static > V getSumValue(Collection from, Function valueFunc, + BinaryOperator accumulator, V defaultValue) { + if (CollUtil.isEmpty(from)) { + return defaultValue; + } + assert !from.isEmpty(); // 断言,避免告警 + return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue); + } + + public static void addIfNotNull(Collection coll, T item) { + if (item == null) { + return; + } + coll.add(item); + } + + public static Collection singleton(T obj) { + return obj == null ? Collections.emptyList() : Collections.singleton(obj); + } + + public static List newArrayList(List> list) { + return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList()); + } + + /** + * 转换为 LinkedHashSet + * + * @param 元素类型 + * @param elementType 集合中元素类型 + * @param value 被转换的值 + * @return {@link LinkedHashSet} + */ + @SuppressWarnings("unchecked") + public static LinkedHashSet toLinkedHashSet(Class elementType, Object value) { + return (LinkedHashSet) toCollection(LinkedHashSet.class, elementType, value); + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/MapUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/MapUtils.java new file mode 100644 index 0000000..f196eb1 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/MapUtils.java @@ -0,0 +1,58 @@ +package cn.lingniu.framework.plugin.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjUtil; +import com.google.common.collect.Multimap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Map 工具类 + */ +public class MapUtils { + + /** + * 从哈希表表中,获得 keys 对应的所有 value 数组 + * + * @param multimap 哈希表 + * @param keys keys + * @return value 数组 + */ + public static List getList(Multimap multimap, Collection keys) { + List result = new ArrayList<>(); + keys.forEach(k -> { + Collection values = multimap.get(k); + if (CollectionUtil.isEmpty(values)) { + return; + } + result.addAll(values); + }); + return result; + } + + /** + * 从哈希表查找到 key 对应的 value,然后进一步处理 + * key 为 null 时, 不处理 + * 注意,如果查找到的 value 为 null 时,不进行处理 + * + * @param map 哈希表 + * @param key key + * @param consumer 进一步处理的逻辑 + */ + public static void findAndThen(Map map, K key, Consumer consumer) { + if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) { + return; + } + V value = map.get(key); + if (value == null) { + return; + } + consumer.accept(value); + } + + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/SetUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/SetUtils.java new file mode 100644 index 0000000..42f3b72 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/SetUtils.java @@ -0,0 +1,17 @@ +package cn.lingniu.framework.plugin.util.collection; + +import cn.hutool.core.collection.CollUtil; + +import java.util.Set; + +/** + * Set 工具类 + */ +public class SetUtils { + + @SafeVarargs + public static Set asSet(T... objs) { + return CollUtil.newHashSet(objs); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/ContextUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/ContextUtils.java new file mode 100644 index 0000000..7cecc7e --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/ContextUtils.java @@ -0,0 +1,63 @@ +package cn.lingniu.framework.plugin.util.config; + +import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * 获取上下文 todo 得设置 setApplicationContext + **/ +public class ContextUtils { + public static Class MainClass; + + private static ConfigurableApplicationContext applicationContext; + + public static void setApplicationContext(ConfigurableApplicationContext applicationContext) { + ContextUtils.applicationContext = applicationContext; + } + + public static ConfigurableApplicationContext getApplicationContext() { + return applicationContext; + } + + public static T getBean(Class type, boolean required) { + if (type != null && applicationContext != null) { + if (required) { + return applicationContext.getBean(type); + } else { + if (applicationContext.getBeansOfType(type).size() > 0) { + return applicationContext.getBean(type); + } + + } + } + return null; + } + + public static Object getBean(String type, boolean required) { + if (type != null && applicationContext != null) { + if (required) { + return applicationContext.getBean(type); + } else { + if (applicationContext.containsBean(type)) { + return applicationContext.getBean(type); + } + + } + } + return null; + } + + public static ConfigurableWebServerApplicationContext getConfigurableWebServerApplicationContext() { + ApplicationContext context = ContextUtils.getApplicationContext(); + if (context != null && context instanceof ConfigurableWebServerApplicationContext) { + return (ConfigurableWebServerApplicationContext) context; + } + return null; + } + + public static boolean isWeb() { + return getConfigurableWebServerApplicationContext() != null; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/PropertyUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/PropertyUtils.java new file mode 100644 index 0000000..900e66b --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/PropertyUtils.java @@ -0,0 +1,43 @@ +package cn.lingniu.framework.plugin.util.config; + + +import org.springframework.boot.convert.ApplicationConversionService; + +public class PropertyUtils { + public static T getProperty(String key, T defaultvalue) { + String value = System.getProperty(key); + if (value == null) { + value = System.getenv(key); + } + if (value == null && ContextUtils.getApplicationContext() != null) { + value = ContextUtils.getApplicationContext().getEnvironment().getProperty(key); + } + if (value == null) { + return defaultvalue; + } + return (T) convert(value, defaultvalue.getClass()); + } + + public static T convert(Object value, Class type) { + if (value == null) { + return null; + } + return (T) ApplicationConversionService.getSharedInstance().convert(value, type); + } + + public static Object getProperty(String key) { + String value = System.getProperty(key); + if (value == null) { + value = System.getenv(key); + } + if (value == null && ContextUtils.getApplicationContext() != null) { + value = ContextUtils.getApplicationContext().getEnvironment().getProperty(key); + } + return value; + } + + public static void setDefaultInitProperty(String key, String propertyValue) { + System.setProperty(key, propertyValue); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/ArrayValuable.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/ArrayValuable.java new file mode 100644 index 0000000..846a9a8 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/ArrayValuable.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.util.core; + +/** + * 可生成 T 数组的接口 + */ +public interface ArrayValuable { + + /** + * @return 数组 + */ + T[] array(); + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/KeyValue.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/KeyValue.java new file mode 100644 index 0000000..1dbd28f --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/KeyValue.java @@ -0,0 +1,20 @@ +package cn.lingniu.framework.plugin.util.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Key Value 的键值对 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KeyValue implements Serializable { + + private K key; + private V value; + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateIntervalEnum.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateIntervalEnum.java new file mode 100644 index 0000000..f66b3db --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateIntervalEnum.java @@ -0,0 +1,47 @@ +package cn.lingniu.framework.plugin.util.date; + +import cn.hutool.core.util.ArrayUtil; +import cn.lingniu.framework.plugin.util.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 时间间隔的枚举 + * + * @author dhb52 + */ +@Getter +@AllArgsConstructor +public enum DateIntervalEnum implements ArrayValuable { + + HOUR(0, "小时"), // 特殊:字典里,暂时不会有这个枚举!!!因为大多数情况下,用不到这个间隔 + DAY(1, "天"), + WEEK(2, "周"), + MONTH(3, "月"), + QUARTER(4, "季度"), + YEAR(5, "年") + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(DateIntervalEnum::getInterval).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer interval; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static DateIntervalEnum valueOf(Integer interval) { + return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values()); + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateUtils.java new file mode 100644 index 0000000..6a1bb97 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateUtils.java @@ -0,0 +1,151 @@ +package cn.lingniu.framework.plugin.util.date; + +import cn.hutool.core.date.LocalDateTimeUtil; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Date; + +/** + * 时间工具类 + */ +public class DateUtils { + + /** + * 时区 - 默认 + */ + public static final String TIME_ZONE_DEFAULT = "GMT+8"; + + /** + * 秒转换成毫秒 + */ + public static final long SECOND_MILLIS = 1000; + + public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd"; + + public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; + + /** + * 将 LocalDateTime 转换成 Date + * + * @param date LocalDateTime + * @return LocalDateTime + */ + public static Date of(LocalDateTime date) { + if (date == null) { + return null; + } + // 将此日期时间与时区相结合以创建 ZonedDateTime + ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault()); + // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳 + Instant instant = zonedDateTime.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return Date.from(instant); + } + + /** + * 将 Date 转换成 LocalDateTime + * + * @param date Date + * @return LocalDateTime + */ + public static LocalDateTime of(Date date) { + if (date == null) { + return null; + } + // 转为时间戳 + Instant instant = date.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + public static Date addTime(Duration duration) { + return new Date(System.currentTimeMillis() + duration.toMillis()); + } + + public static boolean isExpired(LocalDateTime time) { + LocalDateTime now = LocalDateTime.now(); + return now.isAfter(time); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param month 月 + * @param day 日 + * @return 指定时间 + */ + public static Date buildTime(int year, int month, int day) { + return buildTime(year, month, day, 0, 0, 0); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param month 月 + * @param day 日 + * @param hour 小时 + * @param minute 分钟 + * @param second 秒 + * @return 指定时间 + */ + public static Date buildTime(int year, int month, int day, + int hour, int minute, int second) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒 + return calendar.getTime(); + } + + public static Date max(Date a, Date b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.compareTo(b) > 0 ? a : b; + } + + public static LocalDateTime max(LocalDateTime a, LocalDateTime b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.isAfter(b) ? a : b; + } + + /** + * 是否今天 + * + * @param date 日期 + * @return 是否 + */ + public static boolean isToday(LocalDateTime date) { + return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now()); + } + + /** + * 是否昨天 + * + * @param date 日期 + * @return 是否 + */ + public static boolean isYesterday(LocalDateTime date) { + return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1)); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/LocalDateTimeUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/LocalDateTimeUtils.java new file mode 100644 index 0000000..e8ce131 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/LocalDateTimeUtils.java @@ -0,0 +1,352 @@ +package cn.lingniu.framework.plugin.util.date; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.date.TemporalAccessorUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import java.sql.Timestamp; +import java.time.DateTimeException; +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.List; + +import static cn.hutool.core.date.DatePattern.UTC_MS_WITH_XXX_OFFSET_PATTERN; +import static cn.hutool.core.date.DatePattern.createFormatter; + +/** + * 时间工具类,用于 {@link LocalDateTime} + */ +public class LocalDateTimeUtils { + + /** + * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值 + */ + public static LocalDateTime EMPTY = buildTime(1970, 1, 1); + + public static DateTimeFormatter UTC_MS_WITH_XXX_OFFSET_FORMATTER = createFormatter(UTC_MS_WITH_XXX_OFFSET_PATTERN); + + /** + * 解析时间 + * + * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功 + * + * @param time 时间 + * @return 时间字符串 + */ + public static LocalDateTime parse(String time) { + try { + return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN); + } catch (DateTimeParseException e) { + return LocalDateTimeUtil.parse(time); + } + } + + public static LocalDateTime addTime(Duration duration) { + return LocalDateTime.now().plus(duration); + } + + public static LocalDateTime minusTime(Duration duration) { + return LocalDateTime.now().minus(duration); + } + + public static boolean beforeNow(LocalDateTime date) { + return date.isBefore(LocalDateTime.now()); + } + + public static boolean afterNow(LocalDateTime date) { + return date.isAfter(LocalDateTime.now()); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param month 月 + * @param day 日 + * @return 指定时间 + */ + public static LocalDateTime buildTime(int year, int month, int day) { + return LocalDateTime.of(year, month, day, 0, 0, 0); + } + + public static LocalDateTime[] buildBetweenTime(int year1, int month1, int day1, + int year2, int month2, int day2) { + return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)}; + } + + /** + * 判指定断时间,是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param time 指定时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, Timestamp time) { + if (startTime == null || endTime == null || time == null) { + return false; + } + return LocalDateTimeUtil.isIn(LocalDateTimeUtil.of(time), startTime, endTime); + } + + /** + * 判指定断时间,是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param time 指定时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) { + if (startTime == null || endTime == null || time == null) { + return false; + } + return LocalDateTimeUtil.isIn(parse(time), startTime, endTime); + } + + /** + * 判断当前时间是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime == null || endTime == null) { + return false; + } + return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime); + } + + /** + * 判断当前时间是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 是否 + */ + public static boolean isBetween(String startTime, String endTime) { + if (startTime == null || endTime == null) { + return false; + } + LocalDate nowDate = LocalDate.now(); + return LocalDateTimeUtil.isIn(LocalDateTime.now(), + LocalDateTime.of(nowDate, LocalTime.parse(startTime)), + LocalDateTime.of(nowDate, LocalTime.parse(endTime))); + } + + /** + * 判断时间段是否重叠 + * + * @param startTime1 开始 time1 + * @param endTime1 结束 time1 + * @param startTime2 开始 time2 + * @param endTime2 结束 time2 + * @return 重叠:true 不重叠:false + */ + public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) { + LocalDate nowDate = LocalDate.now(); + return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1), + LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2)); + } + + /** + * 获取指定日期所在的月份的开始时间 + * 例如:2023-09-30 00:00:00,000 + * + * @param date 日期 + * @return 月份的开始时间 + */ + public static LocalDateTime beginOfMonth(LocalDateTime date) { + return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN); + } + + /** + * 获取指定日期所在的月份的最后时间 + * 例如:2023-09-30 23:59:59,999 + * + * @param date 日期 + * @return 月份的结束时间 + */ + public static LocalDateTime endOfMonth(LocalDateTime date) { + return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX); + } + + /** + * 获得指定日期所在季度 + * + * @param date 日期 + * @return 所在季度 + */ + public static int getQuarterOfYear(LocalDateTime date) { + return (date.getMonthValue() - 1) / 3 + 1; + } + + /** + * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负 + * + * @param dateTime 日期 + * @return 相差天数 + */ + public static Long between(LocalDateTime dateTime) { + return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS); + } + + /** + * 获取今天的开始时间 + * + * @return 今天 + */ + public static LocalDateTime getToday() { + return LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); + } + + /** + * 获取昨天的开始时间 + * + * @return 昨天 + */ + public static LocalDateTime getYesterday() { + return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1)); + } + + /** + * 获取本月的开始时间 + * + * @return 本月 + */ + public static LocalDateTime getMonth() { + return beginOfMonth(LocalDateTime.now()); + } + + /** + * 获取本年的开始时间 + * + * @return 本年 + */ + public static LocalDateTime getYear() { + return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN); + } + + public static List getDateRangeList(LocalDateTime startTime, + LocalDateTime endTime, + Integer interval) { + // 1.1 找到枚举 + DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); + Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); + // 1.2 将时间对齐 + startTime = LocalDateTimeUtil.beginOfDay(startTime); + endTime = LocalDateTimeUtil.endOfDay(endTime); + + // 2. 循环,生成时间范围 + List timeRanges = new ArrayList<>(); + switch (intervalEnum) { + case HOUR: + while (startTime.isBefore(endTime)) { + timeRanges.add(new LocalDateTime[]{startTime, startTime.plusHours(1).minusNanos(1)}); + startTime = startTime.plusHours(1); + } + case DAY: + while (startTime.isBefore(endTime)) { + timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)}); + startTime = startTime.plusDays(1); + } + break; + case WEEK: + while (startTime.isBefore(endTime)) { + LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, endOfWeek}); + startTime = endOfWeek.plusNanos(1); + } + break; + case MONTH: + while (startTime.isBefore(endTime)) { + LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, endOfMonth}); + startTime = endOfMonth.plusNanos(1); + } + break; + case QUARTER: + while (startTime.isBefore(endTime)) { + int quarterOfYear = getQuarterOfYear(startTime); + LocalDateTime quarterEnd = quarterOfYear == 4 + ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1) + : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, quarterEnd}); + startTime = quarterEnd.plusNanos(1); + } + break; + case YEAR: + while (startTime.isBefore(endTime)) { + LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, endOfYear}); + startTime = endOfYear.plusNanos(1); + } + break; + default: + throw new IllegalArgumentException("Invalid interval: " + interval); + } + // 3. 兜底,最后一个时间,需要保持在 endTime 之前 + LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges); + if (lastTimeRange != null) { + lastTimeRange[1] = endTime; + } + return timeRanges; + } + + /** + * 格式化时间范围 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param interval 时间间隔 + * @return 时间范围 + */ + public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) { + // 1. 找到枚举 + DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); + Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); + + // 2. 循环,生成时间范围 + switch (intervalEnum) { + case HOUR: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATETIME_MINUTE_PATTERN); + case DAY: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN); + case WEEK: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN) + + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime)); + case MONTH: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN); + case QUARTER: + return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime)); + case YEAR: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN); + default: + throw new IllegalArgumentException("Invalid interval: " + interval); + } + } + + /** + * 将给定的 {@link LocalDateTime} 转换为自 Unix 纪元时间(1970-01-01T00:00:00Z)以来的秒数。 + * + * @param sourceDateTime 需要转换的本地日期时间,不能为空 + * @return 自 1970-01-01T00:00:00Z 起的秒数(epoch second) + * @throws NullPointerException 如果 {@code sourceDateTime} 为 {@code null} + * @throws DateTimeException 如果转换过程中发生时间超出范围或其他时间处理异常 + */ + public static Long toEpochSecond(LocalDateTime sourceDateTime) { + return TemporalAccessorUtil.toInstant(sourceDateTime).getEpochSecond(); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/FileUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/FileUtils.java new file mode 100644 index 0000000..7053a01 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/FileUtils.java @@ -0,0 +1,59 @@ +package cn.lingniu.framework.plugin.util.io; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import lombok.SneakyThrows; + +import java.io.File; + +/** + * 文件工具类 + */ +public class FileUtils { + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(String data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeUtf8String(data, file); + return file; + } + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(byte[] data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeBytes(data, file); + return file; + } + + /** + * 创建临时文件,无内容 + * 该文件会在 JVM 退出时,进行删除 + * + * @return 文件 + */ + @SneakyThrows + public static File createTempFile() { + // 创建文件,通过 UUID 保证唯一 + File file = File.createTempFile(IdUtil.simpleUUID(), null); + // 标记 JVM 退出时,自动删除 + file.deleteOnExit(); + return file; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/IoUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/IoUtils.java new file mode 100644 index 0000000..39951f8 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/IoUtils.java @@ -0,0 +1,26 @@ +package cn.lingniu.framework.plugin.util.io; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.InputStream; + +/** + * IO 工具类,用于 {@link IoUtil} 缺失的方法 + */ +public class IoUtils { + + /** + * 从流中读取 UTF8 编码的内容 + * + * @param in 输入流 + * @param isClose 是否关闭 + * @return 内容 + * @throws IORuntimeException IO 异常 + */ + public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException { + return StrUtil.utf8Str(IoUtil.read(in, isClose)); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/ip/IpUtil.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/ip/IpUtil.java new file mode 100644 index 0000000..a1fa975 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/ip/IpUtil.java @@ -0,0 +1,59 @@ +package cn.lingniu.framework.plugin.util.ip; + +import org.springframework.util.CollectionUtils; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * 获取本机IP + */ +public final class IpUtil { + + private static List ipList = null; + + public static final String LOCAL_HOST = "127.0.0.1"; + + /** + * 获取本机IP + */ + public static String getIp() { + if (!CollectionUtils.isEmpty(ipList)) { + return ipList.get(0); + } + List ipList = new ArrayList<>(); + Enumeration allNetInterfaces; + try { + allNetInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + e.printStackTrace(); + return "localhost"; + } + InetAddress ip; + while (allNetInterfaces.hasMoreElements()) { + NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement(); + Enumeration addresses = netInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + ip = (InetAddress) addresses.nextElement(); + if (ip != null && ip instanceof Inet4Address) { + String localIp = ip.getHostAddress(); + if (localIp.equals(LOCAL_HOST)) { + continue; + } + ipList.add(localIp); + } + } + } + if (ipList.size() > 0) { + IpUtil.ipList = ipList; + return ipList.get(0); + } else { + return LOCAL_HOST; + } + } +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtil.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtil.java new file mode 100644 index 0000000..72d8add --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtil.java @@ -0,0 +1,156 @@ +package cn.lingniu.framework.plugin.util.json; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.io.StringWriter; +import java.time.ZoneId; +import java.util.TimeZone; + +/** + * JSON序列化工具类 + */ +@Slf4j +public class JsonUtil implements Serializable { + private static ObjectMapper mapper = new ObjectMapper(); + private static XmlMapper xmlMapper = new XmlMapper(); + + static { + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.registerModule(new JavaTimeModule()); + mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + mapper.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + + xmlMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + xmlMapper.registerModule(new JavaTimeModule()); + xmlMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + xmlMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + xmlMapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false); + xmlMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + xmlMapper.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + + xmlMapper.setDefaultUseWrapper(false); + //字段为null,自动忽略,不再序列化 + //XML标签名:使用骆驼命名的属性名, + xmlMapper.setPropertyNamingStrategy(PropertyNamingStrategy.UPPER_CAMEL_CASE); + //设置转换模式 + xmlMapper.enable(MapperFeature.USE_STD_BEAN_NAMING); + } + + /** + * 类型自动转换 + */ + public static T convertValue(I inValue, ObjectMapper objectMapper, TypeReference toValueTypeRef) { + return objectMapper.convertValue(inValue, toValueTypeRef); + } + + + /** + * 类型自动转换 + */ + public static T convertValue(I inValue, TypeReference toValueTypeRef) { + return convertValue(inValue, mapper, toValueTypeRef); + } + + public static String bean2Json(T obj) { + return bean2Json(obj, mapper, true); + } + + + public static String bean2Json(T obj, boolean forString) { + return bean2Json(obj, mapper, forString); + } + + + public static String bean2Json(T obj, ObjectMapper jsonMapper, boolean forString) { + try { + if (ObjectEmptyUtils.isNotEmpty(obj) && obj instanceof String && forString) { + return obj.toString(); + } + StringWriter sw = new StringWriter(); + JsonGenerator gen = new JsonFactory().createJsonGenerator(sw); + jsonMapper.writeValue(gen, obj); + gen.close(); + return sw.toString(); + } catch (Exception ex) { + log.error(String.format("对象象序列化异常:%s", obj.getClass().getName()), ex); + } + return ""; + } + + public static String bean2Json(T obj, ObjectMapper jsonMapper) { + return bean2Json(obj, jsonMapper, true); + } + + + public static T json2Bean(String jsonStr, Class objClass) { + return json2Bean(jsonStr, objClass, false); + } + + + @SneakyThrows + public static T json2Bean(String jsonStr, Class objClass, boolean throwEx) { + try { + if (ObjectEmptyUtils.isNotEmpty(objClass) + && objClass.equals(String.class) + && !jsonStr.startsWith("\"") + && !jsonStr.endsWith("\"")) { + return (T) jsonStr; + } + return mapper.readValue(jsonStr, objClass); + } catch (Exception ex) { + if (throwEx) { + throw ex; + } + log.error(String.format("对象象反序列化异常:%s", objClass.getName()), ex); + return null; + } + } + + public static T json2Bean(String jsonStr, TypeReference objClass) { + return json2Bean(jsonStr, objClass, mapper, false); + } + + public static T json2Bean(String jsonStr, TypeReference objClass, ObjectMapper mapperClz) { + return json2Bean(jsonStr, objClass, mapperClz, false); + } + + + @SneakyThrows + public static T json2Bean(String jsonStr, TypeReference objClass, ObjectMapper mapperClz, boolean throwEx) { + try { + if (ObjectEmptyUtils.isEmpty(jsonStr)) { + return null; + } + if (ObjectEmptyUtils.isNotEmpty(objClass) && objClass.getType() == String.class + && !jsonStr.startsWith("\"") && !jsonStr.endsWith("\"")) { + return (T) jsonStr; + } + return (ObjectEmptyUtils.isEmpty(mapperClz) ? mapper : mapperClz).readValue(jsonStr, objClass); + } catch (Exception ex) { + if (throwEx) { + throw ex; + } + log.error(String.format("对象象反序列化异常:%s", objClass.getType().getTypeName()), ex); + return null; + } + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtils.java new file mode 100644 index 0000000..746979b --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtils.java @@ -0,0 +1,231 @@ +package cn.lingniu.framework.plugin.util.json; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeDeserializer; +import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeSerializer; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * JSON 工具类 + * + */ +@Slf4j +public class JsonUtils { + + @Getter + private static ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值 + // 解决 LocalDateTime 的序列化 + SimpleModule simpleModule = new JavaTimeModule() + .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE) + .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); + objectMapper.registerModules(simpleModule); + } + + /** + * 初始化 objectMapper 属性 + *

+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean + * + * @param objectMapper ObjectMapper 对象 + */ + public static void init(ObjectMapper objectMapper) { + JsonUtils.objectMapper = objectMapper; + } + + @SneakyThrows + public static String toJsonString(Object object) { + return objectMapper.writeValueAsString(object); + } + + @SneakyThrows + public static byte[] toJsonByte(Object object) { + return objectMapper.writeValueAsBytes(object); + } + + @SneakyThrows + public static String toJsonPrettyString(Object object) { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); + } + + public static T parseObject(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, String path, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + JsonNode treeNode = objectMapper.readTree(text); + JsonNode pathNode = treeNode.path(path); + return objectMapper.readValue(pathNode.toString(), clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, Type type) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(byte[] text, Type type) { + if (ArrayUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 将字符串解析成指定类型的对象 + * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, + * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。 + * + * @param text 字符串 + * @param clazz 类型 + * @return 对象 + */ + public static T parseObject2(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + return JSONUtil.toBean(text, clazz); + } + + public static T parseObject(byte[] bytes, Class clazz) { + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + try { + return objectMapper.readValue(bytes, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", bytes, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null + * + * @param text 字符串 + * @param typeReference 类型引用 + * @return 指定类型的对象 + */ + public static T parseObjectQuietly(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + return null; + } + } + + public static List parseArray(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static List parseArray(String text, String path, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + JsonNode treeNode = objectMapper.readTree(text); + JsonNode pathNode = treeNode.path(path); + return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(String text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(byte[] text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static boolean isJson(String text) { + return JSONUtil.isTypeJSON(text); + } + + /** + * 判断字符串是否为 JSON 类型的字符串 + * @param str 字符串 + */ + public static boolean isJsonObject(String str) { + return JSONUtil.isTypeJSONObject(str); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/NumberSerializer.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/NumberSerializer.java new file mode 100644 index 0000000..59ac6d4 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/NumberSerializer.java @@ -0,0 +1,35 @@ +package cn.lingniu.framework.plugin.util.json.databind; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; + +import java.io.IOException; + +/** + * Long 序列化规则 + * + * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题 + */ +@JacksonStdImpl +public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer { + + private static final long MAX_SAFE_INTEGER = 9007199254740991L; + private static final long MIN_SAFE_INTEGER = -9007199254740991L; + + public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class); + + public NumberSerializer(Class rawType) { + super(rawType); + } + + @Override + public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 超出范围 序列化位字符串 + if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { + super.serialize(value, gen, serializers); + } else { + gen.writeString(value.toString()); + } + } +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeDeserializer.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeDeserializer.java new file mode 100644 index 0000000..d42b89f --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeDeserializer.java @@ -0,0 +1,25 @@ +package cn.lingniu.framework.plugin.util.json.databind; + +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.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 基于时间戳的 LocalDateTime 反序列化器 + */ +public class TimestampLocalDateTimeDeserializer extends JsonDeserializer { + + public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // 将 Long 时间戳,转换为 LocalDateTime 对象 + return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeSerializer.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeSerializer.java new file mode 100644 index 0000000..818cc80 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeSerializer.java @@ -0,0 +1,82 @@ +package cn.lingniu.framework.plugin.util.json.databind; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 基于时间戳的 LocalDateTime 序列化器 + */ +@Slf4j +public class TimestampLocalDateTimeSerializer extends JsonSerializer { + + public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer(); + + private static final Map, Map> FIELD_CACHE = new ConcurrentHashMap<>(); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 情况一:有 JsonFormat 自定义注解,则使用它。 + String fieldName = gen.getOutputContext().getCurrentName(); + if (fieldName != null) { + Object currentValue = gen.getOutputContext().getCurrentValue(); + if (currentValue != null) { + Class clazz = currentValue.getClass(); + Map fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap); + Field field = fieldMap.get(fieldName); + if (field != null && field.isAnnotationPresent(JsonFormat.class)) { + JsonFormat jsonFormat = field.getAnnotation(JsonFormat.class); + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(jsonFormat.pattern()); + gen.writeString(formatter.format(value)); + return; + } catch (Exception ex) { + log.warn("[serialize][({}#{}) 使用 JsonFormat pattern 失败,尝试使用默认的 Long 时间戳]", + clazz.getName(), fieldName, ex); + } + } + } + } + + // 情况二:默认将 LocalDateTime 对象,转换为 Long 时间戳 + gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } + + /** + * 构建字段映射(缓存) + * + * @param clazz 类 + * @return 字段映射 + */ + private Map buildFieldMap(Class clazz) { + Map fieldMap = new HashMap<>(); + for (Field field : ReflectUtil.getFields(clazz)) { + String fieldName = field.getName(); + JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class); + if (jsonProperty != null) { + String value = jsonProperty.value(); + if (StrUtil.isNotEmpty(value) && ObjUtil.notEqual("\u0000", value)) { + fieldName = value; + } + } + fieldMap.put(fieldName, field); + } + return fieldMap; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/MoneyUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/MoneyUtils.java new file mode 100644 index 0000000..21e50ec --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/MoneyUtils.java @@ -0,0 +1,129 @@ +package cn.lingniu.framework.plugin.util.number; + +import cn.hutool.core.math.Money; +import cn.hutool.core.util.NumberUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 金额工具类 + */ +public class MoneyUtils { + + /** + * 金额的小数位数 + */ + private static final int PRICE_SCALE = 2; + + /** + * 百分比对应的 BigDecimal 对象 + */ + public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100); + + /** + * 计算百分比金额,四舍五入 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @return 百分比金额 + */ + public static Integer calculateRatePrice(Integer price, Double rate) { + return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue(); + } + + /** + * 计算百分比金额,向下传入 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @return 百分比金额 + */ + public static Integer calculateRatePriceFloor(Integer price, Double rate) { + return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue(); + } + + /** + * 计算百分比金额 + * + * @param price 金额(单位分) + * @param count 数量 + * @param percent 折扣(单位分),列如 60.2%,则传入 6020 + * @return 商品总价 + */ + public static Integer calculator(Integer price, Integer count, Integer percent) { + price = price * count; + if (percent == null) { + return price; + } + return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100)); + } + + /** + * 计算百分比金额 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @param scale 保留小数位数 + * @param roundingMode 舍入模式 + */ + public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) { + return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以 + .divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100 + } + + /** + * 分转元 + * + * @param fen 分 + * @return 元 + */ + public static BigDecimal fenToYuan(int fen) { + return new Money(0, fen).getAmount(); + } + + /** + * 分转元(字符串) + * + * 例如说 fen 为 1 时,则结果为 0.01 + * + * @param fen 分 + * @return 元 + */ + public static String fenToYuanStr(int fen) { + return new Money(0, fen).toString(); + } + + /** + * 金额相乘,默认进行四舍五入 + * + * 位数:{@link #PRICE_SCALE} + * + * @param price 金额 + * @param count 数量 + * @return 金额相乘结果 + */ + public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) { + if (price == null || count == null) { + return null; + } + return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP); + } + + /** + * 金额相乘(百分比),默认进行四舍五入 + * + * 位数:{@link #PRICE_SCALE} + * + * @param price 金额 + * @param percent 百分比 + * @return 金额相乘结果 + */ + public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) { + if (price == null || percent == null) { + return null; + } + return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/NumberUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/NumberUtils.java new file mode 100644 index 0000000..04ed751 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/NumberUtils.java @@ -0,0 +1,76 @@ +package cn.lingniu.framework.plugin.util.number; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 数字的工具类,补全 {@link NumberUtil} 的功能 + */ +public class NumberUtils { + + public static Long parseLong(String str) { + return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; + } + + public static Integer parseInt(String str) { + return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null; + } + + public static boolean isAllNumber(List values) { + if (CollUtil.isEmpty(values)) { + return false; + } + for (String value : values) { + if (!NumberUtil.isNumber(value)) { + return false; + } + } + return true; + } + + /** + * 通过经纬度获取地球上两点之间的距离 + * + * 参考 <DistanceUtil> 实现,目前它已经被 hutool 删除 + * + * @param lat1 经度1 + * @param lng1 纬度1 + * @param lat2 经度2 + * @param lng2 纬度2 + * @return 距离,单位:千米 + */ + public static double getDistance(double lat1, double lng1, double lat2, double lng2) { + double radLat1 = lat1 * Math.PI / 180.0; + double radLat2 = lat2 * Math.PI / 180.0; + double a = radLat1 - radLat2; + double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; + double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + + Math.cos(radLat1) * Math.cos(radLat2) + * Math.pow(Math.sin(b / 2), 2))); + distance = distance * 6378.137; + distance = Math.round(distance * 10000d) / 10000d; + return distance; + } + + /** + * 提供精确的乘法运算 + * + * 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null + * + * @param values 多个被乘值 + * @return 积 + */ + public static BigDecimal mul(BigDecimal... values) { + for (BigDecimal value : values) { + if (value == null) { + return null; + } + } + return NumberUtil.mul(values); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/BeanUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/BeanUtils.java new file mode 100644 index 0000000..3771b03 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/BeanUtils.java @@ -0,0 +1,53 @@ +package cn.lingniu.framework.plugin.util.object; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.db.PageResult; +import cn.lingniu.framework.plugin.util.collection.CollectionUtils; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Bean 工具类 + * + * 1. 默认使用 {@link BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能 + * 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现 + */ +public class BeanUtils { + + public static T toBean(Object source, Class targetClass) { + return BeanUtil.toBean(source, targetClass); + } + + public static T toBean(Object source, Class targetClass, Consumer peek) { + T target = toBean(source, targetClass); + if (target != null) { + peek.accept(target); + } + return target; + } + + public static List toBean(List source, Class targetType) { + if (source == null) { + return null; + } + return CollectionUtils.convertList(source, s -> toBean(s, targetType)); + } + + public static List toBean(List source, Class targetType, Consumer peek) { + List list = toBean(source, targetType); + if (list != null) { + list.forEach(peek); + } + return list; + } + + + public static void copyProperties(Object source, Object target) { + if (source == null || target == null) { + return; + } + BeanUtil.copyProperties(source, target, false); + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/ObjectUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/ObjectUtils.java new file mode 100644 index 0000000..49ab0be --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/ObjectUtils.java @@ -0,0 +1,65 @@ +package cn.lingniu.framework.plugin.util.object; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.function.Consumer; + +/** + * Object 工具类 + */ +public class ObjectUtils { + + /** + * 复制对象,并忽略 Id 编号 + * + * @param object 被复制对象 + * @param consumer 消费者,可以二次编辑被复制对象 + * @return 复制后的对象 + */ + public static T cloneIgnoreId(T object, Consumer consumer) { + T result = ObjectUtil.clone(object); + // 忽略 id 编号 + Field field = ReflectUtil.getField(object.getClass(), "id"); + if (field != null) { + ReflectUtil.setFieldValue(result, field, null); + } + // 二次编辑 + if (result != null) { + consumer.accept(result); + } + return result; + } + + public static > T max(T obj1, T obj2) { + if (obj1 == null) { + return obj2; + } + if (obj2 == null) { + return obj1; + } + return obj1.compareTo(obj2) > 0 ? obj1 : obj2; + } + + @SafeVarargs + public static T defaultIfNull(T... array) { + for (T item : array) { + if (item != null) { + return item; + } + } + return null; + } + + @SafeVarargs + public static boolean equalsAny(T obj, T... array) { + return Arrays.asList(array).contains(obj); + } + + public static boolean isNotAllEmpty(Object... objs) { + return !ObjectUtil.isAllEmpty(objs); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/servlet/ServletUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/servlet/ServletUtils.java new file mode 100644 index 0000000..23fe70b --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/servlet/ServletUtils.java @@ -0,0 +1,121 @@ +package cn.lingniu.framework.plugin.util.servlet; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.JakartaServletUtil; +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.Map; + +/** + * 客户端工具类 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JsonUtils.toJsonString(object); + JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + /** + * 返回附件 + * + * @param response 响应 + * @param filename 文件名 + * @param content 附件内容 + */ + public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { + // 设置 header 和 contentType + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + // 输出附件 + IoUtil.write(response.getOutputStream(), false, content); + } + + /** + * @param request 请求 + * @return ua + */ + public static String getUserAgent(HttpServletRequest request) { + String ua = request.getHeader("User-Agent"); + return ua != null ? ua : ""; + } + + /** + * 获得请求 + * + * @return HttpServletRequest + */ + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + return ((ServletRequestAttributes) requestAttributes).getRequest(); + } + + public static String getUserAgent() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return getUserAgent(request); + } + + public static String getClientIP() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return JakartaServletUtil.getClientIP(request); + } + + public static boolean isJsonRequest(ServletRequest request) { + return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); + } + + public static String getBody(HttpServletRequest request) { + // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取 + if (isJsonRequest(request)) { + return JakartaServletUtil.getBody(request); + } + return null; + } + + public static byte[] getBodyBytes(HttpServletRequest request) { + // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取 + if (isJsonRequest(request)) { + return JakartaServletUtil.getBodyBytes(request); + } + return null; + } + + public static String getClientIP(HttpServletRequest request) { + return JakartaServletUtil.getClientIP(request); + } + + public static Map getParamMap(HttpServletRequest request) { + return JakartaServletUtil.getParamMap(request); + } + + public static Map getHeaderMap(HttpServletRequest request) { + return JakartaServletUtil.getHeaderMap(request); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringExpressionUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringExpressionUtils.java new file mode 100644 index 0000000..7d97e08 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringExpressionUtils.java @@ -0,0 +1,123 @@ +package cn.lingniu.framework.plugin.util.spring; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Spring EL 表达式的工具类 + * + * @author mashu + */ +public class SpringExpressionUtils { + + /** + * Spring EL 表达式解析器 + */ + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + /** + * 参数名发现器 + */ + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + private SpringExpressionUtils() { + } + + /** + * 从切面中,单个解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionString EL 表达式数组 + * @return 执行界面 + */ + public static Object parseExpression(JoinPoint joinPoint, String expressionString) { + Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); + return result.get(expressionString); + } + + /** + * 从切面中,批量解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionStrings EL 表达式数组 + * @return 结果,key 为表达式,value 为对应值 + */ + public static Map parseExpressions(JoinPoint joinPoint, List expressionStrings) { + // 如果为空,则不进行解析 + if (CollUtil.isEmpty(expressionStrings)) { + return MapUtil.newHashMap(); + } + + // 第一步,构建解析的上下文 EvaluationContext + // 通过 joinPoint 获取被注解方法 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + Method method = methodSignature.getMethod(); + // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组 + String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + // Spring 的表达式上下文对象 + EvaluationContext context = new StandardEvaluationContext(); + // 给上下文赋值 + if (ArrayUtil.isNotEmpty(paramNames)) { + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < paramNames.length; i++) { + context.setVariable(paramNames[i], args[i]); + } + } + + // 第二步,逐个参数解析 + Map result = MapUtil.newHashMap(expressionStrings.size(), true); + expressionStrings.forEach(key -> { + Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); + result.put(key, value); + }); + return result; + } + + /** + * 从 Bean 工厂,解析 EL 表达式的结果 + * + * @param expressionString EL 表达式 + * @return 执行界面 + */ + public static Object parseExpression(String expressionString) { + return parseExpression(expressionString, null); + } + + /** + * 从 Bean 工厂,解析 EL 表达式的结果 + * + * @param expressionString EL 表达式 + * @param variables 变量 + * @return 执行界面 + */ + public static Object parseExpression(String expressionString, Map variables) { + if (StrUtil.isBlank(expressionString)) { + return null; + } + Expression expression = EXPRESSION_PARSER.parseExpression(expressionString); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext())); + if (MapUtil.isNotEmpty(variables)) { + context.setVariables(variables); + } + return expression.getValue(context); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringUtils.java new file mode 100644 index 0000000..f4234c9 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringUtils.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.util.spring; + +import cn.hutool.extra.spring.SpringUtil; + +import java.util.Objects; + +/** + * Spring 工具类 + */ +public class SpringUtils extends SpringUtil { + + /** + * 是否为生产环境 + * + * @return 是否生产环境 + */ + public static boolean isProd() { + String activeProfile = getActiveProfile(); + return Objects.equals("prod", activeProfile); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StrUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StrUtils.java new file mode 100644 index 0000000..35a7a49 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StrUtils.java @@ -0,0 +1,78 @@ +package cn.lingniu.framework.plugin.util.string; + +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 字符串工具类 + */ +public class StrUtils { + + public static String maxLength(CharSequence str, int maxLength) { + return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 + } + + /** + * 给定字符串是否以任何一个字符串开始 + * 给定字符串和数组为空都返回 false + * + * @param str 给定字符串 + * @param prefixes 需要检测的开始字符串 + * @since 3.0.6 + */ + public static boolean startWithAny(String str, Collection prefixes) { + if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { + return false; + } + + for (CharSequence suffix : prefixes) { + if (StrUtil.startWith(str, suffix, false)) { + return true; + } + } + return false; + } + + public static List splitToLong(String value, CharSequence separator) { + long[] longs = StrUtil.splitToLong(value, separator); + return Arrays.stream(longs).boxed().collect(Collectors.toList()); + } + + public static Set splitToLongSet(String value) { + return splitToLongSet(value, StrPool.COMMA); + } + + public static Set splitToLongSet(String value, CharSequence separator) { + long[] longs = StrUtil.splitToLong(value, separator); + return Arrays.stream(longs).boxed().collect(Collectors.toSet()); + } + + public static List splitToInteger(String value, CharSequence separator) { + int[] integers = StrUtil.splitToInt(value, separator); + return Arrays.stream(integers).boxed().collect(Collectors.toList()); + } + + /** + * 移除字符串中,包含指定字符串的行 + * + * @param content 字符串 + * @param sequence 包含的字符串 + * @return 移除后的字符串 + */ + public static String removeLineContains(String content, String sequence) { + if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) { + return content; + } + return Arrays.stream(content.split("\n")) + .filter(line -> !line.contains(sequence)) + .collect(Collectors.joining("\n")); + } + + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StringUtil.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StringUtil.java new file mode 100644 index 0000000..1fa8f24 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StringUtil.java @@ -0,0 +1,72 @@ +package cn.lingniu.framework.plugin.util.string; + +import org.apache.commons.lang3.StringUtils; +import java.util.Vector; + +public class StringUtil extends StringUtils { + + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * @param input the string to split + * @param delimiter the delimiter to use to split the string + * @return split, and return extra data defined past last delimiter, start at begining of string + */ + public static String[] split(String input, String delimiter) { + return split(input, delimiter, true, 0); + } + + + /** + * Split the given string around the delimiter given. + * + * @param str the string to split + * @param delimiter the delimiter to use to split the string + * @param withEnd return data that is found past the last delimiter? + * @param initIndex the starting index to begin splitting around + * @return array of data found around the delimiters + */ + public static String[] split(String str, String delimiter, boolean withEnd, int initIndex) { + String input = str; + //quit if: data is null or is an empty string + if (input == null || input.equals(new String()) || input == "") return new String[0]; + + //create a buffer array for the worst case scenario of data size + Vector dataBuffer = new Vector(); + int index = 0; + + //cut the string begining at the given index if needed + if (initIndex > 0) + input = input.substring(initIndex); + + while (true) { + if (input == null) break; + int cutIndex = input.indexOf(delimiter); + if (cutIndex == -1) { + if (withEnd) + dataBuffer.addElement(input); + break; + } + String sub1 = input.substring(0, cutIndex); + String sub2 = input.substring(cutIndex + 1); + + dataBuffer.addElement(sub1); + input = sub2; + } + //convert the buffer to an array and return it + String[] d = new String[dataBuffer.size()]; + + int l = dataBuffer.size(); + for (int i = 0; i < l; i++) { + d[i] = (String) dataBuffer.elementAt(i); + } + return d; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/validation/ObjectEmptyUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/validation/ObjectEmptyUtils.java new file mode 100644 index 0000000..eedd9ae --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/validation/ObjectEmptyUtils.java @@ -0,0 +1,79 @@ +package cn.lingniu.framework.plugin.util.validation; + +import org.springframework.util.ObjectUtils; + +import java.util.Collection; +import java.util.Map; + +/** + * 判空工具类 + */ +public final class ObjectEmptyUtils { + + /** + * 判断集合是否为空 + */ + public static boolean isEmpty(Collection coll) { + return (coll == null || coll.isEmpty()); + } + + /** + * 判断集合是否不为空 + */ + public static boolean isNotEmpty(Collection coll) { + return !isEmpty(coll); + } + + /** + * 判断map是否为空 + */ + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } + + /** + * 判断map是否不为空 + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * 判断一个对象是否为空 + */ + public static boolean isEmpty(T t) { + if (t == null) { + return true; + } + // 根据对象类型选择最优判断方式 + if (t instanceof String) { + return ((String) t).length() == 0; // 比isEmpty()更直接 + } else if (t instanceof Collection) { + return ((Collection) t).size() == 0; + } else if (t instanceof Map) { + return ((Map) t).size() == 0; + } else if (t.getClass().isArray()) { + return ObjectUtils.isEmpty(t); + } else { + // 对于其他类型,提供安全的字符串判断 + String str = t.toString(); + return str == null || str.isEmpty(); + } + + } + + /** + * 判断数组是否不为空 + */ + public static boolean isEmpty(T[] datas) { + return ObjectUtils.isEmpty(datas); + } + + /** + * 判断一个对象是否不为空 + */ + public static boolean isNotEmpty(T t) { + return !isEmpty(t); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/resources/util.md b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/resources/util.md new file mode 100644 index 0000000..e47a6fe --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/resources/util.md @@ -0,0 +1 @@ +# 框架核心公共util模块 diff --git a/lingniu-framework-plugin/log/logback-spring.xml b/lingniu-framework-plugin/log/logback-spring.xml new file mode 100644 index 0000000..5ff0f40 --- /dev/null +++ b/lingniu-framework-plugin/log/logback-spring.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n + UTF-8 + + + + + + + INFO + ACCEPT + DENY + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n + UTF-8 + + true + + ${LOG_PATH}/${APP_NAME}-info.%d{yyyy-MM-dd}.%i.log + ${MAX_FILE_SIZE} + ${MAX_HISTORY} + ${TOTAL_SIZE_CAP} + + + + + + + ERROR + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n + UTF-8 + + true + + ${LOG_PATH}/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log + ${MAX_FILE_SIZE} + ${MAX_HISTORY} + ${TOTAL_SIZE_CAP} + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n + UTF-8 + + true + + ${LOG_PATH}/${APP_NAME}-business.%d{yyyy-MM-dd}.%i.log + ${MAX_FILE_SIZE} + ${MAX_HISTORY} + ${TOTAL_SIZE_CAP} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/pom.xml b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/pom.xml new file mode 100644 index 0000000..3b5a1bb --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-microservice-common + lingniu-framework-plugin-microservice-common + http://maven.apache.org + + UTF-8 + + + + io.github.openfeign + feign-hystrix + true + + + + + cn.lingniu.framework + lingniu-framework-plugin-web + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + io.github.openfeign + feign-okhttp + + + io.github.openfeign + feign-httpclient + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + commons-logging + commons-logging + + + + + com.alibaba + fastjson + provided + true + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + 3.1.6 + + + org.springframework.retry + spring-retry + 1.3.4 + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.cloud + spring-cloud-context + + + \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/DefaultHttpProperties.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/DefaultHttpProperties.java new file mode 100644 index 0000000..bfb1e44 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/DefaultHttpProperties.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.microservice.common.config; + +import lombok.Data; + + +@Data +public class DefaultHttpProperties { + + /** + * 每个route默认的最大连接数 + */ + private Integer defaultMaxPerRoute = 25; + /** + * 从连接池中获取连接的超时时间,超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool + */ + private Integer connectionRequestTimeout = 500; + /** + * 空闲永久连接检查间隔 + */ + private Integer validateAfterInactivity = 2000; + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/FeignProperties.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/FeignProperties.java new file mode 100644 index 0000000..5029083 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/FeignProperties.java @@ -0,0 +1,18 @@ +package cn.lingniu.framework.plugin.microservice.common.config; + +import feign.Logger; +import lombok.Data; + + +@Data +public class FeignProperties { + + private Logger.Level loggerLevel = Logger.Level.BASIC; + + private Long period = 500L; + + private Integer maxPeriod = 1000; + + private Integer maxAttempts = 1; + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/MircroServiceConfig.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/MircroServiceConfig.java new file mode 100644 index 0000000..de69b55 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/MircroServiceConfig.java @@ -0,0 +1,47 @@ +package cn.lingniu.framework.plugin.microservice.common.config; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Data +@ConfigurationProperties(MircroServiceConfig.PRE_FIX) +public class MircroServiceConfig { + + public static final String PRE_FIX = "framework.lingniu.spring.cloud"; + + /** + * 是否启用OkHttpClient + */ + private Boolean isOkHttp = true; + /** + * common + * 读取超时 + */ + private Integer readTimeout = 1000; + /** + * common + * 连接超时 + */ + private Integer connectTimeout = 1000; + /** + * common + * 整个连接池的最大连接数 + */ + private Integer maxTotal = 150; + /** + * fegin配置 + */ + private FeignProperties feign = new FeignProperties(); + /** + * defaultHttp. 默认http配置 + */ + private DefaultHttpProperties defaultHttp = new DefaultHttpProperties(); + /** + * okHttp相关配置 + */ + private OkHttpProperties okHttp = new OkHttpProperties(); + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/OkHttpProperties.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/OkHttpProperties.java new file mode 100644 index 0000000..f1d4ec0 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/OkHttpProperties.java @@ -0,0 +1,30 @@ +package cn.lingniu.framework.plugin.microservice.common.config; + +import lombok.Data; + +import java.util.concurrent.TimeUnit; + +@Data +public class OkHttpProperties { + + /** + * OkHttp TimeToLive + */ + private Long timeToLive = 2000L; + /** + * 时间单位 + */ + private TimeUnit timeToLiveUnit = TimeUnit.MILLISECONDS; + /** + * 是否开启重定身 + */ + private Boolean isFollowRedirects = true; + /** + * 是否开启SSL验证 + */ + private Boolean disableSslValidation = false; + /** + * 写起时 + */ + private Integer writeTimeout = 1000; +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/ErrorCommonResultHandler.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/ErrorCommonResultHandler.java new file mode 100644 index 0000000..fa70270 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/ErrorCommonResultHandler.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.microservice.common.core; + + +import cn.lingniu.framework.plugin.core.base.CommonResult; + +/** + * @description: 异常解码器 + **/ +public interface ErrorCommonResultHandler { + + CommonResult handler(String body); + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/StringHttpMessageConverter.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/StringHttpMessageConverter.java new file mode 100644 index 0000000..cc896a4 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/StringHttpMessageConverter.java @@ -0,0 +1,122 @@ +package cn.lingniu.framework.plugin.microservice.common.core; + + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of {@link HttpMessageConverter} that can read and write strings. + * + *

By default, this converter supports all media types ({@code */*}), + * and writes with a {@code Content-Type} of {@code text/plain}. This can be overridden + * by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + */ +public class StringHttpMessageConverter extends AbstractHttpMessageConverter { + + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + + @Nullable + private volatile List availableCharsets; + + private boolean writeAcceptCharset = true; + + + /** + * A default constructor that uses {@code "ISO-8859-1"} as the default charset. + * + * @see #StringHttpMessageConverter(Charset) + */ + public StringHttpMessageConverter() { + this(DEFAULT_CHARSET); + } + + /** + * A constructor accepting a default charset to use if the requested content + * type does not specify one. + */ + public StringHttpMessageConverter(Charset defaultCharset) { + super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL); + } + + + /** + * Indicates whether the {@code Accept-Charset} should be written to any outgoing request. + *

Default is {@code true}. + */ + public void setWriteAcceptCharset(boolean writeAcceptCharset) { + this.writeAcceptCharset = writeAcceptCharset; + } + + + @Override + + public boolean supports(Class clazz) { + return String.class == clazz; + } + + @Override + protected String readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { + Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType()); + return StreamUtils.copyToString(inputMessage.getBody(), charset); + } + + @Override + protected Long getContentLength(String str, @Nullable MediaType contentType) { + Charset charset = getContentTypeCharset(contentType); + return (long) str.getBytes(charset).length; + } + + @Override + protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException { + if (this.writeAcceptCharset) { + outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); + } + Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType()); + StreamUtils.copy(str, charset, outputMessage.getBody()); + } + + + /** + * Return the list of supported {@link Charset}s. + *

By default, returns {@link Charset#availableCharsets()}. + * Can be overridden in subclasses. + * + * @return the list of accepted charsets + */ + protected List getAcceptedCharsets() { + List charsets = this.availableCharsets; + if (charsets == null) { + charsets = new ArrayList<>(Charset.availableCharsets().values()); + this.availableCharsets = charsets; + } + return charsets; + } + + private Charset getContentTypeCharset(@Nullable MediaType contentType) { + if (contentType != null && contentType.getCharset() != null) { + return contentType.getCharset(); + } else { + Charset charset = getDefaultCharset(); + Assert.state(charset != null, "No default charset"); + return charset; + } + } + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuErrorDecoder.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuErrorDecoder.java new file mode 100644 index 0000000..7b0b5a5 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuErrorDecoder.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2015-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.lingniu.framework.plugin.microservice.common.core.decoder; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.exception.ServerException; +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import cn.lingniu.framework.plugin.microservice.common.core.ErrorCommonResultHandler; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import feign.Response; +import feign.Util; +import feign.codec.ErrorDecoder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; + +/** + * 异常解析 + */ +@Slf4j +public class LingniuErrorDecoder extends ErrorDecoder.Default implements ErrorDecoder { + + private final ErrorCommonResultHandler handler; + + public LingniuErrorDecoder(ErrorCommonResultHandler errorCommonResultHandler) { + this.handler = errorCommonResultHandler; + } + + @Override + public Exception decode(final String methodKey, final Response response) { + try { + if (HttpStatus.NOT_FOUND.value() == response.status() || ObjectEmptyUtils.isEmpty(response.body())) { + return new ServerException(GlobalErrorCodeConstants.NOT_FOUND); + } else if (HttpStatus.UNAUTHORIZED.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.UNAUTHORIZED); + } else if (HttpStatus.METHOD_NOT_ALLOWED.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.FORBIDDEN); + } else if (HttpStatus.TOO_MANY_REQUESTS.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS); + } else if (HttpStatus.REQUEST_TIMEOUT.value() == response.status() || HttpStatus.GATEWAY_TIMEOUT.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.TIME_OUT_ERROR); + } else if (HttpStatus.BAD_GATEWAY.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.BAD_GATE_WAY_ERROR); + } + String body = Util.toString(response.body().asReader()); + CommonResult result = handler.handler(body); + if (ObjectEmptyUtils.isNotEmpty(result) && result.getCode() != 0) { + if (log.isWarnEnabled()) { + log.warn("微服务调用: {} 响应异常信息:{} {} ", methodKey, result.getCode(), result.getMsg()); + } + return new ServerException(result.getCode(), result.getMsg()); + } else if (HttpStatus.valueOf(response.status()).is5xxServerError()) { + return new ServerException(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR); + } else { + return new ServerException(GlobalErrorCodeConstants.UNKNOWN); + } + } catch (Exception ex) { + log.error("Feign异常解码出现问题", ex); + return new ServerException(GlobalErrorCodeConstants.FEIGN_DECODE_ERROR); + } + } +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuFeignDecoder.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuFeignDecoder.java new file mode 100644 index 0000000..6f1dc33 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuFeignDecoder.java @@ -0,0 +1,85 @@ +/* + * Copyright © 2015-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.lingniu.framework.plugin.microservice.common.core.decoder; + +import feign.FeignException; +import feign.Response; +import feign.codec.Decoder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.LinkedList; + +/** + * LingniuFeignDecoder + */ +@Slf4j +public class LingniuFeignDecoder implements Decoder { + + private Decoder decoder; + + public LingniuFeignDecoder(final Decoder decoder) { + this.decoder = decoder; + } + + @Override + public Object decode(final Response response, final Type t) throws IOException, FeignException { + Type type = t; + if (isParameterizeHttpEntity(type)) { + type = ((ParameterizedType) type).getActualTypeArguments()[0]; + Object decodedObject = decoder.decode(response, type); + return createResponse(decodedObject, response); + } else if (isHttpEntity(type)) { + return createResponse(null, response); + } else { + Object decodeResponse = decoder.decode(response, type); + return decodeResponse; + } + } + + private boolean isParameterizeHttpEntity(final Type type) { + if (type instanceof ParameterizedType) { + return isHttpEntity(((ParameterizedType) type).getRawType()); + } + return false; + } + + @SuppressWarnings("rawtypes") + private boolean isHttpEntity(final Type type) { + if (type instanceof Class) { + Class c = (Class) type; + return HttpEntity.class.isAssignableFrom(c); + } + return false; + } + + @SuppressWarnings("unchecked") + private ResponseEntity createResponse(final Object instance, final Response response) { + MultiValueMap headers = new LinkedMultiValueMap<>(); + for (String key : response.headers().keySet()) { + headers.put(key, new LinkedList<>(response.headers().get(key))); + } + return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response.status())); + } +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/CharlesRequestInterceptor.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/CharlesRequestInterceptor.java new file mode 100644 index 0000000..c97f6b5 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/CharlesRequestInterceptor.java @@ -0,0 +1,83 @@ +package cn.lingniu.framework.plugin.microservice.common.core.interceptor; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +@Slf4j +public class CharlesRequestInterceptor implements RequestInterceptor { + + private static ObjectMapper objectMapper; + + static { + objectMapper = new ObjectMapper(); + } + + @Override + public void apply(RequestTemplate template) { + // feign 不支持 GET 方法传 POJO, json body转query + if (template.method().equals("GET") && ObjectEmptyUtils.isNotEmpty(template.body())) { + try { + JsonNode jsonNode = objectMapper.readTree(template.body()); + template.body(null, Charset.defaultCharset()); + Map> queries = new HashMap<>(); + buildQuery(jsonNode, "", queries); + template.queries(queries); + } catch (IOException e) { + log.error("CharlesRequestInterceptor异常", e); + } + } + } + + private String encoderParameters(String parameterValue) { + try { + return URLEncoder.encode(parameterValue, "utf-8"); + } catch (Exception ex) { + log.error("参数转换异常", ex); + } + return parameterValue; + } + + private void buildQuery(JsonNode jsonNode, String path, Map> queries) { + if (!jsonNode.isContainerNode()) { + if (jsonNode.isNull()) { + return; + } + Collection values = queries.get(path); + if (null == values) { + values = new ArrayList<>(); + queries.put(path, values); + } + values.add(encoderParameters(jsonNode.asText())); + return; + } + if (jsonNode.isArray()) { + Iterator it = jsonNode.elements(); + while (it.hasNext()) { + buildQuery(it.next(), path, queries); + } + } else { + Iterator> it = jsonNode.fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (ObjectEmptyUtils.isNotEmpty(path)) { + buildQuery(entry.getValue(), path + "." + entry.getKey(), queries); + } else { + buildQuery(entry.getValue(), entry.getKey(), queries); + } + } + } + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/FeignLoggerInterceptor.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/FeignLoggerInterceptor.java new file mode 100644 index 0000000..ef839a6 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/FeignLoggerInterceptor.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.microservice.common.core.interceptor; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FeignLoggerInterceptor implements RequestInterceptor { + + public FeignLoggerInterceptor() { + } + + @Override + public void apply(RequestTemplate requestTemplate) { + String bodyContent = ObjectEmptyUtils.isNotEmpty(requestTemplate.body()) ? new String(requestTemplate.body()) : ""; + log.info("Feign调用日志:URL:{} Mehod:{} Body:{} ", requestTemplate.url(), requestTemplate.method(), bodyContent); + } +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/DefaultFeignLoadBalancedConfiguration.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/DefaultFeignLoadBalancedConfiguration.java new file mode 100644 index 0000000..969d29a --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/DefaultFeignLoadBalancedConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package cn.lingniu.framework.plugin.microservice.common.init; + +import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig; +import feign.Client; +import feign.httpclient.ApacheHttpClient; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * DefaultFeignLoadBalancedConfiguration + */ +@Configuration +@ConditionalOnProperty(value = "framework.lingniu.spring.cloud.isOkHttp", havingValue = "false") +class DefaultFeignLoadBalancedConfiguration { + + + @Bean + public Client feignClient(MircroServiceConfig mircroServiceConfig, + LoadBalancerClient loadBalancerClient, + LoadBalancedRetryFactory loadBalancedRetryFactory, + LoadBalancerClientFactory loadBalancerClientFactory) { + ApacheHttpClient delegate = new ApacheHttpClient(httpClient(mircroServiceConfig)); + return new RetryableFeignBlockingLoadBalancerClient(delegate, + loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory); + } + + + public HttpClient httpClient(MircroServiceConfig mircroServiceConfig) { + Registry registry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", SSLConnectionSocketFactory.getSocketFactory()) + .build(); + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry); + connectionManager.setMaxTotal(mircroServiceConfig.getMaxTotal()); + connectionManager.setDefaultMaxPerRoute(mircroServiceConfig.getDefaultHttp().getDefaultMaxPerRoute()); + connectionManager.setValidateAfterInactivity(mircroServiceConfig.getDefaultHttp().getValidateAfterInactivity()); + RequestConfig requestConfig = RequestConfig.custom() + .setSocketTimeout(mircroServiceConfig.getReadTimeout()) + .setConnectTimeout(mircroServiceConfig.getConnectTimeout()) + .setConnectionRequestTimeout(mircroServiceConfig.getDefaultHttp().getConnectionRequestTimeout()) + .build(); + return HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(connectionManager) + .build(); + } + + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/HttpMessageFastJsonConverterConfiguration.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/HttpMessageFastJsonConverterConfiguration.java new file mode 100644 index 0000000..72723f0 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/HttpMessageFastJsonConverterConfiguration.java @@ -0,0 +1,96 @@ +package cn.lingniu.framework.plugin.microservice.common.init; + +import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig; +import cn.lingniu.framework.plugin.microservice.common.core.StringHttpMessageConverter; +import cn.lingniu.framework.plugin.microservice.common.core.decoder.LingniuFeignDecoder; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.form.FormEncoder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.cloud.openfeign.support.SpringDecoder; +import org.springframework.cloud.openfeign.support.SpringEncoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@ConditionalOnClass(name = {"com.alibaba.fastjson.support.config.FastJsonConfig"}) +@ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "json-converter", havingValue = "fastjson") +@Configuration +@AutoConfigureBefore({LingniuCloudAutoConfiguration.class}) +public class HttpMessageFastJsonConverterConfiguration { + + HttpMessageConverters converters = new HttpMessageConverters(createStringConverter(), createFastJsonConverter()); + + @Bean + public Decoder fastJsonFeignDecoder() { + return new LingniuFeignDecoder(new SpringDecoder(() -> converters)); + } + + @Bean + public Encoder fastJsonFeignEncoder() { + return new FormEncoder(new SpringEncoder(() -> converters)); + } + + private HttpMessageConverter createStringConverter() { + //fastConverter转换纯String有问题,该处使用的是重写后的转换器,防止被Spring优先级排到后面导致没有生效 + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8); + stringHttpMessageConverter.setWriteAcceptCharset(false); + return stringHttpMessageConverter; + } + + private HttpMessageConverter createFastJsonConverter() { + //创建fastJson消息转换器 + FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); + //升级最新版本需加============================================================= + List supportedMediaTypes = new ArrayList<>(); + supportedMediaTypes.add(MediaType.APPLICATION_JSON); + supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); + supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML); + supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); + supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM); + supportedMediaTypes.add(MediaType.APPLICATION_PDF); + supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML); + supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML); + supportedMediaTypes.add(MediaType.APPLICATION_XML); + supportedMediaTypes.add(MediaType.IMAGE_GIF); + supportedMediaTypes.add(MediaType.IMAGE_JPEG); + supportedMediaTypes.add(MediaType.IMAGE_PNG); + supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM); + supportedMediaTypes.add(MediaType.TEXT_HTML); + supportedMediaTypes.add(MediaType.TEXT_MARKDOWN); + supportedMediaTypes.add(MediaType.TEXT_PLAIN); + supportedMediaTypes.add(MediaType.TEXT_XML); + fastConverter.setSupportedMediaTypes(supportedMediaTypes); + + //创建配置类 + FastJsonConfig fastJsonConfig = new FastJsonConfig(); + //修改配置返回内容的过滤 + //WriteNullListAsEmpty :List字段如果为null,输出为[],而非null + //WriteNullStringAsEmpty : 字符类型字段如果为null,输出为"",而非null + //DisableCircularReferenceDetect :消除对同一对象循环引用的问题,默认为false(如果不配置有可能会进入死循环) + //WriteNullBooleanAsFalse:Boolean字段如果为null,输出为false,而非null + //WriteMapNullValue:是否输出值为null的字段,默认为false + fastJsonConfig.setSerializerFeatures( + SerializerFeature.DisableCircularReferenceDetect, + SerializerFeature.WriteMapNullValue + ); + fastConverter.setFastJsonConfig(fastJsonConfig); + + return fastConverter; + } + + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudAutoConfiguration.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudAutoConfiguration.java new file mode 100644 index 0000000..63970f5 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudAutoConfiguration.java @@ -0,0 +1,171 @@ +/* + * Copyright © 2015-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.lingniu.framework.plugin.microservice.common.init; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig; +import cn.lingniu.framework.plugin.microservice.common.core.ErrorCommonResultHandler; +import cn.lingniu.framework.plugin.microservice.common.core.decoder.LingniuErrorDecoder; +import cn.lingniu.framework.plugin.microservice.common.core.decoder.LingniuFeignDecoder; +import cn.lingniu.framework.plugin.microservice.common.core.interceptor.CharlesRequestInterceptor; +import cn.lingniu.framework.plugin.microservice.common.core.interceptor.FeignLoggerInterceptor; +import cn.lingniu.framework.plugin.util.json.JsonUtil; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import feign.Client; +import feign.Feign; +import feign.Request; +import feign.Retryer; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import feign.form.FormEncoder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.cloud.openfeign.support.SpringDecoder; +import org.springframework.cloud.openfeign.support.SpringEncoder; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Scope; +import org.springframework.core.env.Environment; +import org.springframework.web.client.RestTemplate; + +import java.util.Optional; + +/** + * LingniuCloudAutoConfiguration配置 + */ +@Configuration +@Slf4j +@EnableFeignClients(basePackages = {"cn.lingniu.framework.plugin.microservice.common", "cn.lingniu"}) +@ConditionalOnClass(Feign.class) +@AutoConfigureBefore(FeignAutoConfiguration.class) +@EnableConfigurationProperties({MircroServiceConfig.class}) +@AutoConfigureAfter(FrameworkWebConfig.class) +@RequiredArgsConstructor +@Import({OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class, HttpMessageFastJsonConverterConfiguration.class}) +public class LingniuCloudAutoConfiguration implements EnvironmentAware { + + private static final long PERIOD = 500L; + private static final int MAX_PERIOD = 1000; + private static final int MAX_ATTEMPTS = 2; + private final ObjectFactory messageConverters; + private final MircroServiceConfig mircroServiceConfig; + private final FrameworkWebConfig frameworkWebConfig; + private Environment environment; + + @Bean + public ErrorDecoder getLingniuErrorDecoder(ErrorCommonResultHandler errorCommonResultHandler) { + return new LingniuErrorDecoder(errorCommonResultHandler); + } + + @Bean + @ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "json-converter", havingValue = "jackson", matchIfMissing = true) + public Decoder getLingniuFeignDecoder() { + return new LingniuFeignDecoder(new SpringDecoder(messageConverters)); + } + + @Bean + @ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "json-converter", havingValue = "jackson", matchIfMissing = true) + public Encoder getFormEncoder() { + return new FormEncoder(new SpringEncoder(messageConverters)); + } + + @Bean + @ConditionalOnMissingBean + public ErrorCommonResultHandler errorHandler() { + return (b) -> JsonUtil.json2Bean(b, CommonResult.class); + } + + @Bean + public CharlesRequestInterceptor charlesRequestInterceptor() { + return new CharlesRequestInterceptor(); + } + + @Bean + public FeignLoggerInterceptor headersInterceptor() { + return new FeignLoggerInterceptor(); + } + + /** + * feignBuilder创建 + */ + @Bean + @Scope("prototype") + public Feign.Builder feignBuilder(ErrorCommonResultHandler handler, Optional retryer, Client restClient, Request.Options options) { + Feign.Builder builder = Feign.builder() + .errorDecoder(new LingniuErrorDecoder(handler)) + .options(options) + .logLevel(mircroServiceConfig.getFeign().getLoggerLevel()) + .retryer(retryer.isPresent() ? retryer.get() : Retryer.NEVER_RETRY) + .client(restClient); + return builder; + } + + @Bean + public Request.Options options() { + return new Request.Options(mircroServiceConfig.getConnectTimeout(), mircroServiceConfig.getReadTimeout()); + } + + /** + * todo 后续可扩展 + * 创建 RestTemplate 实例 + * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder} + */ + @Bean + @ConditionalOnMissingBean + @LoadBalanced + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.build(); + } + + /** + * feignRetryer实例化 + * + * @return feignRetryer实例 + */ + @Bean + @ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "feign-retryer", havingValue = "true", matchIfMissing = false) + public Retryer feignRetryer() { + return new Retryer.Default(ObjectEmptyUtils.isEmpty(mircroServiceConfig.getFeign().getPeriod()) ? PERIOD : mircroServiceConfig.getFeign().getPeriod(), + ObjectEmptyUtils.isEmpty(mircroServiceConfig.getFeign().getMaxPeriod()) ? MAX_PERIOD : mircroServiceConfig.getFeign().getMaxPeriod(), + ObjectEmptyUtils.isEmpty(mircroServiceConfig.getFeign().getMaxAttempts()) ? MAX_ATTEMPTS : mircroServiceConfig.getFeign().getMaxAttempts()); + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudInit.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudInit.java new file mode 100644 index 0000000..29e0b50 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudInit.java @@ -0,0 +1,31 @@ +package cn.lingniu.framework.plugin.microservice.common.init; + + +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; + + +@Order(Integer.MIN_VALUE + 500) +public class LingniuCloudInit implements org.springframework.context.ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + PropertyUtils.setDefaultInitProperty("spring.main.allow-bean-definition-overriding", "true"); + PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[0]", "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure"); + PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[1]", "org.springframework.boot.actuate.autoconfigure.redis.RedisHealthIndicatorAutoConfiguration"); + PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[2]", "org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorAutoConfiguration"); + PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[3]", "org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthIndicatorAutoConfiguration"); + if (environment.getProperty("framework.lingniu.spring.cloud.isOkHttp", "true").equalsIgnoreCase(Boolean.TRUE.toString())) { + PropertyUtils.setDefaultInitProperty("feign.httpclient.enabled", "false"); + PropertyUtils.setDefaultInitProperty("feign.okhttp.enabled", "true"); + } else { + PropertyUtils.setDefaultInitProperty("feign.httpclient.enabled", "true"); + PropertyUtils.setDefaultInitProperty("feign.okhttp.enabled", "false"); + } + } + + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/OkHttpFeignLoadBalancedConfiguration.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/OkHttpFeignLoadBalancedConfiguration.java new file mode 100644 index 0000000..dab17d5 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/OkHttpFeignLoadBalancedConfiguration.java @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package cn.lingniu.framework.plugin.microservice.common.init; + +import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig; +import feign.Client; +import feign.okhttp.OkHttpClient; +import jakarta.annotation.PreDestroy; +import okhttp3.ConnectionPool; +import okhttp3.Interceptor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * OkHttpFeignLoadBalancedConfiguration + */ +@Configuration +@ConditionalOnClass(OkHttpClient.class) +@ConditionalOnProperty(value = "framework.lingniu.spring.cloud.isOkHttp", havingValue = "true", matchIfMissing = true) +class OkHttpFeignLoadBalancedConfiguration { + + @Bean + public Client feignClient(okhttp3.OkHttpClient okHttpClient, + LoadBalancerClient loadBalancerClient, + LoadBalancedRetryFactory loadBalancedRetryFactory, + LoadBalancerClientFactory loadBalancerClientFactory) { + return new RetryableFeignBlockingLoadBalancerClient(new OkHttpClient(okHttpClient), + loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory); + } + + @Configuration + protected static class OkHttpFeignConfiguration { + private okhttp3.OkHttpClient okHttpClient; + + @Bean + public ConnectionPool httpClientConnectionPool(MircroServiceConfig mircroServiceConfig) { + return new ConnectionPool(mircroServiceConfig.getMaxTotal(), + mircroServiceConfig.getOkHttp().getTimeToLive(), mircroServiceConfig.getOkHttp().getTimeToLiveUnit()); + } + + @Bean + public okhttp3.OkHttpClient client(ConnectionPool httpClientConnectionPool, + Optional> okFeignInterceptors, + ConnectionPool connectionPool, MircroServiceConfig mircroServiceConfig) { + okhttp3.OkHttpClient.Builder builder = + new okhttp3.OkHttpClient.Builder(). + connectTimeout(mircroServiceConfig.getConnectTimeout(), TimeUnit.MILLISECONDS) + .readTimeout(mircroServiceConfig.getReadTimeout(), TimeUnit.MILLISECONDS) + .writeTimeout(mircroServiceConfig.getOkHttp().getWriteTimeout(), TimeUnit.MILLISECONDS) + .followRedirects(mircroServiceConfig.getOkHttp().getIsFollowRedirects()) + .connectionPool(connectionPool); + okFeignInterceptors.ifPresent(n -> {n.forEach(builder::addInterceptor);}); + this.okHttpClient = builder.build(); + return okHttpClient; + } + + @PreDestroy + public void destroy() { + if (okHttpClient != null) { + okHttpClient.dispatcher().executorService().shutdown(); + okHttpClient.connectionPool().evictAll(); + } + } + } +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..29f78b9 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.context.ApplicationContextInitializer=\ +cn.lingniu.framework.plugin.microservice.common.init.LingniuCloudInit \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..10708a3 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.microservice.common.init.LingniuCloudAutoConfiguration diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/pom.xml b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/pom.xml new file mode 100644 index 0000000..e54cca2 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-microservice-nacos + lingniu-framework-plugin-microservice-nacos + http://maven.apache.org + + UTF-8 + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + nacos-client + com.alibaba.nacos + + + + + nacos-client + com.alibaba.nacos + + + cn.lingniu.framework + lingniu-framework-plugin-core + compile + + + org.springframework.boot + spring-boot-starter-web + compile + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + compile + + + io.reactivex + rxjava + 1.3.8 + compile + + + \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/nacos.md b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/nacos.md new file mode 100644 index 0000000..378cbb8 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/nacos.md @@ -0,0 +1,72 @@ +# 框架核心web应用模块 + + +## 概述 (Overview) + +1. 定位: 基于 Alibaba Nacos 封装,支持服务注册、发现、多中心注册等功能的自动化组件 +2. 核心能力: + * 支持多中心注册与服务发现 +3. 适用场景: + * 公司内部微服务统一注册中心 + * 与 FeignClient 配合实现服务间调用 + + +## 如何配置 + +```yaml +spring: + application: + name: lingniu-framework-demo + profiles: + active: dev + cloud: + nacos: + enabled: true #开启微服务自定义注册,自动获取当前框架信息,启动时间等到metadata中 + discovery: + server-addr: http://nacos-uat-new-inter.xx.net.cn:8848 #注册中心地址 + username: nacos_test #注册中心用户名 + password: nacos_test #注册中心密码 + +``` + + +## 接口提供端 + +```java +@RestController +@RequestMapping +public class GithubApiController { + + @GetMapping("/repos/{owner}/{repo}/contributors") + public CommonResult> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo) { + // 模拟返回贡献者列表 + Contributor contributor1 = new Contributor("user1", 10); + Contributor contributor2 = new Contributor("user2", 5); + List response = new ArrayList<>(); + response.add(contributor1); + response.add(contributor2); + return CommonResult.success(response); + } +} +``` + +## 接口调用方 + +```java +// name = 接口提供方的应用名 +@FeignClient(name = "lingniu-framework-provider-demo" +//外部接口配置url: @FeignClient(name = "github-api", url = "https://api.github.com" +// 方式1 配置fallback +// , fallback = GithubApiClientFallBack.class +// 方式2 配置fallbackFactory +// , fallbackFactory = GithubApiClientFallbackFactory.class +) +public interface DemoFeignClient { + + @GetMapping("/repos/{owner}/{repo}/contributors") + CommonResult> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo); + + @GetMapping("/reposNotFound") + CommonResult> reposNotFound(@RequestParam("owner") String owner, @RequestParam("repo") String repo); +} +``` \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/pom.xml b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/pom.xml new file mode 100644 index 0000000..38264ec --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-prometheus + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + io.micrometer + micrometer-registry-prometheus + + + io.micrometer + micrometer-registry-influx + + + io.prometheus + prometheus-metrics-tracer-initializer + + + org.springframework.boot + spring-boot-starter-aop + provided + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + + + org.springframework.boot + spring-boot-actuator + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework + spring-web + compile + + + org.springframework.cloud + spring-cloud-commons + provided + true + + + jakarta.servlet + jakarta.servlet-api + + + + diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusCounter.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusCounter.java new file mode 100644 index 0000000..1f3c3c2 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusCounter.java @@ -0,0 +1,34 @@ +package cn.lingniu.framework.plugin.prometheus; + +/** + * Counter 类型代表一种样本数据单调递增的指标,即只增不减,除非监控系统发生了重置 + **/ + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PrometheusCounter { + + /** + * 如果name有设置值,使用name作为Metric name + */ + String name() default ""; + /** + * 标签 + */ + String[] labels() default ""; + /** + * SPEL 参数列表 + */ + String[] parameterKeys() default ""; + /** + * 备注 + */ + String memo() default "default"; +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusMetrics.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusMetrics.java new file mode 100644 index 0000000..df21a48 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusMetrics.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.prometheus; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PrometheusMetrics { + + /** + * 默认为空,程序使用method signature作为Metric name + * 如果name有设置值,使用name作为Metric name + */ + String name() default ""; +} \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusSummary.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusSummary.java new file mode 100644 index 0000000..5bf61dc --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusSummary.java @@ -0,0 +1,34 @@ +package cn.lingniu.framework.plugin.prometheus; + + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Summary 类型 + **/ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PrometheusSummary { + + /** + * 如果name有设置值,使用name作为Metric name + */ + String name() default ""; + /** + * 标签 + */ + String[] labels() default ""; + /** + * SPEL 参数列表 + */ + String[] parameterKeys() default ""; + /** + * 备注 + */ + String memo() default "default"; +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/BasePrometheusAspect.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/BasePrometheusAspect.java new file mode 100644 index 0000000..c3786c0 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/BasePrometheusAspect.java @@ -0,0 +1,28 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import io.micrometer.common.annotation.AnnotationHandler; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.Map; + +/** + * 基础类 + **/ +@Slf4j +public abstract class BasePrometheusAspect { + @Autowired + MeterRegistry registry; + @Autowired + Map annotationHandlerMap; + + protected void addExtraTags(Object builder, ProceedingJoinPoint pjp, Object result) { + AnnotationHandler meterTagAnnotationHandler = annotationHandlerMap.get(builder); + if (meterTagAnnotationHandler == null) { + return; + } + meterTagAnnotationHandler.addAnnotatedParameters(builder, pjp); + meterTagAnnotationHandler.addAnnotatedMethodResult(builder, pjp, result); + } +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/DistributionSummaryMeterTagAnnotationHandler.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/DistributionSummaryMeterTagAnnotationHandler.java new file mode 100644 index 0000000..80b330f --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/DistributionSummaryMeterTagAnnotationHandler.java @@ -0,0 +1,52 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.annotation.AnnotationHandler; +import io.micrometer.common.annotation.NoOpValueResolver; +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.common.annotation.ValueResolver; +import io.micrometer.common.util.StringUtils; +import io.micrometer.core.aop.MeterTag; +import io.micrometer.core.instrument.DistributionSummary; + +import java.util.function.Function; + +/** + * DistributionSummary MeterTag AnnotationHandler + */ +public class DistributionSummaryMeterTagAnnotationHandler extends AnnotationHandler { + + public DistributionSummaryMeterTagAnnotationHandler( + Function, ? extends ValueResolver> resolverProvider, + Function, ? extends ValueExpressionResolver> expressionResolverProvider) { + super((keyValue, builder) -> builder.tag(keyValue.getKey(), keyValue.getValue()), resolverProvider, + expressionResolverProvider, MeterTag.class, (annotation, o) -> { + if (!(annotation instanceof MeterTag)) { + return null; + } + MeterTag meterTag = (MeterTag) annotation; + return KeyValue.of(resolveTagKey(meterTag), + resolveTagValue(meterTag, o, resolverProvider, expressionResolverProvider)); + }); + } + + static String resolveTagKey(MeterTag annotation) { + return StringUtils.isNotBlank(annotation.value()) ? annotation.value() : annotation.key(); + } + + static String resolveTagValue(MeterTag annotation, Object argument, + Function, ? extends ValueResolver> resolverProvider, + Function, ? extends ValueExpressionResolver> expressionResolverProvider) { + String value = null; + if (annotation.resolver() != NoOpValueResolver.class) { + ValueResolver valueResolver = resolverProvider.apply(annotation.resolver()); + value = valueResolver.resolve(argument); + } else if (StringUtils.isNotBlank(annotation.expression())) { + value = expressionResolverProvider.apply(ValueExpressionResolver.class) + .resolve(annotation.expression(), argument); + } else if (argument != null) { + value = argument.toString(); + } + return value == null ? "" : value; + } +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusCounterAspect.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusCounterAspect.java new file mode 100644 index 0000000..fdbce9c --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusCounterAspect.java @@ -0,0 +1,55 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import cn.lingniu.framework.plugin.prometheus.PrometheusCounter; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Tag; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.expression.EvaluationContext; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * Count实现类 + **/ +@Slf4j +@Aspect +public class PrometheusCounterAspect extends BasePrometheusAspect { + + + @Pointcut("@annotation(cn.lingniu.framework.plugin.prometheus.PrometheusCounter)") + public void prometheusCounterAop() { + } + + @Around("prometheusCounterAop() && @annotation(prometheusCounter)") + public Object prometheusCounterAspect(ProceedingJoinPoint point, PrometheusCounter prometheusCounter) throws Throwable { + MethodSignature signature = (MethodSignature) point.getSignature(); + Object val = null; + Method signatureMethod = signature.getMethod(); + String countName = ObjectEmptyUtils.isEmpty(prometheusCounter.name()) ? signatureMethod.getName() : prometheusCounter.name(); + boolean success = true; + try { + val = point.proceed(); + } catch (Exception ex) { + success = false; + throw ex; + } finally { + Counter.Builder builder = Counter.builder(countName) + .tag("response", success ? "SUCCESS" : "FAILED") + .tags(prometheusCounter.labels()) + .description(prometheusCounter.memo()); + addExtraTags(builder, point, val); + builder.register(registry).count(); + } + return val; + } + + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusMetricsAspect.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusMetricsAspect.java new file mode 100644 index 0000000..2d796eb --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusMetricsAspect.java @@ -0,0 +1,65 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import cn.lingniu.framework.plugin.prometheus.PrometheusMetrics; +import io.prometheus.metrics.core.datapoints.Timer; +import io.prometheus.metrics.core.metrics.Counter; +import io.prometheus.metrics.core.metrics.Histogram; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Aspect +@Component +public class PrometheusMetricsAspect { + + private static final Counter requestTotal = Counter.builder().name("couter_all").labelNames("api").help + ("total request couter of api").register(); + private static final Counter requestError = Counter.builder().name("couter_error").labelNames("api").help + ("response Error couter of api").register(); + private static final Histogram histogram = Histogram.builder().name("histogram_consuming").labelNames("api").help + ("response consuming of api").register(); + + + @Pointcut("@annotation(cn.lingniu.framework.plugin.prometheus.PrometheusMetrics)") + public void prometheusMetricsAop() { + } + + @Around(value = "prometheusMetricsAop() && @annotation(prometheusMetrics)") + public Object prometheusMetricsAspect(ProceedingJoinPoint joinPoint, PrometheusMetrics prometheusMetrics) throws Throwable { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + PrometheusMetrics annotation = methodSignature.getMethod().getAnnotation(PrometheusMetrics.class); + if (annotation != null) { + String name; + if (StringUtils.isNotEmpty(annotation.name())) { + name = annotation.name(); + } else { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) + .getRequest(); + name = request.getRequestURI(); + } + requestTotal.labelValues(name).inc(); + Timer requestTimer = histogram.labelValues(name).startTimer(); + Object object; + try { + object = joinPoint.proceed(); + } catch (Exception e) { + requestError.labelValues(name).inc(); + throw e; + } finally { + requestTimer.observeDuration(); + } + return object; + } else { + return joinPoint.proceed(); + } + } + + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusSummaryAspect.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusSummaryAspect.java new file mode 100644 index 0000000..c4134f9 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusSummaryAspect.java @@ -0,0 +1,58 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import cn.lingniu.framework.plugin.prometheus.PrometheusSummary; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Tag; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.expression.EvaluationContext; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * @description: Summary实现类 + **/ +@Slf4j +@Aspect +public class PrometheusSummaryAspect extends BasePrometheusAspect { + + @Pointcut("@annotation(cn.lingniu.framework.plugin.prometheus.PrometheusSummary)") + public void prometheusSummaryAop() { + } + + + @Around("prometheusSummaryAop() && @annotation(prometheusSummary)") + public Object prometheusSummaryAspect(ProceedingJoinPoint point, PrometheusSummary prometheusSummary) throws Throwable { + MethodSignature signature = (MethodSignature) point.getSignature(); + Method signatureMethod = signature.getMethod(); + Object val = null; + long startTime = System.currentTimeMillis(); + String summaryName = ObjectEmptyUtils.isEmpty(prometheusSummary.name()) ? signatureMethod.getName() : prometheusSummary.name(); + boolean success = true; + try { + val = point.proceed(); + } catch (Exception ex) { + success = false; + throw ex; + } finally { + DistributionSummary.Builder builder = DistributionSummary.builder(summaryName) + .tag("response", success ? "SUCCESS" : "FAILED") + .tags(prometheusSummary.labels()) + .description(prometheusSummary.memo()); + addExtraTags(builder, point, val); + builder.register(registry).record(System.currentTimeMillis() - startTime); + } + return val; + } + + + + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/config/PrometheusConfig.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/config/PrometheusConfig.java new file mode 100644 index 0000000..6a7384f --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/config/PrometheusConfig.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.prometheus.config; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@Data +@Accessors(chain = true) +@ConfigurationProperties(PrometheusConfig.PRE_FIX) +public class PrometheusConfig { + + public static final String PRE_FIX = "framework.lingniu.prometheus"; + + /** + * 是否启用 + */ + private Boolean enabled = true; + /** + * 端点暴露端口,对外应用接口必须指定 + */ + private Integer port = 30290; + /** + * 是否特殊节点:一般当同机器部署多应用时启用,true时才允许启用自定义管理端点端口 + */ + private Boolean allowAssignPort = false; + /** + * 端点暴露 + */ + private String exposures = "env, health, info, metrics, prometheus, threaddump"; + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusConfiguration.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusConfiguration.java new file mode 100644 index 0000000..33734c0 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusConfiguration.java @@ -0,0 +1,118 @@ +package cn.lingniu.framework.plugin.prometheus.init; + + +import cn.lingniu.framework.plugin.prometheus.aspect.DistributionSummaryMeterTagAnnotationHandler; +import cn.lingniu.framework.plugin.prometheus.aspect.PrometheusCounterAspect; +import cn.lingniu.framework.plugin.prometheus.aspect.PrometheusMetricsAspect; +import cn.lingniu.framework.plugin.prometheus.aspect.PrometheusSummaryAspect; +import cn.lingniu.framework.plugin.prometheus.config.PrometheusConfig; +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import io.micrometer.common.annotation.AnnotationHandler; +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.common.annotation.ValueResolver; +import io.micrometer.core.aop.CountedMeterTagAnnotationHandler; +import io.micrometer.core.aop.MeterTagAnnotationHandler; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + + +@Configuration +@Import({PrometheusCounterAspect.class, PrometheusMetricsAspect.class, PrometheusSummaryAspect.class}) +@EnableConfigurationProperties({PrometheusConfig.class}) +@Slf4j +public class PrometheusConfiguration implements CommandLineRunner { + + @Autowired + PrometheusConfig prometheusConfig; + @Value("${spring.application.name: 填写项目名称}") + private String applicationName; + + @Bean + MeterRegistryCustomizer appMetricsCommonTags() { + return registry -> registry.config().commonTags("application", applicationName); + } + + @Override + public void run(String... args) { + if (log.isInfoEnabled()) { + try { + String port = (String) PropertyUtils.getProperty("management.server.port"); + // 验证端口号格式 + if (port != null && !port.trim().isEmpty()) { + try { + int portNum = Integer.parseInt(port.trim()); + if (portNum < 1 || portNum > 65535) { + log.warn("Invalid management server port configuration: {}", port); + port = "unknown"; + } + } catch (NumberFormatException e) { + log.warn("Management server port is not a valid number: {}", port); + port = "invalid"; + } + } else { + port = "not configured"; + } + log.info("\r\n\t\t框架已开启Prometheus监控,信息收集端口:{},请至Grafana查询服务监控信息 ! ", port); + } catch (Exception e) { + log.warn("Failed to get management server port, error: {}", e.getMessage()); + } + } + } + + @Bean + @ConditionalOnMissingBean + Function, ? extends ValueResolver> resolverProvider() { + return aClass -> Object::toString; + } + + @Bean + @ConditionalOnMissingBean + Function, ? extends ValueExpressionResolver> expressionResolverProvider() { + return aClass -> new ValueExpressionResolver() { + @Override + public String resolve(String expression, Object parameter) { + try { + SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + ExpressionParser expressionParser = new SpelExpressionParser(); + Expression expressionToEvaluate = expressionParser.parseExpression(expression); + return expressionToEvaluate.getValue(context, parameter, String.class); + } catch (Exception ex) { + log.error("Unable to evaluate SpEL expression {}", expression, ex); + return null; + } + } + }; + } + + @Bean + Map annotationHandlerMap(Function, ? extends ValueResolver> resolverProvider, + Function, ? extends ValueExpressionResolver> expressionResolverProvider) { + Map map = new HashMap<>(8); + map.put(Counter.Builder.class, new CountedMeterTagAnnotationHandler(resolverProvider, expressionResolverProvider)); + map.put(DistributionSummary.Builder.class, new DistributionSummaryMeterTagAnnotationHandler(resolverProvider, expressionResolverProvider)); + map.put(Timer.Builder.class, new MeterTagAnnotationHandler(resolverProvider, expressionResolverProvider)); + return map; + } + + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusInit.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusInit.java new file mode 100644 index 0000000..138ed5b --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusInit.java @@ -0,0 +1,116 @@ +package cn.lingniu.framework.plugin.prometheus.init; + + +import cn.hutool.core.util.RandomUtil; +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.util.string.StringUtil; +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Profiles; + + +@Slf4j +@Order(Integer.MIN_VALUE + 400) +public class PrometheusInit implements ApplicationContextInitializer { + //server port + public static final String SERVER_PORT_PROPERTY = "server.port"; + //默认管理端点端口 + public static final Integer DEFAULT_ACTUATOR_PORT = 30290; + private static final String DEFAULT_ENDPOINT = "prometheus,feign,metrics"; + private static boolean initialized = false; + private String applicationName; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + if (applicationContext == null) { + throw new IllegalArgumentException("Application context cannot be null"); + } + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + if (environment == null) { + throw new IllegalStateException("Environment cannot be null"); + } + applicationName = environment.getProperty(CommonConstant.SPRING_APP_NAME_KEY); + if (applicationName == null || applicationName.trim().isEmpty()) { + throw new IllegalStateException("Application name property is required"); + } + String profile = environment.getProperty(CommonConstant.ACTIVE_PROFILES_PROPERTY); + // profile can be null, which is acceptable + this.initializeSystemProperty(environment, applicationContext, applicationName, profile); + } + + void initializeSystemProperty(ConfigurableEnvironment environment, ConfigurableApplicationContext applicationContext, String appName, String profile) { + // 使用volatile确保多线程环境下的可见性 + if (isInitialized()) { + return; + } + // 检查prometheus是否启用 + String prometheusEnabled = environment.getProperty("framework.lingniu.prometheus.enabled", "true"); + if (Boolean.FALSE.toString().equalsIgnoreCase(prometheusEnabled)) { + return; + } + Integer serverPort = environment.getProperty(SERVER_PORT_PROPERTY, Integer.class, 3001); + Integer configPort = determineConfigPort(environment, serverPort); + // 确保配置端口不与服务器端口冲突 + if (configPort.equals(serverPort)) { + configPort = getAvailableRandomPort(environment, serverPort); + } + setInitialized(true); + setManagementProperties(environment, configPort); + } + + private Integer determineConfigPort(ConfigurableEnvironment environment, Integer serverPort) { + Integer defaultPort = DEFAULT_ACTUATOR_PORT; + Boolean allowAssignPort = environment.acceptsProfiles(Profiles.of("sit", "dev", "uat")) || + environment.getProperty("framework.lingniu.prometheus.allow-assign-port", Boolean.class, false); + if (allowAssignPort) { + return environment.getProperty("framework.lingniu.prometheus.port", Integer.class, defaultPort); + } + return defaultPort; + } + + private Integer getAvailableRandomPort(ConfigurableEnvironment environment, Integer serverPort) { + Integer randomPort; + int attempts = 0; + int maxAttempts = 10; // 防止无限循环 + do { + randomPort = RandomUtil.randomInt(2000, 5000); + attempts++; + if (attempts >= maxAttempts) { + throw new RuntimeException("Failed to find available port after " + maxAttempts + " attempts"); + } + } while (randomPort.equals(serverPort)); + + return randomPort; + } + + private void setManagementProperties(ConfigurableEnvironment environment, Integer configPort) { + PropertyUtils.setDefaultInitProperty("management.server.port", configPort.toString()); + PropertyUtils.setDefaultInitProperty("management.endpoint.health.show-details", "always"); + String resultIncludes = buildEndpointIncludes(environment); + PropertyUtils.setDefaultInitProperty("management.endpoints.web.exposure.include", resultIncludes); + } + + private String buildEndpointIncludes(ConfigurableEnvironment environment) { + String resultIncludes = DEFAULT_ENDPOINT; + String configIncludes = environment.getProperty("framework.lingniu.prometheus.exposures"); + if (!StringUtil.isEmpty(configIncludes)) { + resultIncludes = resultIncludes + "," + configIncludes; + } + return resultIncludes; + } + + + private boolean isInitialized() { + return initialized; + } + + private void setInitialized(boolean value) { + initialized = value; + } + + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..7c3f9cf --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.context.ApplicationContextInitializer=\ +cn.lingniu.framework.plugin.prometheus.init.PrometheusInit \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..1feb6cd --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.prometheus.init.PrometheusConfiguration \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/prometheus.md b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/prometheus.md new file mode 100644 index 0000000..0dfc200 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/prometheus.md @@ -0,0 +1,34 @@ +# 【重要】prometheus graf...更多使用请参考官方资料 + +## 概述 (Overview) + +1. 定位:集成 prometheus监控框架的应用级监控组件,性能指标统计及异常监控能 +2. 核心能力 + * 支持自定义业务埋点,记录关键业务流程的性能与异常信息 + * jvm指标数据 + * 与grafana联动,实现监控数据可视化与告警 +3. 适用场景 + * 用接口性能监控(响应时间、成功率) + * 业务流程异常追踪与问题定位 + * 系统整体运行状态监控 + * 业务指标大屏告警 + +## 如何配置--参考:PrometheusConfig + +```yaml +framework: + lingniu: + prometheus: + enabled: true + port: 30290 + allowAssignPort: false +``` + +## 如何使用 + +参考三个注解、或使用原生方法使用 +- PrometheusCounter +- PrometheusMetrics +- PrometheusSummary + + diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/pom.xml b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/pom.xml new file mode 100644 index 0000000..b844c54 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-skywalking + ${project.artifactId} + + 9.5.0 + + + + + org.apache.skywalking + apm-toolkit-trace + ${skywalking.version} + + + org.apache.skywalking + apm-toolkit-opentracing + ${skywalking.version} + + + + + + diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/src/main/resources/skywalking.md b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/src/main/resources/skywalking.md new file mode 100644 index 0000000..77152b1 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/src/main/resources/skywalking.md @@ -0,0 +1,4 @@ +# skywalking agent对接可参考官方文档 +# skywalking 服务度搭建对接可参考官方文档 + +# https://skywalking.apache.org/docs/skywalking-java/v9.2.0/en/setup/service-agent/java-agent/readme/ \ No newline at end of file diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/pom.xml b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/pom.xml new file mode 100644 index 0000000..197d407 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-rocketmq + lingniu-framework-plugin-rocketmq + http://maven.apache.org + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + org.springframework.boot + spring-boot-autoconfigure + provided + + + com.alibaba + fastjson + + + org.apache.rocketmq + rocketmq-client + + + org.slf4j + slf4j-api + + + com.alibaba + fastjson + + + + + com.alibaba + fastjson + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.springframework.boot + spring-boot-autoconfigure + true + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + org.apache.rocketmq + rocketmq-tools + provided + true + + + diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqConsumer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqConsumer.java new file mode 100644 index 0000000..6a8e8d3 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqConsumer.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.rocketmq; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface RocketMqConsumer { + + /** + * 自定义消费端名称 + */ + String value(); + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqProducer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqProducer.java new file mode 100644 index 0000000..d564a2c --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqProducer.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.rocketmq; + +import cn.lingniu.framework.plugin.rocketmq.core.producer.GeneralRocketMqProducerInner; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; + +/** + * 生产基类 + */ +@Slf4j +public class RocketMqProducer extends GeneralRocketMqProducerInner { +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumeMode.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumeMode.java new file mode 100644 index 0000000..36c01c9 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumeMode.java @@ -0,0 +1,15 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +/** + * 消费者模式 + */ +public enum ConsumeMode { + /** + * 并发消费 + */ + CONCURRENTLY, + /** + * 顺序消费 + */ + ORDERLY +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumerProperties.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumerProperties.java new file mode 100644 index 0000000..bebbb03 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumerProperties.java @@ -0,0 +1,109 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +import lombok.Data; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; + +/** + * 消费端配置信息 + */ +@Data +public class ConsumerProperties { + + /** + * RocketMQ nameServer* + */ + private String nameServer; + /** + * 消费应用BeanName + */ + private String consumerBeanName; + /** + * Topic + */ + private String topic; + /** + * 消费组 + */ + private String consumerGroup; + /** + * 消息表达式类型 默认按照TAG + */ + private SelectorType selectorType = SelectorType.TAG; + /** + * 表达式 + */ + private String selectorExpress; + /** + * 默认同时消费 + */ + private ConsumeMode consumeMode = ConsumeMode.CONCURRENTLY; + /** + * 默认集群消费 + */ + private MessageModel messageModel = MessageModel.CLUSTERING; + /** + * 消费者最小线程 + */ + private Integer consumeThreadMin; + /** + * 消费者最大线程 + */ + private Integer consumeThreadMax; + /** + * 批量消费数量 + * @see MessageListenerConcurrently#consumeMessage + */ + private int consumeMessageBatchMaxSize; + /** + * 批量拉取消息数量 + */ + private int pullBatchSize = 32; + + /** + * 是否批量处理方法 + */ + private Boolean consumerbatchMode; + /** + * 消息的最大重试次数 + */ + private int maxRetryTimes = 15; + /** + * 客户端名称 + */ + private String clientUnitName; + /** + * 消息消费超时时间 + */ + private long mqConsumerTimeOut = 3000; + + public ConsumerProperties() { + this("", "", "", "*", 20, 64, 1, 32, 4); + this.nameServer = ""; + } + + public ConsumerProperties( + String consumerGroup, + String consumerBeanName, + String topic, + String selectorExpress, + int consumeThreadMin, + int consumeThreadMax, + int consumeMessageBatchMaxSize, + int pullBatchSize, + int maxRetryTimes + ) { + this.selectorType = SelectorType.TAG; + this.messageModel = MessageModel.CLUSTERING; + this.consumerGroup = consumerGroup; + this.topic = topic; + this.consumerBeanName = consumerBeanName; + this.selectorExpress = selectorExpress; + this.consumeThreadMin = consumeThreadMin; + this.consumeThreadMax = consumeThreadMax; + this.consumeMessageBatchMaxSize = consumeMessageBatchMaxSize; + this.consumeMode = ConsumeMode.CONCURRENTLY; + this.pullBatchSize = pullBatchSize; + this.maxRetryTimes = maxRetryTimes; + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ProducerProperties.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ProducerProperties.java new file mode 100644 index 0000000..1cbe8e0 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ProducerProperties.java @@ -0,0 +1,79 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + + +/** + * 生产端配置信息--很多优化参数可以扩展 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ProducerProperties { + + /** + * RocketMQ nameServer + */ + String nameServer; + /** + * name of producer + */ + private String group; + /** + * 生产端Topic + */ + private String topic; + private String clientUnitName; + /** + * millis of send message timeout + */ + private int sendMsgTimeout = 3000; + /** + * Compress message body threshold, namely, message body larger than 4k will be compressed on default. + */ + private int compressMsgBodyOverHowmuch = 1024 * 4; + /** + *

Maximum number of retry to perform internally before claiming sending failure in synchronous mode.

+ * This may potentially cause message duplication which is up to application developers to resolve. + */ + private int retryTimesWhenSendFailed = 2; + /** + *

Maximum number of retry to perform internally before claiming sending failure in asynchronous mode.

+ * This may potentially cause message duplication which is up to application developers to resolve. + */ + private int retryTimesWhenSendAsyncFailed = 2; + /** + * Indicate whether to retry another broker on sending failure internally. + */ + private boolean retryAnotherBrokerWhenNotStoreOk = false; + /** + * 是否开启异常切换 + */ + private boolean sendLatencyFaultEnable = false; + /** + * Maximum allowed message size in bytes. + */ + private int maxMessageSize = 1024 * 1024 * 4; // 4M + /** + * 事务消息线程配置 + */ + private Transaction transaction = new Transaction(); + /** + * 是否事务型 + */ + private Boolean isTransactionMQ = false; + + @Getter + @Setter + public static class Transaction { + private int corePoolSize = 5; + private int maximumPoolSize = 10; + private int keepAliveTime = 200; + private int queueCapacity = 2000; + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/RocketMqConfig.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/RocketMqConfig.java new file mode 100644 index 0000000..1e34ba6 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/RocketMqConfig.java @@ -0,0 +1,27 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.HashMap; +import java.util.Map; + + +@Setter +@Getter +@ConfigurationProperties(prefix = RocketMqConfig.PRE_FIX) +public class RocketMqConfig { + + public final static String PRE_FIX = "framework.lingniu.rocketmq"; + + @Getter + @Setter + private Map producers = new HashMap<>(); + + @Getter + @Setter + private Map consumers = new HashMap<>(); + + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/SelectorType.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/SelectorType.java new file mode 100644 index 0000000..c79a1c6 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/SelectorType.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +import org.apache.rocketmq.common.filter.ExpressionType; + +/** + * 过滤类型 + */ +public enum SelectorType { + + /** + * @see ExpressionType#TAG + */ + TAG, + + /** + * @see ExpressionType#SQL92 + */ + SQL92 +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodyJacksonSerializer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodyJacksonSerializer.java new file mode 100644 index 0000000..41a7129 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodyJacksonSerializer.java @@ -0,0 +1,63 @@ +package cn.lingniu.framework.plugin.rocketmq.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +/** + * Jackson 序列化 + **/ +@Slf4j +public class RocketMqBodyJacksonSerializer implements RocketMqBodySerializer { + + public static final String DEFAULT_ZONE = "GMT+08:00"; + public static final String DEFAULT_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; + private static ObjectMapper objectMapper; + + static { + objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + SimpleDateFormat smt = new SimpleDateFormat(DEFAULT_FORMAT); + TimeZone timeZone = TimeZone.getTimeZone(DEFAULT_ZONE); + objectMapper.setDateFormat(smt); + objectMapper.setTimeZone(timeZone); + objectMapper.registerModule(new ParameterNamesModule()).registerModule(new Jdk8Module()).registerModule(new JavaTimeModule()); + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + objectMapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false); + objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + } + + + @Override + public byte[] serialize(Object t) { + try { + return objectMapper.writeValueAsBytes(t); + } catch (Exception ex) { + log.error("JacksonMq序列化异常:", ex); + throw new IllegalArgumentException(ex); + } + } + + + @Override + public T deserialize(byte[] bytes, Class clazz) { + try { + return objectMapper.readValue(bytes, clazz); + } catch (Exception ex) { + log.error(String.format("JacksonMq反序列化异常,原始Json: %s", new String(bytes, StandardCharsets.UTF_8)), ex); + throw new IllegalArgumentException(ex); + } + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodySerializer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodySerializer.java new file mode 100644 index 0000000..52a3564 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodySerializer.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.rocketmq.core; + + +/** + * RocketMq 序列化 + **/ +public interface RocketMqBodySerializer { + + byte[] serialize(Object t); + + T deserialize(byte[] bytes, Class clazz); + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/DefaultRocketMqListenerContainer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/DefaultRocketMqListenerContainer.java new file mode 100644 index 0000000..0b1d20b --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/DefaultRocketMqListenerContainer.java @@ -0,0 +1,329 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer; + +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.MRocketMqListener; +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.RocketMqListener; +import cn.lingniu.framework.plugin.rocketmq.config.ConsumeMode; +import cn.lingniu.framework.plugin.rocketmq.config.SelectorType; +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodySerializer; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + + +@Slf4j +@SuppressWarnings("all") +public class DefaultRocketMqListenerContainer implements InitializingBean, RocketMqListenerContainer { + + @Setter + @Getter + private long suspendCurrentQueueTimeMillis = 1000; + /** + * Message consume retry strategy
-1,no retry,put into DLQ directly
0,broker control retry frequency
+ * >0,client control retry frequency + */ + @Setter + @Getter + private int delayLevelWhenNextConsume = 0; + @Setter + @Getter + private String consumerGroup; + @Setter + @Getter + private String nameServer; + @Setter + @Getter + private String topic; + @Setter + @Getter + private ConsumeMode consumeMode = ConsumeMode.CONCURRENTLY; + @Setter + @Getter + private SelectorType selectorType = SelectorType.TAG; + @Setter + @Getter + private String selectorExpress = "*"; + @Setter + @Getter + private MessageModel messageModel = MessageModel.CLUSTERING; + /** + * 最大线程数 + */ + @Setter + @Getter + private int consumeThreadMax = 20; + /** + * 最小线程数 + */ + @Setter + @Getter + private int consumeThreadMin = 20; + /** + * 最大重试次数 + */ + @Getter + @Setter + private int maxRetryTimes = 15; + /** + * 批量消费数量 + */ + @Setter + @Getter + private int consumeMessageBatchMaxSize = 1; + /** + * 批量拉取消息数量 + */ + @Setter + @Getter + private int pullBatchSize = 32; + @Setter + @Getter + private int pollNameServerInterval = 1000 * 30; + @Setter + @Getter + private int heartbeatBrokerInterval = 1000 * 30; + @Setter + @Getter + private Boolean isBatchMode; + @Getter + @Setter + private String charset = "UTF-8"; + @Getter + @Setter + private String clientUnitName; + @Getter + @Setter + private RocketMqBodySerializer serializer; + @Setter + @Getter + private boolean started; + @Setter + @Getter + private long mqConsumerTimeOut = 3000; + @Setter + private RocketMqListener rocketMQListener; + private DefaultMQPushConsumer consumer; + private Class messageType; + + @Override + public void setupMessageListener(RocketMqListener rocketMQListener) { + this.rocketMQListener = rocketMQListener; + } + + @Override + public void destroy() { + this.setStarted(false); + if (Objects.nonNull(consumer)) { + consumer.shutdown(); + } + log.info("DefaultRocketMQListenerContainer destroyed, {}", this.toString()); + } + + + public synchronized void start() throws MQClientException { + if (this.isStarted()) { + throw new IllegalStateException("DefaultRocketMQListenerContainer already started. " + this.toString()); + } + initRocketMQPushConsumer(); + this.messageType = getMessageType(); + consumer.start(); + this.setStarted(true); + if (log.isInfoEnabled()) { + log.info("开启TOPIC: {} 监听端,消费组:{},消息类型:{},线程信息:{} ~ {},消费模式:{},过虑类型:{},标签:{}", this.getTopic(), this.getConsumerGroup(), messageType.getSimpleName(), this.getConsumeThreadMin() + , this.getConsumeThreadMax(), this.consumeMode.name(), this.getSelectorType().name(), this.selectorExpress); + } + } + + public class DefaultMessageListenerConcurrently implements MessageListenerConcurrently { + private final String topic; + private final String group; + public DefaultMessageListenerConcurrently(String topic, String group) { + this.topic = topic; + this.group = group; + } + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + long now = System.currentTimeMillis(); + List outRetrys = msgs.stream().filter(n -> n.getReconsumeTimes() > maxRetryTimes).collect(Collectors.toList()); + msgs = msgs.stream().filter(n -> n.getReconsumeTimes() <= maxRetryTimes).collect(Collectors.toList()); + if (ObjectEmptyUtils.isEmpty(msgs)) { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + Throwable throwable = null; + try { + if (rocketMQListener instanceof MRocketMqListener) { + ((MRocketMqListener) rocketMQListener).onMessage( + msgs.stream().map(m -> { + RocketMqConsumerMessage item = new RocketMqConsumerMessage<>(this.topic); + item.setOriginMsg(m); + item.setMsg(doConvertMessage(m)); + return item; + }).collect(Collectors.toList()) + ); + } else { + msgs.forEach(p -> {rocketMQListener.onMessage(doConvertMessage(p), p);}); + } + } catch (Throwable ex) { + throwable = ex; + log.error(String.format("%s 消费 %s:%s 发生系统异常", this.getClass().getSimpleName(), this.topic, this.group), ex); + context.setDelayLevelWhenNextConsume(delayLevelWhenNextConsume); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } finally { + long cost = System.currentTimeMillis() - now; + if (cost > mqConsumerTimeOut) { + log.error("[consumeMessage-concurrent][topic({}) group({}) 消费超时({}ms)]", topic, group, cost); + } + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + } + + public class DefaultMessageListenerOrderly implements MessageListenerOrderly { + private final String topic; + private final String group; + public DefaultMessageListenerOrderly(String topic, String group) { + this.topic = topic; + this.group = group; + } + + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + long now = System.currentTimeMillis(); + List outRetrys = msgs.stream().filter(n -> n.getReconsumeTimes() > maxRetryTimes).collect(Collectors.toList()); + msgs = msgs.stream().filter(n -> n.getReconsumeTimes() <= maxRetryTimes).collect(Collectors.toList()); + if (ObjectEmptyUtils.isEmpty(msgs)) { + return ConsumeOrderlyStatus.SUCCESS; + } + Throwable throwable = null; + try { + if (rocketMQListener instanceof MRocketMqListener) { + ((MRocketMqListener) rocketMQListener).onMessage( + msgs.stream().map(m -> { + RocketMqConsumerMessage item = new RocketMqConsumerMessage<>(this.topic); + item.setOriginMsg(m); + item.setMsg(doConvertMessage(m)); + return item; + }).collect(Collectors.toList()) + ); + } else { + msgs.forEach(p -> { + rocketMQListener.onMessage(doConvertMessage(p), p); + }); + } + } catch (Throwable ex) { + throwable = ex; + log.error(String.format("%s 消费 %s:%s 发生系统异常", this.getClass().getSimpleName(), this.topic, this.group), ex); + context.setSuspendCurrentQueueTimeMillis(suspendCurrentQueueTimeMillis); + return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; + } finally { + long cost = System.currentTimeMillis() - now; + if (cost > mqConsumerTimeOut) { + log.error("[consumeMessage-order][topic({}) group({}) 消费超时({}ms)]", topic, group, cost); + } + } + return ConsumeOrderlyStatus.SUCCESS; + } + } + + @Override + public void afterPropertiesSet() throws Exception { + start(); + } + + @SuppressWarnings("unchecked") + private Object doConvertMessage(MessageExt messageExt) { + if (Objects.equals(messageType, MessageExt.class)) { + return messageExt; + } else { + if (Objects.equals(messageType, String.class)) { + return new String(messageExt.getBody(), Charset.forName(charset)); + } else { + return serializer.deserialize(messageExt.getBody(), messageType); + } + } + } + + private Class getMessageType() { + if (Proxy.isProxyClass(rocketMQListener.getClass())) { + return rocketMQListener.getClassType(); + } + Type[] interfaces = rocketMQListener.getClass().getGenericInterfaces(); + if (Objects.nonNull(interfaces)) { + for (Type type : interfaces) { + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + if (Objects.equals(parameterizedType.getRawType(), RocketMqListener.class) + || Objects.equals(parameterizedType.getRawType(), MRocketMqListener.class)) { + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (Objects.nonNull(actualTypeArguments) && actualTypeArguments.length > 0) { + return (Class) actualTypeArguments[0]; + } else { + return Object.class; + } + } + } + } + return Object.class; + } else { + return Object.class; + } + } + + private void initRocketMQPushConsumer() throws MQClientException { + Assert.notNull(rocketMQListener, "Property 'rocketMQListener' is required"); + Assert.notNull(consumerGroup, "Property 'consumerGroup' is required"); + Assert.notNull(nameServer, "Property 'nameServer' is required"); + Assert.notNull(topic, "Property 'topic' is required"); + consumer = new DefaultMQPushConsumer(consumerGroup); + consumer.setNamesrvAddr(nameServer); + consumer.setConsumeThreadMax(consumeThreadMax); + consumer.setConsumeThreadMin(consumeThreadMin); + consumer.setPullBatchSize(pullBatchSize); + consumer.setMessageModel(messageModel); + consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize); + consumer.setUnitName(clientUnitName); + switch (selectorType) { + case TAG: + consumer.subscribe(topic, selectorExpress); + break; + case SQL92: + consumer.subscribe(topic, MessageSelector.bySql(selectorExpress)); + break; + default: + throw new IllegalArgumentException("Property 'selectorType' was wrong."); + } + switch (consumeMode) { + case ORDERLY: + consumer.registerMessageListener(new DefaultMessageListenerOrderly(topic, consumerGroup)); + break; + case CONCURRENTLY: + consumer.registerMessageListener(new DefaultMessageListenerConcurrently(topic, consumerGroup)); + break; + default: + throw new IllegalArgumentException("Property 'consumeMode' was wrong."); + } + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqConsumerMessage.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqConsumerMessage.java new file mode 100644 index 0000000..7699333 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqConsumerMessage.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.rocketmq.common.message.MessageExt; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class RocketMqConsumerMessage { + + public RocketMqConsumerMessage(String topic) { + this.topic = topic; + } + + private T msg; + + private String topic; + + private MessageExt originMsg; +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqListenerContainer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqListenerContainer.java new file mode 100644 index 0000000..7b33bbf --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqListenerContainer.java @@ -0,0 +1,10 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer; + +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.RocketMqListener; +import org.springframework.beans.factory.DisposableBean; + + +public interface RocketMqListenerContainer extends DisposableBean { + + void setupMessageListener(RocketMqListener messageListener); +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/GeneralMsgListener.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/GeneralMsgListener.java new file mode 100644 index 0000000..52896a9 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/GeneralMsgListener.java @@ -0,0 +1,55 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer.listener; + +import cn.lingniu.framework.plugin.rocketmq.core.consumer.RocketMqConsumerMessage; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.common.message.MessageExt; +import java.io.Serializable; +import java.lang.reflect.ParameterizedType; +import java.util.List; + +@Slf4j +public abstract class GeneralMsgListener implements MRocketMqListener, RocketMqListener { + + @Getter + private Class messageType; + + /** + * 获取真实参数类型 + */ + public GeneralMsgListener() { + ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass(); + this.messageType = (Class) pt.getActualTypeArguments()[0]; + } + + /** + * 消息消费主体 + * @param msg + */ + protected abstract void onMessage(RocketMqConsumerMessage msg); + + /** + * 解决代理模式范型丢失问题 + */ + @Override + public Class getClassType() { + return this.messageType; + } + + @Override + public void onMessage(List> msgs) { + if (ObjectEmptyUtils.isEmpty(msgs)) { + return; + } + msgs.stream().forEach(n -> onMessage(n)); + } + + @Override + public final void onMessage(T message, MessageExt messageExt) { + RocketMqConsumerMessage msg = new RocketMqConsumerMessage<>(); + msg.setMsg(message); + msg.setOriginMsg(messageExt); + onMessage(msg); + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/MRocketMqListener.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/MRocketMqListener.java new file mode 100644 index 0000000..20b05fb --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/MRocketMqListener.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer.listener; + + +import cn.lingniu.framework.plugin.rocketmq.core.consumer.RocketMqConsumerMessage; + +import java.util.List; + +/** + * 批量消费 + */ +public interface MRocketMqListener extends RocketMqListener { + void onMessage(List> msgs); +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/RocketMqListener.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/RocketMqListener.java new file mode 100644 index 0000000..4cc07c9 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/RocketMqListener.java @@ -0,0 +1,18 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer.listener; + +import org.apache.rocketmq.common.message.MessageExt; + +/** + * @param + */ +public interface RocketMqListener { + + void onMessage(T message, MessageExt messageExt); + + /** + * 消费端处理数据类型 + */ + default Class getClassType() { + return null; + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/GeneralRocketMqProducerInner.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/GeneralRocketMqProducerInner.java new file mode 100644 index 0000000..b709713 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/GeneralRocketMqProducerInner.java @@ -0,0 +1,164 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer; + + +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnFail; +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnSuccess; +import cn.lingniu.framework.plugin.util.json.JsonUtil; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.client.producer.selector.SelectMessageQueueByHash; +import org.apache.rocketmq.common.message.MessageConst; +import org.springframework.beans.factory.DisposableBean; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 生产内部基类 + */ +@Slf4j +public abstract class GeneralRocketMqProducerInner implements RocketMqProducerInner, DisposableBean { + + private RocketMqTemplate rocketMQTemplate; + private String topic; + private String tag; + + @Override + public void setRocketMQTemplate(RocketMqTemplate template, String topic) { + rocketMQTemplate = template; + this.topic = topic; + } + + @Override + public void asyncSend(RocketMqSendMsgBody message, SendMessageOnFail onMsgFail) { + asyncSend(message, (m, r) -> {}, onMsgFail); + } + + @Override + public void asyncSend(RocketMqSendMsgBody message) { + asyncSend(message, (m, r) -> {}, (m, ex) -> {}); + } + + @Override + public void asyncSend(RocketMqSendMsgBody message, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + rocketMQTemplate.asyncSend(ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, + ObjectEmptyUtils.isNotEmpty(message.getMessageTag()) ? message.getMessageTag() : this.tag, + message.getBody(), buildMessageProperties(message), new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + if (null != onMsgSuccess) { + onMsgSuccess.call(message.getBody(), sendResult); + } + if (log.isInfoEnabled() && ObjectEmptyUtils.isNotEmpty(message) && ObjectEmptyUtils.isNotEmpty(message.getMessageKey())) { + log.info("消息:{} 异步写入:{} 成功", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : topic); + } + } + @Override + public void onException(Throwable e) { + log.error(message.getClass().getSimpleName() + " MQ异步写入失败:" + JsonUtil.bean2Json(message), e); + if (null != onMsgFail) { + onMsgFail.call(message.getBody(), e); + } + } + }, rocketMQTemplate.getProducer().getSendMsgTimeout()); + } + + @Override + public SendResult syncSend(RocketMqSendMsgBody message, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + SendResult result = rocketMQTemplate.syncSend(ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, + ObjectEmptyUtils.isNotEmpty(message.getMessageTag()) ? message.getMessageTag() : this.tag, message.getBody(), buildMessageProperties(message)); + if (!result.getSendStatus().equals(SendStatus.SEND_OK)) { + log.error("消息 {} 同步写入 {} 失败,结果:{}", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, result.getSendStatus()); + if (!ObjectEmptyUtils.isEmpty(onMsgFail)) { + onMsgFail.call(message.getBody(), null); + } + } else { + onMsgSuccess.call(message.getBody(), result); + } + return result; + } + + @Override + public SendResult syncSend(RocketMqSendMsgBody message, MessageQueueSelector selector, String selectorKey, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + SendResult result = rocketMQTemplate.syncSend(ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, + ObjectEmptyUtils.isNotEmpty(message.getMessageTag()) ? message.getMessageTag() : this.tag, message.getBody(), buildMessageProperties(message), selector, selectorKey); + if (!result.getSendStatus().equals(SendStatus.SEND_OK)) { + log.error("消息 {} 同步写入 {} 失败,结果:{}", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, result.getSendStatus()); + if (!ObjectEmptyUtils.isEmpty(onMsgFail)) { + onMsgFail.call(message.getBody(), null); + } + } else { + onMsgSuccess.call(message.getBody(), result); + } + return result; + } + + @Override + public SendResult syncSend(RocketMqSendMsgBody message, String selectorKey, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + return syncSend(message, new SelectMessageQueueByHash(), selectorKey, onMsgSuccess, onMsgFail); + } + + @Override + public SendResult syncSend(RocketMqSendMsgBody message) { + return syncSend(message, (m, r) -> {}, null); + } + + + @Override + public SendResult syncSendInTransaction(RocketMqSendMsgBody message, Object arg) { + return syncSendInTransaction(message, arg, (n, r) -> {}, (t, e) -> {}); + } + + @Override + public SendResult syncSendInTransaction(RocketMqSendMsgBody message, Object arg, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + SendResult result = rocketMQTemplate.sendMessageInTransaction(ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, + ObjectEmptyUtils.isNotEmpty(message.getMessageTag()) ? message.getMessageTag() : this.tag, + message.getBody(), buildMessageProperties(message), arg); + if (log.isInfoEnabled() && ObjectEmptyUtils.isNotEmpty(message) && ObjectEmptyUtils.isNotEmpty(message.getMessageKey())) { + log.info("消息:{} 事务写入:{} 成功", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : topic); + } + if (!result.getSendStatus().equals(SendStatus.SEND_OK)) { + log.error("消息 {} 事务写入 {} 失败,结果:{}", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, result.getSendStatus()); + } else { + onMsgSuccess.call(message.getBody(), result); + } + return result; + } + + @Override + public void asyncSend(Collection> messageBodies) { + if (ObjectEmptyUtils.isEmpty(messageBodies)) { + return; + } + RocketMqSendMsgBody message = messageBodies.stream().findAny().get(); + rocketMQTemplate.asyncSend(message.getTopic(), message.getMessageTag(), + messageBodies.stream().filter(ObjectEmptyUtils::isNotEmpty).collect(Collectors.toList()), + n -> ((RocketMqSendMsgBody) n).getBody(), + n -> buildMessageProperties((RocketMqSendMsgBody) n), + rocketMQTemplate.getProducer().getSendMsgTimeout()); + } + + protected final Map buildMessageProperties(RocketMqSendMsgBody message) { + Map properties = new HashMap<>(); + if (ObjectEmptyUtils.isNotEmpty(message.getMessageKey())) { + properties.put(MessageConst.PROPERTY_KEYS, message.getMessageKey()); + } + if (ObjectEmptyUtils.isNotEmpty(message.getDelayLevel())) { + properties.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, message.getDelayLevel()); + } + return properties; + } + + @Override + public void destroy() { + Optional.ofNullable(this.rocketMQTemplate).ifPresent(r -> r.destroy()); + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqProducerInner.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqProducerInner.java new file mode 100644 index 0000000..9b16a55 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqProducerInner.java @@ -0,0 +1,36 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer; + +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnFail; +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnSuccess; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendResult; +import java.io.Serializable; +import java.util.Collection; + +/** + * 生产者接口 + */ +public interface RocketMqProducerInner { + + void setRocketMQTemplate(RocketMqTemplate template, String topic); + + void asyncSend(RocketMqSendMsgBody message, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); + + void asyncSend(RocketMqSendMsgBody message, SendMessageOnFail onMsgFail); + + void asyncSend(RocketMqSendMsgBody message); + + SendResult syncSend(RocketMqSendMsgBody message); + + SendResult syncSend(RocketMqSendMsgBody message, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); + + SendResult syncSend(RocketMqSendMsgBody message, MessageQueueSelector selector, String selectorKey, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); + + SendResult syncSend(RocketMqSendMsgBody message, String selectorKey, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); + + void asyncSend(Collection> msgs); + + SendResult syncSendInTransaction(RocketMqSendMsgBody message, Object args); + + SendResult syncSendInTransaction(RocketMqSendMsgBody message, Object args, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqSendMsgBody.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqSendMsgBody.java new file mode 100644 index 0000000..743b0a2 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqSendMsgBody.java @@ -0,0 +1,27 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * mq 发送消息body + */ +@Data +@Accessors(chain = true) +public class RocketMqSendMsgBody { + + T body; + + String messageKey; + + String messageTag; + + String topic; + /** + * 级别的定时消息如下:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h + */ + Integer delayLevel; + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqTemplate.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqTemplate.java new file mode 100644 index 0000000..e29ca46 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqTemplate.java @@ -0,0 +1,326 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer; + +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodySerializer; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.TransactionSendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageQueue; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * RockMQTemplate + */ +@Slf4j +public class RocketMqTemplate implements InitializingBean, DisposableBean { + + @Getter + @Setter + private DefaultMQProducer producer; + @Getter + @Setter + private RocketMqBodySerializer serializer; + @Getter + @Setter + private String charset = "UTF-8"; + + public SendResult syncSend(Message message) { + return this.syncSend(message, producer.getSendMsgTimeout()); + } + + public SendResult syncSend(Message message, long timeout) { + try { + SendResult sendResult = producer.send(message, timeout); + return sendResult; + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties) { + return this.syncSend(topic, tag, msgObj, properties, this.producer.getSendMsgTimeout()); + } + + /** + * 同步发送消息 + * @param topic + * @param tag + * @param msgObj + * @param properties + * @param timeout + * @return + */ + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + return this.syncSend(message, timeout); + } + + + public void asyncSend(Message message, SendCallback sendCallback) { + this.asyncSend(message, sendCallback, producer.getSendMsgTimeout()); + } + + public void asyncSend(Message message, SendCallback sendCallback, long timeout) { + try { + producer.send(message, sendCallback, timeout); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + public void asyncSend(String topic, String tag, Collection message, Function bodyFun, Function> properties, long timeout) { + List messages = message.stream().filter(ObjectEmptyUtils::isNotEmpty) + .map(n -> createMessage(topic, tag, bodyFun.apply(n), ObjectEmptyUtils.isNotEmpty(properties) ? properties.apply(n) : null)) + .collect(Collectors.toList()); + try { + producer.send(messages, timeout); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + public void asyncSend(String topic, String tag, Object msgObj, Map properties, SendCallback sendCallback) { + this.asyncSend(topic, tag, msgObj, properties, sendCallback, producer.getSendMsgTimeout()); + } + + /** + * 异步发送消息 + * @param topic + * @param tag + * @param msgObj + * @param properties + * @param sendCallback + * @param timeout + */ + public void asyncSend(String topic, String tag, Object msgObj, Map properties, SendCallback sendCallback, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + this.asyncSend(message, sendCallback, timeout); + } + + public void sendOneWay(Message message) { + try { + producer.sendOneway(message); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * 不关心消息是否送达 + * @param topic + * @param tag + * @param msgObj + * @param properties + */ + public void sendOneWay(String topic, String tag, Object msgObj, Map properties) { + Message message = createMessage(topic, tag, msgObj, properties); + sendOneWay(message); + } + + + public SendResult syncSend(Message message, MessageQueue messageQueue) { + return this.syncSend(message, messageQueue, producer.getSendMsgTimeout()); + } + + public SendResult syncSend(Message message, MessageQueue messageQueue, long timeout) { + try { + SendResult sendResult = producer.send(message, messageQueue, timeout); + return sendResult; + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, MessageQueue mq) { + return this.syncSend(topic, tag, msgObj, properties, mq, producer.getSendMsgTimeout()); + } + + /** + * 同步发送到指定队列 + * @param topic + * @param tag + * @param msgObj + * @param properties + * @param mq + * @param timeout + */ + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, MessageQueue mq, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + return this.syncSend(message, mq, timeout); + } + + public void asyncSend(Message message, MessageQueue messageQueue, SendCallback sendCallback) { + this.asyncSend(message, messageQueue, sendCallback, producer.getSendMsgTimeout()); + } + + + public void asyncSend(Message message, MessageQueue messageQueue, SendCallback sendCallback, long timeout) { + try { + producer.send(message, messageQueue, sendCallback, timeout); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * 异步发送到指定队列 + * + * @param topic + * @param tag + * @param msgObj + * @param properties + * @param mq + * @param sendCallback + */ + public void asyncSend(String topic, String tag, Object msgObj, Map properties, MessageQueue mq, SendCallback sendCallback) { + this.asyncSend(topic, tag, msgObj, properties, mq, sendCallback, producer.getSendMsgTimeout()); + } + + + public void asyncSend(String topic, String tag, Object msgObj, Map properties, MessageQueue mq, SendCallback sendCallback, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + this.asyncSend(message, mq, sendCallback, timeout); + } + + /** + * 不关心是否送达到指定队列 + * + * @param message + * @param messageQueue + */ + public void sendOneWay(Message message, MessageQueue messageQueue) { + try { + producer.sendOneway(message, messageQueue); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + + public void sendOneWay(String topic, String tag, Object msgObj, Map properties, MessageQueue mq) { + Message message = createMessage(topic, tag, msgObj, properties); + this.sendOneWay(message, mq); + } + + + public SendResult syncSend(Message message, MessageQueueSelector selector, Object arg) { + return syncSend(message, selector, arg, producer.getSendMsgTimeout()); + } + + public SendResult syncSend(Message message, MessageQueueSelector selector, Object arg, long timeout) { + try { + SendResult sendResult = producer.send(message, selector, arg, timeout); + return sendResult; + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg) { + return this.syncSend(topic, tag, msgObj, properties, selector, arg, producer.getSendMsgTimeout()); + } + + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + return this.syncSend(message, selector, arg, timeout); + } + + public void asyncSend(Message message, MessageQueueSelector selector, Object arg, SendCallback sendCallback) { + this.asyncSend(message, selector, arg, sendCallback, producer.getSendMsgTimeout()); + } + + public void asyncSend(Message message, MessageQueueSelector selector, Object arg, SendCallback sendCallback, long timeout) { + try { + producer.send(message, selector, arg, sendCallback, timeout); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + public void asyncSend(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg, SendCallback sendCallback) { + this.asyncSend(topic, tag, msgObj, properties, selector, arg, sendCallback, producer.getSendMsgTimeout()); + } + + public void asyncSend(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg, SendCallback sendCallback, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + this.asyncSend(message, selector, arg, sendCallback, timeout); + } + + public TransactionSendResult sendMessageInTransaction(String topic, String tag, Object msgObj, Map properties, Object arg) { + Message message = createMessage(topic, tag, msgObj, properties); + try { + TransactionSendResult result = producer.sendMessageInTransaction(message, arg); + return result; + } catch (Exception e) { + log.error("send transaction message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + + public void sendOneWay(Message message, MessageQueueSelector selector, Object arg) { + try { + producer.sendOneway(message, selector, arg); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + + public void sendOneWay(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg) { + Message message = createMessage(topic, tag, msgObj, properties); + this.sendOneWay(message, selector, arg); + } + + @Override + public void afterPropertiesSet() throws Exception { + Assert.notNull(producer, "Property 'producer' is required"); + producer.start(); + } + + @Override + public void destroy() { + Optional.ofNullable(producer).ifPresent(p -> p.shutdown()); + } + + private Message createMessage(String topic, String tag, Object msgObj, Map properties) { + Message rocketMsg = new Message(topic, tag, serializer.serialize(msgObj)); + if (!CollectionUtils.isEmpty(properties)) { + rocketMsg.setFlag((Integer) properties.getOrDefault("FLAG", 0)); + rocketMsg.setWaitStoreMsgOK((Boolean) properties.getOrDefault(MessageConst.PROPERTY_WAIT_STORE_MSG_OK, true)); + Optional.ofNullable((String) properties.get(MessageConst.PROPERTY_KEYS)).ifPresent(keys -> rocketMsg.setKeys(keys)); + Optional.ofNullable((Integer) properties.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL)).ifPresent(delay -> rocketMsg.setDelayTimeLevel(delay)); + properties.entrySet().stream().filter(entry -> !MessageConst.STRING_HASH_SET.contains(entry.getKey()) && !Objects.equals(entry.getKey(), "FLAG")) + .forEach(entry -> {rocketMsg.putUserProperty(entry.getKey(), String.valueOf(entry.getValue()));}); + } + return rocketMsg; + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnFail.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnFail.java new file mode 100644 index 0000000..81d4e5e --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnFail.java @@ -0,0 +1,8 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer.call; + +@FunctionalInterface +public interface SendMessageOnFail { + + void call(T message, Throwable ex); + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnSuccess.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnSuccess.java new file mode 100644 index 0000000..d00bf3b --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnSuccess.java @@ -0,0 +1,9 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer.call; + +import org.apache.rocketmq.client.producer.SendResult; + +@FunctionalInterface +public interface SendMessageOnSuccess { + + void call(T message, SendResult result); +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/TransactionListenerImpl.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/TransactionListenerImpl.java new file mode 100644 index 0000000..dc29dc4 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/TransactionListenerImpl.java @@ -0,0 +1,48 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer.call; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @description: TransactionListenerImpl + **/ +@Slf4j +public class TransactionListenerImpl implements TransactionListener { + private AtomicInteger transactionIndex = new AtomicInteger(0); + + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + localTrans.put(msg.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + Integer status = localTrans.get(msg.getTransactionId()); + if (null != status) { + switch (status) { + case 0: + return LocalTransactionState.UNKNOW; + case 1: + return LocalTransactionState.COMMIT_MESSAGE; + case 2: + return LocalTransactionState.ROLLBACK_MESSAGE; + default: + return LocalTransactionState.COMMIT_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} + diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqInit.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqInit.java new file mode 100644 index 0000000..2a08029 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqInit.java @@ -0,0 +1,33 @@ +package cn.lingniu.framework.plugin.rocketmq.init; + +import cn.lingniu.framework.plugin.rocketmq.config.RocketMqConfig; +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodyJacksonSerializer; +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodySerializer; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + + +/** + * RocketMQ 自动装载 + */ +@Configuration +@EnableConfigurationProperties(RocketMqConfig.class) +@ConditionalOnClass({MQClientAPIImpl.class, DefaultMQPushConsumer.class}) +@Import({RocketMqStartAutoConfiguration.class}) +@Slf4j +public class RocketMqInit { + + @Bean + @ConditionalOnMissingBean + public RocketMqBodySerializer rocketMQSerializer() { + return new RocketMqBodyJacksonSerializer(); + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqStartAutoConfiguration.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqStartAutoConfiguration.java new file mode 100644 index 0000000..5b763bd --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqStartAutoConfiguration.java @@ -0,0 +1,210 @@ +package cn.lingniu.framework.plugin.rocketmq.init; + +import cn.lingniu.framework.plugin.core.context.SpringBeanApplicationContext; +import cn.lingniu.framework.plugin.rocketmq.RocketMqConsumer; +import cn.lingniu.framework.plugin.rocketmq.RocketMqProducer; +import cn.lingniu.framework.plugin.rocketmq.core.consumer.DefaultRocketMqListenerContainer; +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.RocketMqListener; +import cn.lingniu.framework.plugin.rocketmq.config.ConsumerProperties; +import cn.lingniu.framework.plugin.rocketmq.config.ProducerProperties; +import cn.lingniu.framework.plugin.rocketmq.config.RocketMqConfig; +import cn.lingniu.framework.plugin.rocketmq.core.producer.RocketMqProducerInner; +import cn.lingniu.framework.plugin.rocketmq.core.producer.RocketMqTemplate; +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodySerializer; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import cn.lingniu.framework.plugin.util.string.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.client.producer.TransactionMQProducer; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.util.Assert; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; + +@Slf4j +@Configuration +@ConditionalOnClass(DefaultMQPushConsumer.class) +@EnableConfigurationProperties(RocketMqConfig.class) +public class RocketMqStartAutoConfiguration implements ApplicationContextAware { + + @Autowired + private RocketMqConfig rocketMQConfig; + @Autowired + private RocketMqBodySerializer mqSerializer; + + private AtomicLong counter = new AtomicLong(0); + private ConfigurableApplicationContext applicationContext; + + + @Async + @Order(1000) + @EventListener(WebServerInitializedEvent.class) + public void afterStart(WebServerInitializedEvent event) { + if ("management".equalsIgnoreCase(event.getApplicationContext().getServerNamespace())) { + return; + } + startContainer(); + } + + public void startContainer() { + Map producerBeans = SpringBeanApplicationContext.getBeans(RocketMqProducer.class); + Optional.ofNullable(producerBeans).ifPresent(b -> b.forEach((k, v) -> { + List keys = Arrays.asList(StringUtil.split(v.getClass().getName(), ".")); + if (ObjectEmptyUtils.isNotEmpty(keys)) { + String beanName = keys.get(keys.size() - 1); + if (ObjectEmptyUtils.isNotEmpty(rocketMQConfig.getProducers())) { + String mapName = rocketMQConfig.getProducers().keySet().stream().filter(m -> m.equalsIgnoreCase(beanName)).findAny().orElse(null); + if (ObjectEmptyUtils.isNotEmpty(mapName)) { + registerContainer(k, v, rocketMQConfig.getProducers().get(mapName)); + } else { + log.error("{} 生产端配置异常,未开启!", k); + } + } + } + })); + + //region 配置消费端 + Map beans = this.applicationContext.getBeansWithAnnotation(RocketMqConsumer.class); + Optional.ofNullable(beans).ifPresent(b -> b.forEach((k, v) -> { + List keys = Arrays.asList(StringUtil.split(replacePattern(v.toString(), "\\@[A-Za-z0-9]+$", ""), ".")); + if (ObjectEmptyUtils.isNotEmpty(keys)) { + String beanName = keys.get(keys.size() - 1); + if (ObjectEmptyUtils.isNotEmpty(rocketMQConfig.getConsumers()) + && (rocketMQConfig.getConsumers().entrySet().stream().anyMatch(n -> beanName.equalsIgnoreCase(n.getKey())) + || rocketMQConfig.getConsumers().entrySet().stream().anyMatch(n -> beanName.equalsIgnoreCase(n.getValue().getConsumerBeanName())))) { + if (rocketMQConfig.getConsumers().entrySet().stream().anyMatch(n -> beanName.equalsIgnoreCase(n.getValue().getConsumerBeanName()))) { + registerContainer(k, v, rocketMQConfig.getConsumers().entrySet().stream().filter(n -> beanName.equalsIgnoreCase(n.getValue().getConsumerBeanName())) + .findAny().get().getValue()); + return; + } + registerContainer(k, v, rocketMQConfig.getConsumers().entrySet().stream().filter(r -> beanName.equalsIgnoreCase(r.getKey())).findAny().get().getValue()); + } else { + log.error("{} 消费端配置异常,未开启!", k); + } + } + })); + } + + + private void registerContainer(String beanName, Object bean, ProducerProperties producerProperties) { + Class clazz = AopUtils.getTargetClass(bean); + if (!RocketMqProducerInner.class.isAssignableFrom(bean.getClass())) { + throw new IllegalStateException(clazz + " is not instance of " + RocketMqProducerInner.class.getName()); + } + RocketMqProducerInner producer = (RocketMqProducerInner) bean; + String groupName = producerProperties.getGroup(); + Assert.hasText(groupName, "[framework.lingniu.rocketmq.producers.xxx.group] must not be null"); + DefaultMQProducer defaultMQProducer = !(producer instanceof TransactionListener) ? + new DefaultMQProducer(producerProperties.getGroup()) + : new TransactionMQProducer(producerProperties.getGroup()); + if (producer instanceof TransactionListener) { + ((TransactionMQProducer) defaultMQProducer).setExecutorService(new ThreadPoolExecutor( + producerProperties.getTransaction().getCorePoolSize(), producerProperties.getTransaction().getMaximumPoolSize(), + producerProperties.getTransaction().getKeepAliveTime(), TimeUnit.SECONDS, + new ArrayBlockingQueue<>(producerProperties.getTransaction().getQueueCapacity()), r -> { + Thread thread = new Thread(r); + thread.setName(String.format("client-transaction-msg-check-thread-%s", groupName)); + return thread; + }) + ); + ((TransactionMQProducer) defaultMQProducer).setTransactionListener((TransactionListener) producer); + } + defaultMQProducer.setNamesrvAddr(producerProperties.getNameServer()); + defaultMQProducer.setSendMsgTimeout(producerProperties.getSendMsgTimeout()); + defaultMQProducer.setRetryTimesWhenSendFailed(producerProperties.getRetryTimesWhenSendFailed()); + defaultMQProducer.setRetryTimesWhenSendAsyncFailed(producerProperties.getRetryTimesWhenSendAsyncFailed()); + defaultMQProducer.setMaxMessageSize(producerProperties.getMaxMessageSize()); + defaultMQProducer.setCompressMsgBodyOverHowmuch(producerProperties.getCompressMsgBodyOverHowmuch()); + defaultMQProducer.setRetryAnotherBrokerWhenNotStoreOK(producerProperties.isRetryAnotherBrokerWhenNotStoreOk()); + defaultMQProducer.setUnitName(producerProperties.getClientUnitName()); + defaultMQProducer.setSendLatencyFaultEnable(producerProperties.isSendLatencyFaultEnable()); + RocketMqTemplate template = new RocketMqTemplate(); + template.setProducer(defaultMQProducer); + template.setSerializer(mqSerializer); + producer.setRocketMQTemplate(template, producerProperties.getTopic()); + try { + template.afterPropertiesSet(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void registerContainer(String beanName, Object bean, ConsumerProperties consumerProperties) { + Class clazz = AopUtils.getTargetClass(bean); + if (!RocketMqListener.class.isAssignableFrom(bean.getClass())) { + log.warn("{} is not instance of {}", clazz, RocketMqListener.class.getName()); + return; + } + RocketMqListener rocketMQListener = (RocketMqListener) bean; + BeanDefinitionBuilder beanBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultRocketMqListenerContainer.class); + if (ObjectEmptyUtils.isNotEmpty(consumerProperties)) { + beanBuilder.addPropertyValue("nameServer", consumerProperties.getNameServer()); + beanBuilder.addPropertyValue("topic", consumerProperties.getTopic()); + beanBuilder.addPropertyValue("consumerGroup", consumerProperties.getConsumerGroup()); + beanBuilder.addPropertyValue("consumeMode", consumerProperties.getConsumeMode()); + beanBuilder.addPropertyValue("consumeThreadMax", consumerProperties.getConsumeThreadMax()); + beanBuilder.addPropertyValue("consumeThreadMin", consumerProperties.getConsumeThreadMin()); + beanBuilder.addPropertyValue("messageModel", consumerProperties.getMessageModel()); + beanBuilder.addPropertyValue("selectorExpress", consumerProperties.getSelectorExpress()); + beanBuilder.addPropertyValue("selectorType", consumerProperties.getSelectorType()); + beanBuilder.addPropertyValue("rocketMQListener", rocketMQListener); + beanBuilder.addPropertyValue("consumeMessageBatchMaxSize", consumerProperties.getConsumeMessageBatchMaxSize()); + beanBuilder.addPropertyValue("pullBatchSize", consumerProperties.getPullBatchSize()); + beanBuilder.addPropertyValue("maxRetryTimes", consumerProperties.getMaxRetryTimes()); + beanBuilder.addPropertyValue("isBatchMode", consumerProperties.getConsumerbatchMode()); + beanBuilder.addPropertyValue("clientUnitName", consumerProperties.getClientUnitName()); + beanBuilder.addPropertyValue("mqConsumerTimeOut", consumerProperties.getMqConsumerTimeOut()); + + } else { + log.error("消费端 {} 缺少配置信息,请查看!", rocketMQListener.getClass().getSimpleName()); + } + + beanBuilder.addPropertyValue("serializer", mqSerializer); + beanBuilder.setDestroyMethodName("destroy"); + String containerBeanName = String.format("%s_%s", DefaultRocketMqListenerContainer.class.getName(), counter.incrementAndGet()); + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory(); + beanFactory.registerBeanDefinition(containerBeanName, beanBuilder.getBeanDefinition()); + DefaultRocketMqListenerContainer container = beanFactory.getBean(containerBeanName, DefaultRocketMqListenerContainer.class); + if (!container.isStarted()) { + try { + container.start(); + } catch (Exception e) { + log.error("started container failed. {}", container, e); + throw new RuntimeException(e); + } + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = (ConfigurableApplicationContext) applicationContext; + } + + private String replacePattern(final String source, final String regex, final String replacement) { + return Pattern.compile(regex, Pattern.DOTALL).matcher(source).replaceAll(replacement); + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..f157573 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.rocketmq.init.RocketMqInit diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/mq-config.md b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/mq-config.md new file mode 100644 index 0000000..555de0e --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/mq-config.md @@ -0,0 +1,118 @@ +# 【重要】RocketMq服务端搭建和更多详细资料---参考官网 + +## 概述 (Overview) + +1. 基于 RocketMqClient 封装的分布式消息处理组件 +2. 核心能力 + +* 消息处理能力 + +- 双模式监听:支持批量消息处理(MRocketMQListener)和单条消息处理(RocketMQListener) +- 生产端支持:通过继承 GeneralMqProducer 快速实现消息生产功能 +- 消费端支持:通过继承 GeneralMsgListener 和实现监听接口处理消息 +- 多样化消息类型:支持普通消息、定时消息、延迟消息等多种消息类型 + +* 配置管理 + - 统一配置前缀:所有配置项使用 framework.lingniu.rocketmq 作为根路径 + - NameServer配置:支持直接配置 nameServer 地址连接RocketMQ集群 + - 消费策略配置:支持配置消费线程数、批量大小、重试次数等参数 + +3. 适用场景: + - 异步任务处理:数据同步等后台任务处理 + - 系统解耦:微服务间通过消息队列进行松耦合通信 + - 流量削峰:应对突发业务高峰,平滑处理大量消息 + - 可靠消息传输:通过重试机制保证重要业务消息不丢失 + +## 如何配置--更多参数参考:RedissonConfig/RedissonProperties + +```yaml 最小配置例子 +framework: + lingniu: + # rocketmq 最小化配置示例 + rocketmq: + consumers: + TestC1Consumer: + nameServer: mq_test_n1.tst.mid:9876 + consumerGroup: consumer1 + topic: test1-yw-topic + consumerBeanName: TestC1Consumer + producers: + TestMqProducer: + nameServer: mq_test_n1.tst.mid:9876 + group: test1-yw-topic-producer + topic: test1-yw-topic + TestTransProducer: + nameServer: mq_test_n1.tst.mid:9876 + group: test1-yw-trans-topic-producer + topic: test1-yw-trans-topic + isTransactionMQ: true +``` +```yaml 详细配置 + + + +framework: + lingniu: + # rocketmq所有配置示例 + rocketmq: + # 是否开启数据源加密 + encryptEnabled: false + # 消费者配置 + consumers: + # 消费者bean名称 + consumer1: + nameServer: mq_test_n1.tst.mid:9876 + # 消费组 + consumerGroup: consumer1 + # 消费对象名字 + consumerBeanName: consumer1 + # Topic + topic: test1-yw-topic + # 订阅表达式,默认为 * + selectorExpress: "*" + # 消费者最小线程数 + consumeThreadMin: 20 + # 消费者最大线程数 + consumeThreadMax: 32 + # 批量消费数量 + consumeMessageBatchMaxSize: 1 + # 批量拉取消息数量 + pullBatchSize: 32 + # 消息的最大重试次数 + maxRetryTimes: 16 + + # 生产者配置 + producers: + # 生产者的bean名称 + TestMqProducer: + nameServer: mq_test_n1.tst.mid:9876 + # 生产者组 + group: test1-yw-topic-producer + # 生产端Topic + topic: test1-yw-topic + # 发送超时(ms),默认 3000 + sendMsgTimeout: 3000 + # 消息体压缩阀值(kb),默认 4096 + compressMsgBodyOverHowmuch: 4096 + # 同步发送消息失败,是否重新发送,默认 2 + retryTimesWhenSendFailed: 2 + # 异步发送消息失败,是否重新发送,默认 2 + retryTimesWhenSendAsyncFailed: 2 + # 重试另一个Broker当发送消息失败, 默认 false + retryAnotherBrokerWhenNotStoreOk: false + # 默认不启用延迟容错,通过统计每个队列的发送耗时情况来计算broker是否可用 + sendLatencyFaultEnable: false + TestTransMqProducer: + group: test1-yw-trans-topic-producer + topic: test1-yw-trans-topic + # 是否事务型 + isTransactionMQ: true + # 事务消息线程配置 + transaction: + corePoolSize: 5 + # 本地事务执行结果查询线程池配置 + maximumPoolSize: 10 + keepAliveTime: 200 + queueCapacity: 2000 +``` + diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/pom.xml b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/pom.xml new file mode 100644 index 0000000..95affd8 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-web + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.hutool + hutool-all + + + org.springframework.boot + spring-boot + + + org.springframework + spring-core + + + org.springframework.boot + spring-boot-actuator + + + org.springframework + spring-aop + + + org.springframework.boot + spring-boot-autoconfigure + compile + true + + + org.springframework.boot + spring-boot-starter-aop + + + com.google.guava + guava + + + com.google.code.gson + gson + + + + + + + + commons-io + commons-io + + + + org.springframework + spring-webmvc + compile + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + jakarta.servlet + jakarta.servlet-api + + + + diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/ApplicationStartEventListener.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/ApplicationStartEventListener.java new file mode 100644 index 0000000..bfbcfce --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/ApplicationStartEventListener.java @@ -0,0 +1,45 @@ +package cn.lingniu.framework.plugin.web; + +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.Async; +import org.springframework.util.StringUtils; + +@Slf4j +@Configuration +public class ApplicationStartEventListener { + + @Async + @Order + @EventListener(WebServerInitializedEvent.class) + public void afterStart(WebServerInitializedEvent event) { + try { + if (event == null || event.getWebServer() == null) { + log.warn("Web server initialized event is null or web server is null"); + return; + } + + Environment environment = event.getApplicationContext().getEnvironment(); + if (environment == null) { + log.warn("Application environment is null"); + return; + } + + String appName = environment.getProperty(CommonConstant.SPRING_APP_NAME_KEY, "UNKNOWN").toUpperCase(); + int localPort = event.getWebServer().getPort(); + String profile = StringUtils.arrayToCommaDelimitedString(environment.getActiveProfiles()); + String containerType = event.getWebServer().getClass().getSimpleName(); + + log.info("Application [{}] started successfully on port [{}] with profiles [{}], container type: [{}]", + appName, localPort, profile, containerType); + + } catch (Exception e) { + log.error("Error occurred while handling web server initialized event", e); + } + } +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/ApiAccessLog.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/ApiAccessLog.java new file mode 100644 index 0000000..ada80f0 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/ApiAccessLog.java @@ -0,0 +1,49 @@ +package cn.lingniu.framework.plugin.web.apilog; + + +import cn.lingniu.framework.plugin.web.apilog.enums.OperateTypeEnum; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 访问日志注解 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiAccessLog { + + // ========== 开关字段 ========== + + /** + * 是否记录访问日志 + */ + boolean enable() default true; + /** + * 是否记录请求参数 + * + * 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭 + */ + boolean requestEnable() default true; + /** + * 是否记录响应结果 + * + * 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开 + */ + boolean responseEnable() default false; + /** + * 敏感参数数组 + * + * 添加后,请求参数、响应结果不会记录该参数 + */ + String[] sanitizeKeys() default {}; + /** + * 操作分类 + * + * 实际并不是数组,因为枚举不能设置 null 作为默认值 + */ + OperateTypeEnum[] operateType() default {}; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/enums/OperateTypeEnum.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/enums/OperateTypeEnum.java new file mode 100644 index 0000000..34c70b1 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/enums/OperateTypeEnum.java @@ -0,0 +1,51 @@ +package cn.lingniu.framework.plugin.web.apilog.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作日志的操作类型 + * + * @author portal + */ +@Getter +@AllArgsConstructor +public enum OperateTypeEnum { + + /** + * 查询 + */ + GET(1), + /** + * 新增 + */ + CREATE(2), + /** + * 修改 + */ + UPDATE(3), + /** + * 删除 + */ + DELETE(4), + /** + * 导出 + */ + EXPORT(5), + /** + * 导入 + */ + IMPORT(6), + /** + * 其它 + * + * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 + */ + OTHER(0); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/filter/ApiAccessLogFilter.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/filter/ApiAccessLogFilter.java new file mode 100644 index 0000000..5b07184 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/filter/ApiAccessLogFilter.java @@ -0,0 +1,241 @@ +package cn.lingniu.framework.plugin.web.apilog.filter; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import cn.lingniu.framework.plugin.web.apilog.ApiAccessLog; +import cn.lingniu.framework.plugin.web.apilog.enums.OperateTypeEnum; +import cn.lingniu.framework.plugin.web.apilog.interceptor.ApiAccessLogInterceptor; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import cn.lingniu.framework.plugin.web.bae.filter.ApiRequestFilter; +import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Iterator; +import java.util.Map; + + +/** + * API 访问日志 Filter + * todo 目的:记录 API 访问日志输出---后续可以输出db + * + */ +@Slf4j +public class ApiAccessLogFilter extends ApiRequestFilter { + + private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"}; + + private final String applicationName; + + private final ApiAccessLogCommonApi apiAccessLogApi; + + public ApiAccessLogFilter(FrameworkWebConfig frameworkWebConfig, String applicationName, ApiAccessLogCommonApi apiAccessLogApi) { + super(frameworkWebConfig); + this.applicationName = applicationName; + this.apiAccessLogApi = apiAccessLogApi; + } + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 获得开始时间 + LocalDateTime beginTime = LocalDateTime.now(); + // 提前获得参数,避免 XssFilter 过滤处理 + Map queryString = ServletUtils.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; + + try { + // 继续过滤器 + filterChain.doFilter(request, response); + // 正常执行,记录日志 + createApiAccessLog(request, beginTime, queryString, requestBody, null); + } catch (Exception ex) { + // 异常执行,记录日志 + createApiAccessLog(request, beginTime, queryString, requestBody, ex); + throw ex; + } + } + + private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime, + Map queryString, String requestBody, Exception ex) { + ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO(); + try { + boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex); + if (!enable) { + return; + } + apiAccessLogApi.createApiAccessLogAsync(accessLog); + } catch (Throwable th) { + log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), JsonUtils.toJsonString(accessLog), th); + } + } + + private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime, + Map queryString, String requestBody, Exception ex) { + // 判断:是否要记录操作日志 + HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD); + ApiAccessLog accessLogAnnotation = null; + if (handlerMethod != null) { + accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class); + if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) { + return false; + } + } + + // 处理用户信息 + accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置访问结果 + CommonResult result = WebFrameworkUtils.getCommonResult(request); + if (result != null) { + accessLog.setResultCode(result.getCode()); + accessLog.setResultMsg(result.getMsg()); + } else if (ex != null) { + accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()); + accessLog .setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); + } else { + accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + accessLog.setResultMsg(""); + } + // 设置请求字段 + accessLog.setRequestUrl(request.getRequestURI()); + accessLog.setRequestMethod(request.getMethod()); + accessLog.setApplicationName(ApplicationNameContext.getApplicationName()); + accessLog.setUserAgent(ServletUtils.getUserAgent(request)); + accessLog.setUserIp(ServletUtils.getClientIP(request)); + String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null; + Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE; + if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false + Map requestParams = MapUtil.builder() + .put("query", sanitizeMap(queryString, sanitizeKeys)) + .put("body", sanitizeJson(requestBody, sanitizeKeys)).build(); + accessLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + } + Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE; + if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true + accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys)); + } + // 持续时间 + accessLog.setBeginTime(beginTime); + accessLog.setEndTime(LocalDateTime.now()); + accessLog.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS)); + // 操作模块 + OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ? + accessLogAnnotation.operateType()[0] : parseOperateLogType(request); + accessLog.setOperateType(operateType.getType()); + return true; + } + + // ========== 解析 @ApiAccessLog、@Swagger 注解 ========== + + private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) { + RequestMethod requestMethod = ArrayUtil.firstMatch(method -> + StrUtil.equalsAnyIgnoreCase(method.name(), request.getMethod()), RequestMethod.values()); + if (requestMethod == null) { + return OperateTypeEnum.OTHER; + } + switch (requestMethod) { + case GET: + return OperateTypeEnum.GET; + case POST: + return OperateTypeEnum.CREATE; + case PUT: + return OperateTypeEnum.UPDATE; + case DELETE: + return OperateTypeEnum.DELETE; + default: + return OperateTypeEnum.OTHER; + } + } + + // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ========== + + private static String sanitizeMap(Map map, String[] sanitizeKeys) { + if (CollUtil.isEmpty(map)) { + return null; + } + if (sanitizeKeys != null) { + MapUtil.removeAny(map, sanitizeKeys); + } + MapUtil.removeAny(map, SANITIZE_KEYS); + return JsonUtils.toJsonString(map); + } + + private static String sanitizeJson(String jsonString, String[] sanitizeKeys) { + if (StrUtil.isEmpty(jsonString)) { + return null; + } + try { + JsonNode rootNode = JsonUtils.parseTree(jsonString); + sanitizeJson(rootNode, sanitizeKeys); + return JsonUtils.toJsonString(rootNode); + } catch (Exception e) { + // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 + log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); + return jsonString; + } + } + + private static String sanitizeJson(CommonResult commonResult, String[] sanitizeKeys) { + if (commonResult == null) { + return null; + } + String jsonString = JsonUtils.toJsonString(commonResult); + try { + JsonNode rootNode = JsonUtils.parseTree(jsonString); + sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉 + return JsonUtils.toJsonString(rootNode); + } catch (Exception e) { + // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 + log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); + return jsonString; + } + } + + private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) { + // 情况一:数组,遍历处理 + if (node.isArray()) { + for (JsonNode childNode : node) { + sanitizeJson(childNode, sanitizeKeys); + } + return; + } + // 情况二:非 Object,只是某个值,直接返回 + if (!node.isObject()) { + return; + } + // 情况三:Object,遍历处理 + Iterator> iterator = node.fields(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (ArrayUtil.contains(sanitizeKeys, entry.getKey()) + || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) { + iterator.remove(); + continue; + } + sanitizeJson(entry.getValue(), sanitizeKeys); + } + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/interceptor/ApiAccessLogInterceptor.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/interceptor/ApiAccessLogInterceptor.java new file mode 100644 index 0000000..89e235e --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/interceptor/ApiAccessLogInterceptor.java @@ -0,0 +1,102 @@ +package cn.lingniu.framework.plugin.web.apilog.interceptor; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import cn.lingniu.framework.plugin.util.spring.SpringUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StopWatch; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.IntStream; + +/** + * API 访问日志 Interceptor + * + * todo 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。 + */ +@Slf4j +public class ApiAccessLogInterceptor implements HandlerInterceptor { + + public static final String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD"; + + private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用 + HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null; + if (handlerMethod != null) { + request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod); + } + + // 打印 request 日志 + if (!SpringUtils.isProd()) { + Map queryString = ServletUtils.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; + if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) { + log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI()); + } else { + log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(), + StrUtil.blankToDefault(requestBody, queryString.toString())); + } + // 计时 + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch); + // 打印 Controller 路径 + printHandlerMethodPosition(handlerMethod); + } + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + // 打印 response 日志 + if (!SpringUtils.isProd()) { + StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH); + if (stopWatch != null && stopWatch.isRunning() ) { + stopWatch.stop(); + log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]", + request.getRequestURI(), stopWatch.getTotalTimeMillis()); + } + } + } + + /** + * 打印 Controller 方法路径 + */ + private void printHandlerMethodPosition(HandlerMethod handlerMethod) { + if (handlerMethod == null) { + return; + } + Method method = handlerMethod.getMethod(); + Class clazz = method.getDeclaringClass(); + try { + // 获取 method 的 lineNumber + List clazzContents = FileUtil.readUtf8Lines( + ResourceUtil.getResource(null, clazz).getPath().replace("/target/classes/", "/src/main/java/") + + clazz.getSimpleName() + ".java"); + Optional lineNumber = IntStream.range(0, clazzContents.size()) + .filter(i -> clazzContents.get(i).contains(" " + method.getName() + "(")) // 简单匹配,不考虑方法重名 + .mapToObj(i -> i + 1) // 行号从 1 开始 + .findFirst(); + if (!lineNumber.isPresent()) { + return; + } + // 打印结果 + System.out.printf("\tController 方法路径:%s(%s.java:%d)\n", clazz.getName(), clazz.getSimpleName(), lineNumber.get()); + } catch (Exception ignore) { + // 忽略异常。原因:仅仅打印,非重要逻辑 + } + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogApiImpl.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogApiImpl.java new file mode 100644 index 0000000..7049f56 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogApiImpl.java @@ -0,0 +1,26 @@ +package cn.lingniu.framework.plugin.web.apilog.logger; + +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogService; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + + +/** + * API 访问日志的 API 实现类 + */ +@Service +@Validated +public class ApiAccessLogApiImpl implements ApiAccessLogCommonApi { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @Override + public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { + apiAccessLogService.createApiAccessLog(createDTO); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogServiceImpl.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogServiceImpl.java new file mode 100644 index 0000000..865262c --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogServiceImpl.java @@ -0,0 +1,40 @@ +package cn.lingniu.framework.plugin.web.apilog.logger; + +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.object.BeanUtils; +import cn.lingniu.framework.plugin.util.string.StrUtils; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogService; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogDO; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + + +/** + * API 访问日志 Service 实现类 todo 可以扩展 + */ +@Slf4j +@Service +@Validated +public class ApiAccessLogServiceImpl implements ApiAccessLogService { + + @Autowired(required = false) + private FrameworkWebConfig frameworkWebConfig; + + @Override + public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { + ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class); + apiAccessLog.setRequestParams(StrUtils.maxLength(apiAccessLog.getRequestParams(), ApiAccessLogDO.REQUEST_PARAMS_MAX_LENGTH)); + apiAccessLog.setResultMsg(StrUtils.maxLength(apiAccessLog.getResultMsg(), ApiAccessLogDO.RESULT_MSG_MAX_LENGTH)); + if (frameworkWebConfig != null && createDTO.getDuration() > frameworkWebConfig.getApiLog().getUrlErrorTimeOut()) { + log.error("api请求异常,详细信息:{}", JsonUtils.toJsonString(createDTO)); + } else { + log.info("api请求正常,详细信息:{}", JsonUtils.toJsonString(createDTO)); + } + } + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogApiImpl.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogApiImpl.java new file mode 100644 index 0000000..5e365cb --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogApiImpl.java @@ -0,0 +1,23 @@ +package cn.lingniu.framework.plugin.web.apilog.logger; + +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogCommonApi; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogService; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +/** + * API 访问日志的 API 接口 + */ +@Service +public class ApiErrorLogApiImpl implements ApiErrorLogCommonApi { + + @Resource + private ApiErrorLogService apiErrorLogService; + + @Override + public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { + apiErrorLogService.createApiErrorLog(createDTO); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogServiceImpl.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogServiceImpl.java new file mode 100644 index 0000000..246c0de --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogServiceImpl.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.web.apilog.logger; + +import cn.hutool.json.JSONObject; +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.object.BeanUtils; +import cn.lingniu.framework.plugin.util.string.StrUtils; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogService; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogDO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + + +/** + * API 错误日志 Service 实现类 todo 可以扩展 + */ +@Service +@Validated +@Slf4j +public class ApiErrorLogServiceImpl implements ApiErrorLogService { + + + @Override + public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { + ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) ; + apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), ApiErrorLogDO.REQUEST_PARAMS_MAX_LENGTH)); + log.error("api请求异常,详细信息:{}", JsonUtils.toJsonString(apiErrorLog)); + } + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogCommonApi.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogCommonApi.java new file mode 100644 index 0000000..12f92a7 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogCommonApi.java @@ -0,0 +1,29 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.api; + +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; +import org.springframework.scheduling.annotation.Async; + + +/** + * API 访问日志的 API 接口 + */ +public interface ApiAccessLogCommonApi { + + /** + * 创建 API 访问日志 + * + * @param createDTO 创建信息 + */ + void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO); + + /** + * 【异步】创建 API 访问日志 + * + * @param createDTO 访问日志 DTO + */ + @Async + default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) { + createApiAccessLog(createDTO); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogService.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogService.java new file mode 100644 index 0000000..7c88617 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogService.java @@ -0,0 +1,18 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.api; + + +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; + +/** + * API 访问日志 Service 接口 + */ +public interface ApiAccessLogService { + + /** + * 创建 API 访问日志 + * + * @param createReqDTO API 访问日志 + */ + void createApiAccessLog(ApiAccessLogCreateReqDTO createReqDTO); + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogCommonApi.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogCommonApi.java new file mode 100644 index 0000000..5e027b2 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogCommonApi.java @@ -0,0 +1,27 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.api; + +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; +import org.springframework.scheduling.annotation.Async; + +/** + * API 错误日志的 API 接口 + */ +public interface ApiErrorLogCommonApi { + + /** + * 创建 API 错误日志 + * + * @param createDTO 创建信息 + */ + void createApiErrorLog( ApiErrorLogCreateReqDTO createDTO); + + /** + * 【异步】创建 API 异常日志 + * + * @param createDTO 异常日志 DTO + */ + @Async + default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) { + createApiErrorLog(createDTO); + } +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogService.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogService.java new file mode 100644 index 0000000..f60c67b --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogService.java @@ -0,0 +1,18 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.api; + + +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; + +/** + * API 错误日志 Service 接口 + */ +public interface ApiErrorLogService { + + /** + * 创建 API 错误日志 + * + * @param createReqDTO API 错误日志 + */ + void createApiErrorLog(ApiErrorLogCreateReqDTO createReqDTO); + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogCreateReqDTO.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogCreateReqDTO.java new file mode 100644 index 0000000..fcef911 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogCreateReqDTO.java @@ -0,0 +1,79 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * API 访问日志---后续可以跟用户登陆信息互通 + */ +@Data +public class ApiAccessLogCreateReqDTO { + + /** + * 用户编号 + */ + private Long userId = 0L; + /** + * 用户类型 + */ + private Integer userType = 0; + + /** + * 应用名 + */ + private String applicationName; + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + */ + private String requestParams; + /** + * 响应结果 + */ + private String responseBody; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + /** + * 开始请求时间 + */ + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 结果码 + */ + private Integer resultCode; + /** + * 结果提示 + */ + private String resultMsg; + + /** + * 操作分类 + * + * 枚举,参见 OperateTypeEnum 类 + */ + private Integer operateType; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogDO.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogDO.java new file mode 100644 index 0000000..c154901 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogDO.java @@ -0,0 +1,99 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * API 访问日志 + */ +@Data +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiAccessLogDO { + + /** + * {@link #requestParams} 的最大长度 + */ + public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000; + + /** + * {@link #resultMsg} 的最大长度 + */ + public static final Integer RESULT_MSG_MAX_LENGTH = 512; + + + /** + * 应用名 + * + * 目前读取 `spring.application.name` 配置项 + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 响应结果 + */ + private String responseBody; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 执行相关字段 ========== + + + /** + * 开始请求时间 + */ + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogCreateReqDTO.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogCreateReqDTO.java new file mode 100644 index 0000000..4ea77e2 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogCreateReqDTO.java @@ -0,0 +1,85 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * API 错误日志 + */ +@Data +public class ApiErrorLogCreateReqDTO { + + /** + * 账号编号 + */ + private Long userId = 0L; + /** + * 用户类型 + */ + private Integer userType = 0; + /** + * 应用名 + */ + private String applicationName; + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + */ + private String requestParams; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + /** + * 异常时间 + */ + private LocalDateTime exceptionTime; + /** + * 异常名 + */ + private String exceptionName; + /** + * 异常发生的类全名 + */ + private String exceptionClassName; + /** + * 异常发生的类文件 + */ + private String exceptionFileName; + /** + * 异常发生的方法名 + */ + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + */ + private Integer exceptionLineNumber; + /** + * 异常的栈轨迹异常的栈轨迹 + */ + private String exceptionStackTrace; + /** + * 异常导致的根消息 + */ + private String exceptionRootCauseMessage; + /** + * 异常导致的消息 + */ + private String exceptionMessage; + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogDO.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogDO.java new file mode 100644 index 0000000..fb1054f --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogDO.java @@ -0,0 +1,118 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * API 异常数据 + */ +@Data +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor + +public class ApiErrorLogDO { + + /** + * {@link #requestParams} 的最大长度 + */ + public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000; + + + /** + * 应用名 + * + * 目前读取 spring.application.name + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 异常相关字段 ========== + + /** + * 异常发生时间 + */ + private LocalDateTime exceptionTime; + /** + * 异常名 + * + * {@link Throwable#getClass()} 的类全名 + */ + private String exceptionName; + /** + * 异常导致的消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getMessage(Throwable)} + */ + private String exceptionMessage; + /** + * 异常导致的根消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getRootCauseMessage(Throwable)} + */ + private String exceptionRootCauseMessage; + /** + * 异常的栈轨迹 + * + * {@link org.apache.commons.lang3.exception.ExceptionUtils#getStackTrace(Throwable)} + */ + private String exceptionStackTrace; + /** + * 异常发生的类全名 + * + * {@link StackTraceElement#getClassName()} + */ + private String exceptionClassName; + /** + * 异常发生的类文件 + * + * {@link StackTraceElement#getFileName()} + */ + private String exceptionFileName; + /** + * 异常发生的方法名 + * + * {@link StackTraceElement#getMethodName()} + */ + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + * + * {@link StackTraceElement#getLineNumber()} + */ + private Integer exceptionLineNumber; + + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/ApiRequestFilter.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/ApiRequestFilter.java new file mode 100644 index 0000000..2641742 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/ApiRequestFilter.java @@ -0,0 +1,38 @@ +package cn.lingniu.framework.plugin.web.bae.filter; + +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.util.CollectionUtils; +import org.springframework.web.filter.OncePerRequestFilter; + + +/** + * 可以过滤排除指定的url + * + */ +@RequiredArgsConstructor +public abstract class ApiRequestFilter extends OncePerRequestFilter { + + protected final FrameworkWebConfig frameworkWebConfig; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + if (Boolean.FALSE.equals(frameworkWebConfig.getApiLog().getEnable())) { + return true; + } + if (CollectionUtils.isEmpty(frameworkWebConfig.getApiLog().getExcludeUrls())) { + return false; + } + // 过滤 API 请求的地址 + String apiUri = request.getRequestURI().substring(request.getContextPath().length()); + for (String exclude : frameworkWebConfig.getApiLog().getExcludeUrls()) { + if (StrUtil.startWithAny(apiUri, exclude)) { + return true; + } + } + return false; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyFilter.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyFilter.java new file mode 100644 index 0000000..a70b43f --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyFilter.java @@ -0,0 +1,41 @@ +package cn.lingniu.framework.plugin.web.bae.filter; + +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * Request Body 缓存 Filter,实现它的可重复读取 + */ +public class CacheRequestBodyFilter extends OncePerRequestFilter { + + /** + * 需要排除的 URI + * 1. 排除 Spring Boot Admin 相关请求,避免客户端连接中断导致的异常。 + */ + private static final String[] IGNORE_URIS = {"/admin/", "/actuator/"}; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new CacheRequestBodyWrapper(request), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 1. 校验是否为排除的 URL + String requestURI = request.getRequestURI(); + if (StrUtil.startWithAny(requestURI, IGNORE_URIS)) { + return true; + } + // 2. 只处理 json 请求内容 + return !ServletUtils.isJsonRequest(request); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyWrapper.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyWrapper.java new file mode 100644 index 0000000..d1134fa --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyWrapper.java @@ -0,0 +1,76 @@ +package cn.lingniu.framework.plugin.web.bae.filter; + + +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; + +/** + * Request Body 缓存 Wrapper + */ +public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { + + /** + * 缓存的内容 + */ + private final byte[] body; + + public CacheRequestBodyWrapper(HttpServletRequest request) { + super(request); + body = ServletUtils.getBodyBytes(request); + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public int getContentLength() { + return body.length; + } + + @Override + public long getContentLengthLong() { + return body.length; + } + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + // 返回 ServletInputStream + return new ServletInputStream() { + + @Override + public int read() { + return inputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) {} + + @Override + public int available() { + return body.length; + } + + }; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalExceptionHandler.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..2b751d2 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalExceptionHandler.java @@ -0,0 +1,383 @@ +package cn.lingniu.framework.plugin.web.bae.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.core.exception.ServerException; +import cn.lingniu.framework.plugin.core.exception.ServiceException; +import cn.lingniu.framework.plugin.core.exception.util.ServiceExceptionUtil; +import cn.lingniu.framework.plugin.util.collection.SetUtils; +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogCommonApi; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; +import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.google.common.util.concurrent.UncheckedExecutionException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.ValidationException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.util.Assert; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.servlet.NoHandlerFoundException; +import java.nio.file.AccessDeniedException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants.*; + + +/** + * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 + */ +//@RestControllerAdvice +@ControllerAdvice +@ResponseBody +@AllArgsConstructor +@Slf4j +public class GlobalExceptionHandler { + + /** + * 忽略的 ServiceException 错误提示,避免打印过多 logger + */ + public static final Set IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌"); + + private final ApiErrorLogCommonApi apiErrorLogApi; + + /** + * 处理所有异常,主要是提供给 Filter 使用 + * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 + * + * @param request 请求 + * @param ex 异常 + * @return 通用返回 + */ + public CommonResult allExceptionHandler(HttpServletRequest request, Throwable ex) { + if (ex instanceof MissingServletRequestParameterException) { + return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); + } + if (ex instanceof MethodArgumentTypeMismatchException) { + return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); + } + if (ex instanceof MethodArgumentNotValidException) { + return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); + } + if (ex instanceof BindException) { + return bindExceptionHandler((BindException) ex); + } + if (ex instanceof ConstraintViolationException) { + return constraintViolationExceptionHandler((ConstraintViolationException) ex); + } + if (ex instanceof ValidationException) { + return validationException((ValidationException) ex); + } + if (ex instanceof MaxUploadSizeExceededException) { + return maxUploadSizeExceededExceptionHandler((MaxUploadSizeExceededException) ex); + } + if (ex instanceof NoHandlerFoundException) { + return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); + } + if (ex instanceof HttpRequestMethodNotSupportedException) { + return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); + } + if (ex instanceof HttpMediaTypeNotSupportedException) { + return httpMediaTypeNotSupportedExceptionHandler((HttpMediaTypeNotSupportedException) ex); + } + if (ex instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex); + } + if (ex instanceof AccessDeniedException) { + return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); + } + return defaultExceptionHandler(request, ex); + } + + /** + * 处理 SpringMVC 请求参数缺失 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 + */ + @ExceptionHandler(value = MissingServletRequestParameterException.class) + public CommonResult missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { + log.warn("[missingServletRequestParameterExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); + } + + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { + log.warn("[methodArgumentTypeMismatchExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); + } + + /** + * 处理 SpringMVC 参数校验不正确 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { + log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); + // 获取 errorMessage + String errorMessage = null; + FieldError fieldError = ex.getBindingResult().getFieldError(); + if (fieldError == null) { + // 组合校验,参考自 https://t.zsxq.com/3HVTx + List allErrors = ex.getBindingResult().getAllErrors(); + if (CollUtil.isNotEmpty(allErrors)) { + errorMessage = allErrors.get(0).getDefaultMessage(); + } + } else { + errorMessage = fieldError.getDefaultMessage(); + } + // 转换 CommonResult + if (StrUtil.isEmpty(errorMessage)) { + return CommonResult.error(BAD_REQUEST); + } + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage)); + } + + /** + * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 + */ + @ExceptionHandler(BindException.class) + public CommonResult bindExceptionHandler(BindException ex) { + log.warn("[handleBindException]", ex); + FieldError fieldError = ex.getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @SuppressWarnings("PatternVariableCanBeUsed") + public CommonResult methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { + log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); + if (ex.getCause() instanceof InvalidFormatException) { + InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); + } + if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) { + return CommonResult.error(BAD_REQUEST.getCode(), "请求参数类型错误: request body 缺失"); + } + return defaultExceptionHandler(ServletUtils.getRequest(), ex); + } + + /** + * 处理 Validator 校验不通过产生的异常 + */ + @ExceptionHandler(value = ConstraintViolationException.class) + public CommonResult constraintViolationExceptionHandler(ConstraintViolationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + ConstraintViolation constraintViolation = ex.getConstraintViolations().iterator().next(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); + } + + /** + * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 + */ + @ExceptionHandler(value = ValidationException.class) + public CommonResult validationException(ValidationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 + return CommonResult.error(BAD_REQUEST); + } + + /** + * 处理上传文件过大异常 + */ + @ExceptionHandler(MaxUploadSizeExceededException.class) + public CommonResult maxUploadSizeExceededExceptionHandler(MaxUploadSizeExceededException ex) { + return CommonResult.error(BAD_REQUEST.getCode(), "上传文件过大,请调整后重试"); + } + + /** + * 处理 SpringMVC 请求地址不存在 + * + * 注意,它需要设置如下两个配置项: + * 1. spring.mvc.throw-exception-if-no-handler-found 为 true + * 2. spring.mvc.static-path-pattern 为 /statics/** + */ + @ExceptionHandler(NoHandlerFoundException.class) + public CommonResult noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { + log.warn("[noHandlerFoundExceptionHandler]", ex); + return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); + } + + /** + * 处理 SpringMVC 请求方法不正确 + * + * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public CommonResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { + log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); + return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); + } + + /** + * 处理 SpringMVC 请求的 Content-Type 不正确 + * + * 例如说,A 接口的 Content-Type 为 application/json,结果请求的 Content-Type 为 application/octet-stream,导致不匹配 + */ + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public CommonResult httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException ex) { + log.warn("[httpMediaTypeNotSupportedExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求类型不正确:%s", ex.getMessage())); + } + + /** + * 处理 Spring Security 权限不足的异常 + * + * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 + */ + @ExceptionHandler(value = AccessDeniedException.class) + public CommonResult accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { + log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), + req.getRequestURL(), ex); + return CommonResult.error(FORBIDDEN); + } + + /** + * 处理 Guava UncheckedExecutionException + * + * 例如说,缓存加载报错,可见 https://t.zsxq.com/UszdH + */ + @ExceptionHandler(value = UncheckedExecutionException.class) + public CommonResult uncheckedExecutionExceptionHandler(HttpServletRequest req, UncheckedExecutionException ex) { + return allExceptionHandler(req, ex.getCause()); + } + + /** + * 处理业务异常 ServiceException 例如说,商品库存不足,用户手机号已存在。 + */ + @ExceptionHandler(value = ServiceException.class) + public CommonResult serviceExceptionHandler(ServiceException ex) { + // 不包含的时候,才进行打印,避免 ex 堆栈过多 + if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) { + // 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到 + try { + StackTraceElement[] stackTraces = ex.getStackTrace(); + for (StackTraceElement stackTrace : stackTraces) { + if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceExceptionUtil.class.getName())) { + log.warn("[serviceExceptionHandler]\n\t{}", stackTrace); + break; + } + } + } catch (Exception ignored) { + // 忽略日志,避免影响主流程 + } + } + return CommonResult.error(ex.getCode(), ex.getMessage()); + } + + /** + * 处理业务异常 ServiceException 例如说,商品库存不足,用户手机号已存在。 + */ + @ExceptionHandler(value = ServerException.class) + public CommonResult serverExceptionHandler(ServerException ex) { + try { + StackTraceElement[] stackTraces = ex.getStackTrace(); + for (StackTraceElement stackTrace : stackTraces) { + if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceExceptionUtil.class.getName())) { + log.warn("[serverExceptionHandler]\n\t{}", stackTrace); + break; + } + } + } catch (Exception ignored) { + // 忽略日志,避免影响主流程 + } + return CommonResult.error(ex.getCode(), ex.getMessage()); + } + + /** + * 处理系统异常,兜底处理所有的一切 + */ + @ExceptionHandler(value = Exception.class) + public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { + // 特殊:如果是 ServiceException 的异常,则直接返回 + if (ex.getCause() != null && ex.getCause() instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex.getCause()); + } + if (ex.getCause() != null && ex.getCause() instanceof ServerException) { + return serviceExceptionHandler((ServiceException) ex.getCause()); + } + // 情况二:处理异常 + log.error("[defaultExceptionHandler]", ex); + // 插入异常日志 + createExceptionLog(req, ex); + // 返回 ERROR CommonResult + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + + private void createExceptionLog(HttpServletRequest req, Throwable e) { + // 插入错误日志 + ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO(); + try { + // 初始化 errorLog + buildExceptionLog(errorLog, req, e); + // 执行插入 errorLog + apiErrorLogApi.createApiErrorLogAsync(errorLog); + } catch (Throwable th) { + log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); + } + } + + private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) { + // 处理用户信息 + errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置异常字段 + errorLog.setExceptionName(e.getClass().getName()); + errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); + errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); + errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e)); + StackTraceElement[] stackTraceElements = e.getStackTrace(); + Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); + StackTraceElement stackTraceElement = stackTraceElements[0]; + errorLog.setExceptionClassName(stackTraceElement.getClassName()); + errorLog.setExceptionFileName(stackTraceElement.getFileName()); + errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); + errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); + // 设置其它字段 + errorLog.setApplicationName(ApplicationNameContext.getApplicationName()); + errorLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder() + .put("query", ServletUtils.getParamMap(request)) + .put("body", ServletUtils.getBody(request)).build(); + errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + errorLog.setRequestMethod(request.getMethod()); + errorLog.setUserAgent(ServletUtils.getUserAgent(request)); + errorLog.setUserIp(ServletUtils.getClientIP(request)); + errorLog.setExceptionTime(LocalDateTime.now()); + } + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalResponseBodyHandler.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalResponseBodyHandler.java new file mode 100644 index 0000000..98df1b4 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalResponseBodyHandler.java @@ -0,0 +1,42 @@ +package cn.lingniu.framework.plugin.web.bae.handler; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * 全局响应结果(ResponseBody)处理器 + * + */ +@ControllerAdvice +public class GlobalResponseBodyHandler implements ResponseBodyAdvice { + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public boolean supports(MethodParameter returnType, Class converterType) { + if (returnType.getMethod() == null) { + return false; + } + // 只拦截返回结果为 CommonResult 类型 + return returnType.getMethod().getReturnType() == CommonResult.class; + } + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + // 记录 Controller 结果 + if (returnType.getMethod().getReturnType() == CommonResult.class) { + WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body); + return body; + } + return body; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/interceptor/ResponseCheckInterceptor.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/interceptor/ResponseCheckInterceptor.java new file mode 100644 index 0000000..e410ee7 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/interceptor/ResponseCheckInterceptor.java @@ -0,0 +1,38 @@ +package cn.lingniu.framework.plugin.web.bae.interceptor; + +import cn.hutool.core.util.ClassUtil; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + + + +@Slf4j +@Order(Ordered.LOWEST_PRECEDENCE + 1000) +public class ResponseCheckInterceptor implements HandlerInterceptor { + + @Override + public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { + if (!(o instanceof HandlerMethod)) { + return; + } + HandlerMethod handlerMethod = (HandlerMethod) o; + boolean flag = ClassUtil.isAssignable(CommonResult.class, handlerMethod.getMethod().getReturnType()) + || ClassUtil.isAssignable(Iterable.class, handlerMethod.getMethod().getReturnType()); + if (log.isErrorEnabled() && !flag) { + log.error("\r\n\t {} 接口响应返回类型为:{} 不符合规范\r\n\t\t返回类型必须是:CommonResult,响应规范请查看:GlobalResponseBodyHandler, 异常响应体实现细则请查看:cn.lingniu.framework.plugin.web.base.handler.GlobalExceptionHandler", + handlerMethod.getMethod().getName(), handlerMethod.getMethod().getReturnType().getName()); + } + } + + @Override + public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/util/WebFrameworkUtils.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/util/WebFrameworkUtils.java new file mode 100644 index 0000000..91918ab --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/util/WebFrameworkUtils.java @@ -0,0 +1,123 @@ +package cn.lingniu.framework.plugin.web.bae.util; + +import cn.hutool.core.util.NumberUtil; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.base.TerminalEnum; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + + +/** + * 专属于 web 包的工具类 + */ +public class WebFrameworkUtils { + + //todo 后续扩展 + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type"; + //todo 后续扩展 + + private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; + + /** + * 终端的 Header + * + * @see TerminalEnum + */ + public static final String HEADER_TERMINAL = "terminal"; + + private static FrameworkWebConfig frameworkWebConfig; + + public WebFrameworkUtils(FrameworkWebConfig frameworkWebConfig) { + WebFrameworkUtils.frameworkWebConfig = frameworkWebConfig; + } + + public static void setLoginUserId(ServletRequest request, Long userId) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); + } + + /** + * 设置用户类型 + * + * @param request 请求 + * @param userType 用户类型 + */ + public static void setLoginUserType(ServletRequest request, Integer userType) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType); + } + + /** + * 获得当前用户的编号,从请求中 + * 注意:该方法仅限于 framework 框架使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Long getLoginUserId(HttpServletRequest request) { + if (request == null) { + return null; + } + return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); + } + + /** + * 获得当前用户的类型 + * 注意:该方法仅限于 web 相关的 framework 组件使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Integer getLoginUserType(HttpServletRequest request) { + if (request == null) { + return null; + } + // 1. 优先,从 Attribute 中获取 + Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); + if (userType != null) { + return userType; + } + return null; + } + + public static Integer getLoginUserType() { + HttpServletRequest request = getRequest(); + return getLoginUserType(request); + } + + public static Long getLoginUserId() { + HttpServletRequest request = getRequest(); + return getLoginUserId(request); + } + + public static Integer getTerminal() { + HttpServletRequest request = getRequest(); + if (request == null) { + return TerminalEnum.UNKNOWN.getTerminal(); + } + String terminalValue = request.getHeader(HEADER_TERMINAL); + return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal()); + } + + public static void setCommonResult(ServletRequest request, CommonResult result) { + request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); + } + + public static CommonResult getCommonResult(ServletRequest request) { + return (CommonResult) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); + } + + @SuppressWarnings("PatternVariableCanBeUsed") + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; + return servletRequestAttributes.getRequest(); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiEncryptProperties.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiEncryptProperties.java new file mode 100644 index 0000000..b1da316 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiEncryptProperties.java @@ -0,0 +1,59 @@ +package cn.lingniu.framework.plugin.web.config; + +import lombok.Data; + + +/** + * HTTP API 加解密配置 + * + */ +@Data +public class ApiEncryptProperties { + + /** + * 是否开启 + */ + private Boolean enable = false; + + /** + * 请求头(响应头)名称不能为空 + * + * 1. 如果该请求头非空,则表示请求参数已被「前端」加密,「后端」需要解密 + * 2. 如果该响应头非空,则表示响应结果已被「后端」加密,「前端」需要解密 + */ + private String header = "X-Api-Encrypt"; + + /** + * 对称加密算法,用于请求/响应的加解密---对称加密算法不能为空 + * + * 目前支持 + * 【对称加密】: + * 1. {@link cn.hutool.crypto.symmetric.SymmetricAlgorithm#AES} + * 2. {@link cn.hutool.crypto.symmetric.SM4#ALGORITHM_NAME} (需要自己二次开发,成本低) + * 【非对称加密】 + * 1. {@link cn.hutool.crypto.asymmetric.AsymmetricAlgorithm#RSA} + * 2. {@link cn.hutool.crypto.asymmetric.SM2} (需要自己二次开发,成本低) + * + * @see 什么是公钥和私钥? + */ + private String algorithm = "AES"; + + /** + * 请求的解密密钥---请求的解密密钥不能为空 + * + * 注意: + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 + * 2. 如果是【非对称加密】时,它「后端」对应的是“私钥”。对应的,「前端」对应的是“公钥”。(重要!!!) + */ + private String requestKey = ""; + + /** + * 响应的加密密钥-- 响应的加密密钥不能为空 + * + * 注意: + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 + * 2. 如果是【非对称加密】时,它「后端」对应的是“公钥”。对应的,「前端」对应的是“私钥”。(重要!!!) + */ + private String responseKey = ""; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiLogProperties.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiLogProperties.java new file mode 100644 index 0000000..0539595 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiLogProperties.java @@ -0,0 +1,26 @@ +package cn.lingniu.framework.plugin.web.config; + +import com.google.common.collect.Lists; +import lombok.Data; + +import java.util.List; + +@Data +public class ApiLogProperties { + + /** + * 是否开启 + */ + private Boolean enable = false; + /** + * 超过3s 的url 打印error日志 + */ + private Long urlErrorTimeOut = 3000l; + + /** + * 排除的url + */ + private List excludeUrls = Lists.newArrayList(); + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/FrameworkWebConfig.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/FrameworkWebConfig.java new file mode 100644 index 0000000..e98ccde --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/FrameworkWebConfig.java @@ -0,0 +1,30 @@ +package cn.lingniu.framework.plugin.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 可扩展多种公共属性配置,时区等问题 + */ +@ConfigurationProperties(prefix = FrameworkWebConfig.PRE_FIX) +@Data +public class FrameworkWebConfig { + + public static final String PRE_FIX = "framework.lingniu.web"; + + + /** + * 接口请求日志 + */ + private ApiLogProperties apiLog = new ApiLogProperties(); + + /** + * api接口加解密 + */ + private ApiEncryptProperties apiEncrypt = new ApiEncryptProperties(); + + /** + * 静态资源配置 + */ + private StaticResourceProperties staticResource = new StaticResourceProperties(); +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/StaticResourceProperties.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/StaticResourceProperties.java new file mode 100644 index 0000000..25e649e --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/StaticResourceProperties.java @@ -0,0 +1,20 @@ +package cn.lingniu.framework.plugin.web.config; + +import lombok.Data; + +/** + * 静态资源配置 + */ +@Data +public class StaticResourceProperties { + + /** + * 是否启用静态资源处理 + */ + private Boolean enable = true; + + /** + * 是否忽略 favicon.ico 404 错误 + */ + private Boolean ignoreFaviconNotFound = true; +} \ No newline at end of file diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/ApiEncrypt.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/ApiEncrypt.java new file mode 100644 index 0000000..04899e6 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/ApiEncrypt.java @@ -0,0 +1,27 @@ +package cn.lingniu.framework.plugin.web.encrypt; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * HTTP API 加解密注解 + */ +@Documented +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiEncrypt { + + /** + * 是否对请求参数进行解密,默认 true + */ + boolean request() default true; + + /** + * 是否对响应结果进行加密,默认 true + */ + boolean response() default true; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiDecryptRequestWrapper.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiDecryptRequestWrapper.java new file mode 100644 index 0000000..b9d37ba --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiDecryptRequestWrapper.java @@ -0,0 +1,84 @@ +package cn.lingniu.framework.plugin.web.encrypt.filter; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.symmetric.SymmetricDecryptor; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * 解密请求 {@link HttpServletRequestWrapper} 实现类 + */ +public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + public ApiDecryptRequestWrapper(HttpServletRequest request, + SymmetricDecryptor symmetricDecryptor, + AsymmetricDecryptor asymmetricDecryptor) throws IOException { + super(request); + // 读取 body,允许 HEX、BASE64 传输 + String requestBody = StrUtil.utf8Str( + IoUtil.readBytes(request.getInputStream(), false)); + + // 解密 body + body = symmetricDecryptor != null ? symmetricDecryptor.decrypt(requestBody) + : asymmetricDecryptor.decrypt(requestBody, KeyType.PrivateKey); + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public int getContentLength() { + return body.length; + } + + @Override + public long getContentLengthLong() { + return body.length; + } + + @Override + public ServletInputStream getInputStream() { + ByteArrayInputStream stream = new ByteArrayInputStream(body); + return new ServletInputStream() { + + @Override + public int read() { + return stream.read(); + } + + @Override + public int available() { + return body.length; + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + }; + } +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptFilter.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptFilter.java new file mode 100644 index 0000000..1a965e4 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptFilter.java @@ -0,0 +1,155 @@ +package cn.lingniu.framework.plugin.web.encrypt.filter; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; +import cn.hutool.crypto.symmetric.SymmetricDecryptor; +import cn.hutool.crypto.symmetric.SymmetricEncryptor; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.exception.util.ServiceExceptionUtil; +import cn.lingniu.framework.plugin.util.object.ObjectUtils; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import cn.lingniu.framework.plugin.web.config.ApiEncryptProperties; +import cn.lingniu.framework.plugin.web.encrypt.ApiEncrypt; +import cn.lingniu.framework.plugin.web.bae.handler.GlobalExceptionHandler; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; + +import java.io.IOException; + + +/** + * API 加密过滤器,处理 {@link ApiEncrypt} 注解。 + * 1. 解密请求参数 + * 2. 加密响应结果 + * + * 疑问:为什么不使用 SpringMVC 的 RequestBodyAdvice 或 ResponseBodyAdvice 机制呢? + * 回答:考虑到项目中会记录访问日志、异常日志,以及 HTTP API 签名等场景,最好是全局级、且提前做解析!!! + */ +@Slf4j +public class ApiEncryptFilter extends OncePerRequestFilter { + + private final ApiEncryptProperties apiEncryptProperties; + + private final RequestMappingHandlerMapping requestMappingHandlerMapping; + + private final GlobalExceptionHandler globalExceptionHandler; + + private final SymmetricDecryptor requestSymmetricDecryptor; + private final AsymmetricDecryptor requestAsymmetricDecryptor; + + private final SymmetricEncryptor responseSymmetricEncryptor; + private final AsymmetricEncryptor responseAsymmetricEncryptor; + + public ApiEncryptFilter(ApiEncryptProperties apiEncryptProperties, + RequestMappingHandlerMapping requestMappingHandlerMapping, + GlobalExceptionHandler globalExceptionHandler) { + this.apiEncryptProperties = apiEncryptProperties; + this.requestMappingHandlerMapping = requestMappingHandlerMapping; + this.globalExceptionHandler = globalExceptionHandler; + if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "AES")) { + this.requestSymmetricDecryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getRequestKey())); + this.requestAsymmetricDecryptor = null; + this.responseSymmetricEncryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getResponseKey())); + this.responseAsymmetricEncryptor = null; + } else if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "RSA")) { + this.requestSymmetricDecryptor = null; + this.requestAsymmetricDecryptor = SecureUtil.rsa(apiEncryptProperties.getRequestKey(), null); + this.responseSymmetricEncryptor = null; + this.responseAsymmetricEncryptor = SecureUtil.rsa(null, apiEncryptProperties.getResponseKey()); + } else { + // 补充说明:如果要支持 SM2、SM4 等算法,可在此处增加对应实例的创建,并添加相应的 Maven 依赖即可。 + throw new IllegalArgumentException("不支持的加密算法:" + apiEncryptProperties.getAlgorithm()); + } + } + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + // 获取 @ApiEncrypt 注解 + ApiEncrypt apiEncrypt = getApiEncrypt(request); + boolean requestEnable = apiEncrypt != null && apiEncrypt.request(); + boolean responseEnable = apiEncrypt != null && apiEncrypt.response(); + String encryptHeader = request.getHeader(apiEncryptProperties.getHeader()); + if (!requestEnable && !responseEnable && StrUtil.isBlank(encryptHeader)) { + chain.doFilter(request, response); + return; + } + + // 1. 解密请求 + if (ObjectUtils.equalsAny(HttpMethod.valueOf(request.getMethod()), + HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)) { + try { + if (StrUtil.isNotBlank(encryptHeader)) { + request = new ApiDecryptRequestWrapper(request, + requestSymmetricDecryptor, requestAsymmetricDecryptor); + } else if (requestEnable) { + throw ServiceExceptionUtil.invalidParamException("请求未包含加密标头,请检查是否正确配置了加密标头"); + } + } catch (Exception ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } + + // 2. 执行过滤器链 + if (responseEnable) { + // 特殊:仅包装,最后执行。目的:Response 内容可以被重复读取!!! + response = new ApiEncryptResponseWrapper(response); + } + chain.doFilter(request, response); + + // 3. 加密响应(真正执行) + if (responseEnable) { + ((ApiEncryptResponseWrapper) response).encrypt(apiEncryptProperties, + responseSymmetricEncryptor, responseAsymmetricEncryptor); + } + } + + /** + * 获取 @ApiEncrypt 注解 + * + * @param request 请求 + */ + @SuppressWarnings("PatternVariableCanBeUsed") + private ApiEncrypt getApiEncrypt(HttpServletRequest request) { + try { + // 特殊:兼容 SpringBoot 2.X 版本会报错的问题 https://t.zsxq.com/kqyiB + if (!ServletRequestPathUtils.hasParsedRequestPath(request)) { + ServletRequestPathUtils.parseAndCache(request); + } + + // 解析 @ApiEncrypt 注解 + HandlerExecutionChain mappingHandler = requestMappingHandlerMapping.getHandler(request); + if (mappingHandler == null) { + return null; + } + Object handler = mappingHandler.getHandler(); + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + ApiEncrypt annotation = handlerMethod.getMethodAnnotation(ApiEncrypt.class); + if (annotation == null) { + annotation = handlerMethod.getBeanType().getAnnotation(ApiEncrypt.class); + } + return annotation; + } + } catch (Exception e) { + log.error("[getApiEncrypt][url({}/{}) 获取 @ApiEncrypt 注解失败]", + request.getRequestURI(), request.getMethod(), e); + } + return null; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptResponseWrapper.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptResponseWrapper.java new file mode 100644 index 0000000..73776a7 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptResponseWrapper.java @@ -0,0 +1,108 @@ +package cn.lingniu.framework.plugin.web.encrypt.filter; + +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.symmetric.SymmetricEncryptor; +import cn.lingniu.framework.plugin.web.config.ApiEncryptProperties; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +/** + * 加密响应 {@link HttpServletResponseWrapper} 实现类 + * + */ +public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper { + + private final ByteArrayOutputStream byteArrayOutputStream; + private final ServletOutputStream servletOutputStream; + private final PrintWriter printWriter; + + public ApiEncryptResponseWrapper(HttpServletResponse response) { + super(response); + this.byteArrayOutputStream = new ByteArrayOutputStream(); + this.servletOutputStream = this.getOutputStream(); + this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream)); + } + + public void encrypt(ApiEncryptProperties properties, + SymmetricEncryptor symmetricEncryptor, + AsymmetricEncryptor asymmetricEncryptor) throws IOException { + // 1.1 清空 body + HttpServletResponse response = (HttpServletResponse) this.getResponse(); + response.resetBuffer(); + // 1.2 获取 body + this.flushBuffer(); + byte[] body = byteArrayOutputStream.toByteArray(); + // 2. 添加加密 header 标识 + this.addHeader(properties.getHeader(), "true"); + // 特殊:特殊:https://juejin.cn/post/6867327674675625992 + this.addHeader("Access-Control-Expose-Headers", properties.getHeader()); + + // 3.1 加密 body + String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body) + : asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey); + // 3.2 输出加密后的 body:(设置 header 要放在 response 的 write 之前) + response.getWriter().write(encryptedBody); + } + + @Override + public PrintWriter getWriter() { + return printWriter; + } + + @Override + public void flushBuffer() throws IOException { + if (servletOutputStream != null) { + servletOutputStream.flush(); + } + if (printWriter != null) { + printWriter.flush(); + } + } + + @Override + public void reset() { + byteArrayOutputStream.reset(); + } + + @Override + public ServletOutputStream getOutputStream() { + return new ServletOutputStream() { + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + } + + @Override + public void write(int b) { + byteArrayOutputStream.write(b); + } + + @Override + @SuppressWarnings("NullableProblems") + public void write(byte[] b) throws IOException { + byteArrayOutputStream.write(b); + } + + @Override + @SuppressWarnings("NullableProblems") + public void write(byte[] b, int off, int len) { + byteArrayOutputStream.write(b, off, len); + } + + }; + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiEncryptAutoConfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiEncryptAutoConfiguration.java new file mode 100644 index 0000000..4047d74 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiEncryptAutoConfiguration.java @@ -0,0 +1,34 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum; +import cn.lingniu.framework.plugin.web.config.ApiEncryptProperties; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import cn.lingniu.framework.plugin.web.encrypt.filter.ApiEncryptFilter; +import cn.lingniu.framework.plugin.web.bae.handler.GlobalExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + + +@AutoConfiguration +@Slf4j +@EnableConfigurationProperties(ApiEncryptProperties.class) +@ConditionalOnProperty(prefix = "framework.lingniu.web.apiEncrypt", name = "enable", havingValue = "true") +public class ApiEncryptAutoConfiguration { + + @Bean + public FilterRegistrationBean apiEncryptFilter(FrameworkWebConfig frameworkWebConfig, + RequestMappingHandlerMapping requestMappingHandlerMapping, + GlobalExceptionHandler globalExceptionHandler) { + ApiEncryptFilter filter = new ApiEncryptFilter(frameworkWebConfig.getApiEncrypt(), + requestMappingHandlerMapping, globalExceptionHandler); + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(WebFilterOrderEnum.API_ENCRYPT_FILTER); + return bean; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiLogAutoConfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiLogAutoConfiguration.java new file mode 100644 index 0000000..4221452 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiLogAutoConfiguration.java @@ -0,0 +1,44 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum; +import cn.lingniu.framework.plugin.web.apilog.filter.ApiAccessLogFilter; +import cn.lingniu.framework.plugin.web.apilog.interceptor.ApiAccessLogInterceptor; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import jakarta.servlet.Filter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@AutoConfiguration(after = WebAutoConfiguration.class) +public class ApiLogAutoConfiguration implements WebMvcConfigurer { + + /** + * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 + */ + @Bean + @ConditionalOnProperty(prefix = "framework.lingniu.web.apiLog", value = "enable", matchIfMissing = false) + public FilterRegistrationBean apiAccessLogFilter(FrameworkWebConfig frameworkWebConfig, + @Value("${spring.application.name}") String applicationName, + ApiAccessLogCommonApi apiAccessLogApi) { + ApiAccessLogFilter filter = new ApiAccessLogFilter(frameworkWebConfig, applicationName, apiAccessLogApi); + return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new ApiAccessLogInterceptor()); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/EnableGlobalResponseBody.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/EnableGlobalResponseBody.java new file mode 100644 index 0000000..1d026e3 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/EnableGlobalResponseBody.java @@ -0,0 +1,20 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.web.bae.handler.GlobalResponseBodyHandler; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 待扩展 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Import({GlobalResponseBodyHandler.class}) +public @interface EnableGlobalResponseBody { +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/JacksonAutoConfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/JacksonAutoConfiguration.java new file mode 100644 index 0000000..c8f8e60 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/JacksonAutoConfiguration.java @@ -0,0 +1,77 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.json.databind.NumberSerializer; +import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeDeserializer; +import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeSerializer; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@AutoConfiguration(after = org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class) +@Slf4j +public class JacksonAutoConfiguration { + + /** + * 从 Builder 源头定制(关键:使用 *ByType,避免 handledType 要求) + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer ldtEpochMillisCustomizer() { + return builder -> builder + // Long -> Number + .serializerByType(Long.class, NumberSerializer.INSTANCE) + .serializerByType(Long.TYPE, NumberSerializer.INSTANCE) + // LocalDate / LocalTime + .serializerByType(LocalDate.class, LocalDateSerializer.INSTANCE) + .deserializerByType(LocalDate.class, LocalDateDeserializer.INSTANCE) + .serializerByType(LocalTime.class, LocalTimeSerializer.INSTANCE) + .deserializerByType(LocalTime.class, LocalTimeDeserializer.INSTANCE) + // LocalDateTime < - > EpochMillis + .serializerByType(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE) + .deserializerByType(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); + } + + /** + * 以 Bean 形式暴露 Module(Boot 会自动注册到所有 ObjectMapper) + */ + @Bean + public Module timestampSupportModuleBean() { + SimpleModule m = new SimpleModule("TimestampSupportModule"); + // Long -> Number,避免前端精度丢失 + m.addSerializer(Long.class, NumberSerializer.INSTANCE); + m.addSerializer(Long.TYPE, NumberSerializer.INSTANCE); + // LocalDate / LocalTime + m.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE); + m.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE); + m.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE); + m.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE); + // LocalDateTime < - > EpochMillis + m.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE); + m.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); + return m; + } + + /** + * 初始化全局 JsonUtils,直接使用主 ObjectMapper + */ + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public JsonUtils jsonUtils(ObjectMapper objectMapper) { + JsonUtils.init(objectMapper); + log.debug("[init][初始化 JsonUtils 成功]"); + return new JsonUtils(); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseDetectAutoconfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseDetectAutoconfiguration.java new file mode 100644 index 0000000..9bde5bc --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseDetectAutoconfiguration.java @@ -0,0 +1,52 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.web.bae.interceptor.ResponseCheckInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + + +@Slf4j +@Configuration +@ConditionalOnMissingClass({"org.springframework.security.authentication.AuthenticationManager"}) +public class ResponseDetectAutoconfiguration implements CommandLineRunner, WebMvcConfigurer { + + @Autowired + ResponseCheckInterceptor responseCheckInterceptor; + + /** + * 默认排除URL清单 + */ + public static final Set excludesPattern= new HashSet<>( + Arrays.asList("*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,/doc.html,/webjars/**,/v2/api-docs,/actuator/**" + .split("\\s*,\\s*"))); + + @Override + public void addInterceptors(InterceptorRegistry registry) { + List urls = new ArrayList<>(); + urls.addAll(excludesPattern.stream().collect(Collectors.toList())); + registry.addInterceptor(responseCheckInterceptor) + .addPathPatterns( Collections.singletonList("/**") ) + .excludePathPatterns(urls); + } + + @Override + public void run(String... args) { + if(log.isInfoEnabled()) { + log.info("\r\n\t\tDEV/SIT/UAT/TEST 环境已开启返回响应体规范检测功能,请按lingniu-web框架规范使用响应体结构~~"); + } + } +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseInterceptorAutoconfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseInterceptorAutoconfiguration.java new file mode 100644 index 0000000..bbb8e42 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseInterceptorAutoconfiguration.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.web.bae.interceptor.ResponseCheckInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Slf4j +@Configuration +@ConditionalOnMissingClass({"org.springframework.security.authentication.AuthenticationManager"}) +public class ResponseInterceptorAutoconfiguration { + @Bean + public ResponseCheckInterceptor responseInterceptor(){ + return new ResponseCheckInterceptor(); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/WebAutoConfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/WebAutoConfiguration.java new file mode 100644 index 0000000..a8fc3a1 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/WebAutoConfiguration.java @@ -0,0 +1,71 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum; +import cn.lingniu.framework.plugin.web.ApplicationStartEventListener; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import cn.lingniu.framework.plugin.web.bae.filter.CacheRequestBodyFilter; +import cn.lingniu.framework.plugin.web.bae.handler.GlobalExceptionHandler; +import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils; +import jakarta.servlet.Filter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.Order; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@AutoConfiguration +@EnableConfigurationProperties(FrameworkWebConfig.class) +@Import({GlobalExceptionHandler.class, ApplicationStartEventListener.class}) +public class WebAutoConfiguration { + + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public WebFrameworkUtils webFrameworkUtils(FrameworkWebConfig frameworkWebConfig) { + // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean + return new WebFrameworkUtils(frameworkWebConfig); + } + + // ========== Filter 相关 ========== + + /** + * 创建 CorsFilter Bean,解决跨域问题 + */ + @Bean + @Order(value = WebFilterOrderEnum.CORS_FILTER) // 特殊:修复因执行顺序影响到跨域配置不生效问题 + public FilterRegistrationBean corsFilterBean() { + // 创建 CorsConfiguration 对象 + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); // 设置访问源地址 + config.addAllowedHeader("*"); // 设置访问源请求头 + config.addAllowedMethod("*"); // 设置访问源请求方法 + // 创建 UrlBasedCorsConfigurationSource 对象 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 + return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER); + } + + /** + * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 + */ + @Bean + public FilterRegistrationBean requestBodyCacheFilter() { + return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER); + } + + + + public static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/favicon/FaviconAutoConfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/favicon/FaviconAutoConfiguration.java new file mode 100644 index 0000000..b2e5179 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/favicon/FaviconAutoConfiguration.java @@ -0,0 +1,41 @@ +package cn.lingniu.framework.plugin.web.init.favicon; + +import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum; +import cn.lingniu.framework.plugin.web.apilog.filter.ApiAccessLogFilter; +import cn.lingniu.framework.plugin.web.apilog.interceptor.ApiAccessLogInterceptor; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import cn.lingniu.framework.plugin.web.init.WebAutoConfiguration; +import jakarta.servlet.Filter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@AutoConfiguration(after = WebAutoConfiguration.class) +public class FaviconAutoConfiguration implements WebMvcConfigurer { + + /** + * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 + */ + @Bean + public FilterRegistrationBean faviconFilter() { + FaviconFilter filter = new FaviconFilter(); + return createFilterBean(filter, WebFilterOrderEnum.FAVICON_CONTEXT_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new ApiAccessLogInterceptor()); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/favicon/FaviconFilter.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/favicon/FaviconFilter.java new file mode 100644 index 0000000..3a83bd1 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/favicon/FaviconFilter.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.web.init.favicon; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class FaviconFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + if ("/favicon.ico".equals(request.getRequestURI())) { + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.setStatus(HttpServletResponse.SC_NOT_FOUND); // 或者 SC_OK 并设置内容为空(如果需要) + } else { + filterChain.doFilter(request, response); + } + } +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..f545065 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +cn.lingniu.framework.plugin.web.init.JacksonAutoConfiguration +cn.lingniu.framework.plugin.web.init.WebAutoConfiguration +cn.lingniu.framework.plugin.web.init.ResponseInterceptorAutoconfiguration +cn.lingniu.framework.plugin.web.init.ResponseDetectAutoconfiguration +cn.lingniu.framework.plugin.web.init.ApiLogAutoConfiguration +cn.lingniu.framework.plugin.web.init.ApiEncryptAutoConfiguration +cn.lingniu.framework.plugin.web.init.favicon.FaviconAutoConfiguration diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/web.md b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/web.md new file mode 100644 index 0000000..000726a --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/web.md @@ -0,0 +1,57 @@ +# 框架核心web应用模块 + + +## 概述 (Overview) + +1. 定位: 基于 springmvc封装的规范web组件 +2. 核心能力: + * 统一返回结果检测:非生产环境接口返回参数不规范,控制台会打印error日志。 + * 请求日志:支持注解方式规范打印请求日志,可扩展调用db。 + * 参数加解密:可解密请求参数,加密响应结果。 + * 全局异常处理机制:GlobalExceptionHandler + * 统一结果返回处理:GlobalResponseBodyHandler +3. 适用场景: + * Web应用公共功能 + + +## 统一返回结果检测 +- 检测条件:profile:test","dev","uat","sit +- 检测实现类:ResponseCheckInterceptor +- 加载类: + ResponseDetectAutoconfiguration + ResponseInterceptorAutoconfiguration + +## 请求日志 +- 使用方式:@ApiAccessLog +- 参数配置请查看:FrameworkWebConfig.ApiLogProperties +- filter:ApiAccessLogFilter +- 最终输出打印:ApiAccessLogServiceImpl + + +## 统一返回结果处理:CommonResult +- 实现参考:GlobalResponseBodyHandler + +## 统一异常处理:GlobalExceptionHandler,并记录日志-ApiErrorLogServiceImpl + +## api 加解密 +- 使用方式:@ApiEncrypt +- 参数配置请查看:FrameworkWebConfig.ApiEncryptProperties +- filter:ApiEncryptFilter +- 逻辑处理:ApiDecryptRequestWrapper、ApiEncryptResponseWrapper + +## 如何配置 + +```yaml +framework: + lingniu: + web: + apiLog: + #开关 + enable: true + apiEncrypt: + #开关 + enable: false + algorithm: "AES" + requestKey: "xxx" + responseKey: "xxx" +``` diff --git a/lingniu-framework-starters/lingniu-framework-starters-nacos/pom.xml b/lingniu-framework-starters/lingniu-framework-starters-nacos/pom.xml new file mode 100644 index 0000000..87aa422 --- /dev/null +++ b/lingniu-framework-starters/lingniu-framework-starters-nacos/pom.xml @@ -0,0 +1,32 @@ + + + + cn.lingniu.framework + lingniu-framework-starters-web + 1.0.0-SNAPSHOT + ../lingniu-framework-starters-web + + 4.0.0 + + lingniu-framework-starters-nacos + pom + + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-nacos + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + + + \ No newline at end of file diff --git a/lingniu-framework-starters/lingniu-framework-starters-web/pom.xml b/lingniu-framework-starters/lingniu-framework-starters-web/pom.xml new file mode 100644 index 0000000..b088381 --- /dev/null +++ b/lingniu-framework-starters/lingniu-framework-starters-web/pom.xml @@ -0,0 +1,62 @@ + + + + cn.lingniu.framework + lingniu-framework-starters + 1.0.0-SNAPSHOT + ../ + + 4.0.0 + + lingniu-framework-starters-web + pom + + + + javax.validation + validation-api + + + + org.hibernate.validator + hibernate-validator + compile + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + cn.lingniu.framework + lingniu-framework-plugin-web + + + + org.yaml + snakeyaml + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + \ No newline at end of file diff --git a/lingniu-framework-starters/pom.xml b/lingniu-framework-starters/pom.xml new file mode 100644 index 0000000..cc6bb03 --- /dev/null +++ b/lingniu-framework-starters/pom.xml @@ -0,0 +1,20 @@ + + + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../lingniu-framework-dependencies + + + 4.0.0 + pom + lingniu-framework-starters + + + + lingniu-framework-starters-web + lingniu-framework-starters-nacos + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..68534ad --- /dev/null +++ b/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + cn.lingniu.framework + lingniu-framework-parent + 1.0.0-SNAPSHOT + + org.springframework.boot + spring-boot-starter-parent + 3.5.0 + + + + lingniu-framework-dependencies + lingniu-framework-plugin/config/lingniu-framework-plugin-apollo + lingniu-framework-plugin/lingniu-framework-plugin-util + lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel + lingniu-framework-plugin/lingniu-framework-plugin-core + lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob + lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis + lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson + lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache + lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq + lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking + lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus + lingniu-framework-plugin/web/lingniu-framework-plugin-web + lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common + lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos + + + + + + pom + lingniu-framework-parent + http://maven.apache.org + + + + + lnh2e-releases + https://nexus.lnh2e.com/repository/maven-releases/ + + + lnh2e-snapshots + https://nexus.lnh2e.com/repository/maven-snapshots/ + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + maven-source-plugin + + true + + + + compile + + jar + + + + + + + \ No newline at end of file