From 98c057de11d989a9d6a02fb91ddb94c8d4cecc26 Mon Sep 17 00:00:00 2001 From: Eric <01714308@yto.net.cn> Date: Fri, 16 Jan 2026 18:51:16 +0800 Subject: [PATCH] Initial commit --- .gitignore | 45 + .idea/.gitignore | 3 + .idea/checkstyle-idea.xml | 15 + README.md | 0 demo/lingniu-framework-demo/pom.xml | 106 +++ .../java/cn/lingniu/demo/DemoApplication.java | 16 + .../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 | 71 ++ .../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 | 100 +++ .../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 | 55 ++ .../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 | 64 ++ .../src/main/resources/application-web.yml | 12 + .../src/main/resources/application-xxljob.yml | 21 + .../src/main/resources/application.yml | 48 ++ .../src/main/resources/applicationTest.yml | 22 + .../src/main/resources/logback-spring.xml | 65 ++ 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 ++ framework.md | 14 + img.png | Bin 0 -> 171637 bytes lingniu-framework-dependencies/pom.xml | 768 ++++++++++++++++++ .../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 + .../main/resources/META-INF/spring.factories | 2 + .../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 | 46 ++ .../init/RedissonAutoConfiguration.java | 53 ++ .../RedissonBeanRegisterPostProcessor.java | 75 ++ ...sonDistributedLockAspectConfiguration.java | 92 +++ .../main/resources/META-INF/spring.factories | 3 + .../src/main/resources/redisson-config.md | 121 +++ .../lingniu-framework-plugin-apollo/pom.xml | 56 ++ .../plugin/apollo/config/ApolloConfig.java | 32 + .../extend/ApolloAnnotationProcessor.java | 79 ++ .../extend/ApolloConfigChangeLogListener.java | 58 ++ .../plugin/apollo/extend/ClassPoolUtils.java | 26 + .../FrameworkApolloConfigRegistrarHelper.java | 52 ++ .../apollo/init/ApolloAutoConfiguration.java | 26 + .../plugin/apollo/init/ApolloInit.java | 114 +++ ...llo.spring.spi.ApolloConfigRegistrarHelper | 1 + .../main/resources/META-INF/spring.factories | 4 + .../src/main/resources/apollo-config.md | 37 + .../lingniu-framework-plugin-mybatis/pom.xml | 111 +++ .../mybatis/base/DBSqlLogInterceptor.java | 181 +++++ .../plugin/mybatis/base/DataSourceEnum.java | 22 + .../mybatis/base/DruidAdRemoveFilter.java | 36 + .../mybatis/config/DataSourceConfig.java | 29 + .../init/DataSourceAutoConfiguration.java | 67 ++ .../plugin/mybatis/init/DataSourceInit.java | 19 + .../main/resources/META-INF/spring.factories | 4 + .../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 + .../main/resources/META-INF/spring.factories | 2 + .../src/main/resources/xxl-job.md | 43 + .../lingniu-framework-plugin-core/pom.xml | 85 ++ .../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 | 111 +++ .../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 | 4 + .../pom.xml | 55 ++ .../main/resources/META-INF/spring.factories | 0 .../src/main/resources/nacos.md | 72 ++ .../pom.xml | 64 ++ .../plugin/prometheus/PrometheusCounter.java | 34 + .../plugin/prometheus/PrometheusMetrics.java | 19 + .../plugin/prometheus/PrometheusSummary.java | 34 + .../aspect/BasePrometheusAspect.java | 41 + .../aspect/PrometheusCounterAspect.java | 75 ++ .../aspect/PrometheusMetricsAspect.java | 73 ++ .../prometheus/aspect/PrometheusService.java | 56 ++ .../aspect/PrometheusServiceImpl.java | 111 +++ .../aspect/PrometheusSummaryAspect.java | 98 +++ .../prometheus/config/PrometheusConfig.java | 32 + .../init/PrometheusConfiguration.java | 81 ++ .../prometheus/init/PrometheusInit.java | 116 +++ .../main/resources/META-INF/spring.factories | 4 + .../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 | 105 +++ .../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 | 315 +++++++ .../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 | 208 +++++ .../main/resources/META-INF/spring.factories | 3 + .../src/main/resources/mq-config.md | 118 +++ .../web/lingniu-framework-plugin-web/pom.xml | 101 +++ .../web/ApplicationStartEventListener.java | 45 + .../plugin/web/apilog/ApiAccessLog.java | 49 ++ .../web/apilog/enums/OperateTypeEnum.java | 51 ++ .../web/apilog/filter/ApiAccessLogFilter.java | 242 ++++++ .../interceptor/ApiAccessLogInterceptor.java | 100 +++ .../apilog/logger/ApiAccessLogApiImpl.java | 26 + .../logger/ApiAccessLogServiceImpl.java | 32 + .../web/apilog/logger/ApiErrorLogApiImpl.java | 25 + .../apilog/logger/ApiErrorLogServiceImpl.java | 32 + .../logger/api/ApiAccessLogCommonApi.java | 30 + .../logger/api/ApiAccessLogService.java | 18 + .../logger/api/ApiErrorLogCommonApi.java | 29 + .../apilog/logger/api/ApiErrorLogService.java | 18 + .../model/ApiAccessLogCreateReqDTO.java | 89 ++ .../apilog/logger/model/ApiAccessLogDO.java | 99 +++ .../logger/model/ApiErrorLogCreateReqDTO.java | 101 +++ .../apilog/logger/model/ApiErrorLogDO.java | 118 +++ .../web/bae/filter/ApiRequestFilter.java | 38 + .../bae/filter/CacheRequestBodyFilter.java | 41 + .../bae/filter/CacheRequestBodyWrapper.java | 75 ++ .../bae/handler/GlobalExceptionHandler.java | 383 +++++++++ .../handler/GlobalResponseBodyHandler.java | 42 + .../interceptor/ResponseCheckInterceptor.java | 39 + .../web/bae/util/WebFrameworkUtils.java | 123 +++ .../web/config/ApiEncryptProperties.java | 63 ++ .../plugin/web/config/ApiLogProperties.java | 22 + .../plugin/web/config/FrameworkWebConfig.java | 25 + .../plugin/web/encrypt/ApiEncrypt.java | 27 + .../filter/ApiDecryptRequestWrapper.java | 83 ++ .../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 | 74 ++ .../main/resources/META-INF/spring.factories | 8 + .../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 | 81 ++ 280 files changed, 16665 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/checkstyle-idea.xml 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.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 framework.md create mode 100644 img.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.factories 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/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.factories 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/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/spring.factories 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/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.factories 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-nacos/pom.xml create mode 100644 lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/META-INF/spring.factories 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/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/PrometheusService.java create mode 100644 lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusServiceImpl.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/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/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/resources/META-INF/spring.factories 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/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml 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/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..0b75550 --- /dev/null +++ b/demo/lingniu-framework-demo/pom.xml @@ -0,0 +1,106 @@ + + + 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 + + + + + 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..1f9f6b9 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/DemoApplication.java @@ -0,0 +1,16 @@ +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..56ddc07 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/JetCacheController.java @@ -0,0 +1,71 @@ +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 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 javax.annotation.Resource; +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..7185f79 --- /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..3793871 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/RedissonLockControllerDemo.java @@ -0,0 +1,100 @@ +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..9b4d76e --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebController.java @@ -0,0 +1,55 @@ +package cn.lingniu.demo.web; + + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.web.apilog.ApiAccessLog; +import org.checkerframework.checker.units.qual.A; +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..67e461e --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-rocketmq.yml @@ -0,0 +1,64 @@ + +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: + 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..5953fde --- /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://nacos-uat-new-inter.xx.net.cn:8848 #注册中心地址 + username: nacos_test #注册中心用户名 + password: nacos_test #注册中心密码 + + #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: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 + + +# apollo 配置 +framework: + lingniu: + apollo: + #namespaces必须配置,配置文件列表,以逗号分割 + namespaces: application,applicationTest + # 是否开启 Apollo 配置,默认true + enabled: true + # appId 等于 spring.application.name,无需配置 + appId: lingniu-framework-demo + # meta 地址,框架自动获取,无需配置 + meta: http://10.130.36.237:8180 diff --git a/demo/lingniu-framework-demo/src/main/resources/applicationTest.yml b/demo/lingniu-framework-demo/src/main/resources/applicationTest.yml new file mode 100644 index 0000000..3eb9989 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/applicationTest.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..b78b63c --- /dev/null +++ b/demo/lingniu-framework-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/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..4ca9a8b --- /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://nacos-uat-new-inter.xx.net.cn:8848 #注册中心地址 + username: nacos_test #注册中心用户名 + password: nacos_test #注册中心密码 + 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/framework.md b/framework.md new file mode 100644 index 0000000..2cc6438 --- /dev/null +++ b/framework.md @@ -0,0 +1,14 @@ +## lingniu-framework 介绍 +- 是一个基于 SpringBoot/cloud、Apollo、Nacos 等开发的内部框架。 +## lingniu-framework 特点 +* 基于 Spring 完整的 MVC + ORM 支持。 +* 支持多数据源、分库分表和分布式事务。 +* 统一注册:支持Nacos作为注册中心,实现多配置、分群组、分命名空间、多业务模块的注册和发现功能; +* 支持基于 Apollo 分布式配置中心 +* 完整的分布式缓存、分布式session、分布式锁支持 +* 多租户功能:集成Mybatis Plus +* 组件(Mysql/Oracle/Redis/JetCache/XXLJob等)多租户友好支持 +* 消息中心:集成消息中间件RocketMQ,对业务进行异步处理; +* 等等 +## 组件介绍 +![img.png](img.png) \ No newline at end of file diff --git a/img.png b/img.png new file mode 100644 index 0000000000000000000000000000000000000000..963a98c795bf303044794b705b40ebdf1d52e7f9 GIT binary patch literal 171637 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

-szzaB#YQ$2&k2Eq}OeEwxaB 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

BE1auNJ%#e_+K)R*N-1Glx^TdBJu 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 + + + cn.lingniu.framework + lingniu-framework-parent + 1.0.0-SNAPSHOT + + 4.0.0 + pom + lingniu-framework-dependencies + + + 1.0.0-SNAPSHOT + 1.7.0 + 1.9.2 + 3.7.5.v20211028-RELEASE + 3.9.0 + 1.7.14 + 1.70 + 2021.0.6.1 + 3.1.0 + 2.18.0 + 2.18.0 + 5.8.38 + 4.9.3 + 2.7.18 + 2021.0.6 + 11.10 + UTF-8 + 3.17.0 + 3.2.2 + 4.13 + 1.8 + 0.6.4 + 1.0.2 + 1.1.0.Final + 27.0-jre + 1.18.38 + 1.2.25 + -Xmx1G + 1.1.1 + 4.5.1 + + 1.10 + 1.5.0 + 2.2.3 + 1.8 + 2.6 + 2.6.1 + 3.1 + 2.10 + 2.8.2 + 2.10 + 1.4 + 2.18.1 + 2.5.2 + + 1.10 + 2.2 + 2.6 + + 2.10.4 + 2.7 + 2.4.3 + 3.5.1 + 2.4 + 2.18.1 + 2.6 + 2.2 + 1.2.9 + 1.7.32 + 2.7.1 + 2.13.0 + 2.2.1 + 1.9.6 + 2.3.0 + 1.2.83 + + 1.4.2 + 2.16.0 + 1.1.2 + 6.4.2.RELEASE + 3.7.1 + 2.18.0 + 4.0.3 + 3.1.5 + 2.1.1 + 5.8.38 + 2.5.1 + 4.5 + 3.5.3.2 + 3.5.3.2 + 4.2.0 + 2.1.2 + 3.5.14 + 2.3.2 + 1.4.2 + 5.3.3 + 1.1.5 + 2.13.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.pagehelper + pagehelper-spring-boot-starter + ${pagehelper-starter.version} + + + + com.github.pagehelper + pagehelper + ${pagehelper.version} + + + + com.github.jsqlparser + jsqlparser + ${jsqlparser.version} + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis-spring-boot.version} + + + + org.mybatis + mybatis-spring + ${mybatis-spring.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + io.micrometer + micrometer-registry-prometheus + 1.9.17 + + + + io.lettuce + lettuce-core + ${lettuce.version} + + + ojdbc + ojdbc6 + 11.2.0.4.0 + + + org.bouncycastle + bcprov-jdk15on + ${bcprov.version} + + + one.util + streamex + 0.7.0 + + + org.jooq + jool + 0.9.13 + + + org.pf4j + pf4j + ${pf4j-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 + ${hutoll.version} + + + + + com.jfinal + jfinal + 4.8 + + + + com.jfinal + jfinal-weixin + 2.5 + + + + net.dreamlu + mica-auto + 1.1.0 + + + + org.unidal.framework + foundation-service + 4.1.1 + + + org.redisson + redisson + 3.17.5 + + + + com.caucho + hessian + ${hessian.version} + + + + javax.persistence + persistence-api + ${javax.persistence.version} + + + + 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 + + + org.mybatis + mybatis-typehandlers-jsr310 + 1.0.1 + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus-boot-starter.version} + + + com.baomidou + mybatis-plus + ${mybatis-plus-boot-starter.version} + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${dynamic-datasource-starter.version} + + + + joda-time + joda-time + ${joda-time.version} + + + com.alibaba + druid-spring-boot-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} + + + slf4j-log4j12 + org.slf4j + ${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 + + + + org.jsoup + jsoup + ${jsoup.version} + + + commons-net + commons-net + ${commons-net.version} + + + org.javassist + javassist + 3.24.1-GA + + + com.alibaba + fastjson + ${fastjson.version} + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + io.github.openfeign + feign-core + ${feign-core.version} + + + org.t-io + tio-core + ${tio.version} + + + org.t-io + tio-websocket-server + ${tio.version} + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + com.alibaba.nacos + nacos-client + ${nacos-client.version} + + + + com.baomidou + mybatis-plus-core + ${mybatis-plus.version} + + + org.mybatis + mybatis + ${mybatis.version} + + + + org.codehaus.plexus + plexus-container-default + ${plexus.container.default} + + + com.google.collections + google-collections + + + + + + com.belerweb + pinyin4j + ${pinyin.version} + + + + org.dromara.dynamictp + dynamic-tp-spring-boot-starter-apollo + ${dynamictp.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-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.factories b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..6188ab4 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.jetcache.autoconfigure.JetCacheAutoConfiguration \ No newline at end of file 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..d13ddd8 --- /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,46 @@ +package cn.lingniu.framework.plugin.redisson.config; + +import lombok.Data; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +/** + * 详细配置 + */ +@Data +public class RedissonProperties { + + /** + * redis连接串信息 host:port,host:port, 多个用,分割,最后一个是;是密码,可以不配置 + */ + private String redisAddresses; + + @Pattern(regexp = "^(SLAVE|MASTER|MASTER_SLAVE)$", message = "readMode must be one of SLAVE, MASTER, MASTER_SLAVE") + private String readMode = "MASTER"; + /** + * 类型 + */ + private RedisType storageType = RedisType.CLUSTER; + private String clientName = ""; + + @Min(value = 0, message = "database must be greater than or equal to 0") + @Max(value = 15, message = "database must be less than or equal to 15") + 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/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..245931d --- /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,92 @@ +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +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.factories b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..fe4d817 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.redisson.init.RedissonAutoConfiguration \ No newline at end of file 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..a1339f3 --- /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,32 @@ +package cn.lingniu.framework.plugin.apollo.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import javax.validation.constraints.NotBlank; + +@Data +@ConfigurationProperties(prefix = ApolloConfig.PRE_FIX) +public class ApolloConfig { + + public final static String PRE_FIX = "framework.lingniu.apollo"; + + /** + * 是否开始Apollo配置 + */ + private Boolean enabled = true; + /** + * Meta地址 + */ + @NotBlank + 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..29c5653 --- /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 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; + +import javax.annotation.Resource; + +@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/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..b4303a6 --- /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,52 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import com.ctrip.framework.apollo.core.spi.Ordered; +import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValueProcessor; +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.SpringValueDefinitionProcessor; +import com.ctrip.framework.apollo.spring.util.BeanRegistrationUtil; +import com.google.common.collect.Lists; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.annotation.AnnotationAttributes; +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 { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + AnnotationAttributes attributes = AnnotationAttributes + .fromMap(importingClassMetadata.getAnnotationAttributes(EnableApolloConfig.class.getName())); + String[] namespaces = System.getProperty("apollo.bootstrap.namespaces", "application").split(","); + //attributes.getStringArray("value"); + int order = attributes.getNumber("order"); + PropertySourcesProcessor.addNamespaces(Lists.newArrayList(namespaces), order); + + 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.getName(), + PropertySourcesPlaceholderConfigurer.class, propertySourcesPlaceholderPropertyValues); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesProcessor.class.getName(), + PropertySourcesProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(), + ApolloAnnotationProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), + SpringValueProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueDefinitionProcessor.class.getName(), + SpringValueDefinitionProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloJsonValueProcessor.class.getName(), + ApolloJsonValueProcessor.class); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } +} 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..81d2f5f --- /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,114 @@ +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.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 static boolean isload = false; + + private String applicationName; + + public final static String PROJECT = "Apollo"; + private final static String AppId = "app.id"; + public final static String ApolloMeta = "apollo.meta"; + private final static String ApolloBootstrapEnabled = "apollo.bootstrap.enabled"; + private final static String ApolloBootstrapNamespaces = "apollo.bootstrap.namespaces"; + private final static String ApolloBootstrapEagerLoadEnabled = "apollo.bootstrap.eagerLoad.enabled"; + private final static String UserDir = "user.dir"; + private final static String ApolloCacheDir = "apollo.cacheDir"; + public final static String Env = "env"; + + 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))) { + this.replaceCatInit(environment); + return; + } + if (ObjectEmptyUtils.isEmpty(environment.getProperty(FrameworkEnabled)) || environment.getProperty(FrameworkEnabled).equalsIgnoreCase("false")) { + setDefaultProperty(ApolloBootstrapEnabled, "false"); + setDefaultProperty(ApolloBootstrapEagerLoadEnabled, "false"); + return; + } + //默认设置app.id + PropertyUtils.setDefaultInitProperty(AppId, appName); + setDefaultProperty(ApolloMeta, environment.getProperty(FrameworkMeta)); + setDefaultProperty(ApolloBootstrapEnabled, "true"); + setDefaultProperty(ApolloBootstrapNamespaces, environment.getProperty(NameSpaces, "application")); + setDefaultProperty(ApolloBootstrapEagerLoadEnabled, "true"); + setDefaultProperty("spring.boot.enableautoconfiguration", "true"); + setDefaultProperty(Env, profile.toUpperCase()); + setDefaultProperty(ApolloCacheDir, System.getProperty(UserDir) + File.separator + "apolloConfig" + File.separator); + this.replaceCatInit(environment); + } + + private void replaceCatInit(ConfigurableEnvironment environment) { + try { + ClassPool classPool = ClassPoolUtils.getInstance(); + CtClass ctClass = classPool.get("com.ctrip.framework.apollo.tracer.internals.DefaultMessageProducerManager"); + if (!isload) { + isload = true; + CtConstructor[] constructors = ctClass.getConstructors(); + if (constructors != null && constructors.length > 0) { + CtConstructor constructor = constructors[0]; + constructor.setBody(newMethodCode()); + } + if (ctClass.isFrozen()) { + ctClass.defrost(); + } + ctClass.toClass(); + log.info("{} {} {}", ApolloInit.class, PROJECT, "重写 apollo init ok"); + } + } catch (Exception exp) { + log.error("{} {} {} {}", ApolloInit.class, PROJECT, "重写 apollo init 异常", exp); + } + } + + private String newMethodCode() { + String code = "{" + " producer = new com.ctrip.framework.apollo.tracer.internals.NullMessageProducerManager().getProducer();" + "}"; + return code; + } + + 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/spring.factories b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..455cf8f --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.apollo.init.ApolloAutoConfiguration +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/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..cee1683 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/pom.xml @@ -0,0 +1,111 @@ + + + 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 + + + + javax.persistence + persistence-api + + + 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} + + + ojdbc + ojdbc6 + true + + + + javax.servlet + javax.servlet-api + provided + true + + + com.alibaba + druid-spring-boot-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..8cd7973 --- /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,181 @@ +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 (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..cc989d4 --- /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 org.springframework.web.filter.OncePerRequestFilter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +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..cbe4f2b --- /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,29 @@ +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; + +} 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..25abd07 --- /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,67 @@ +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.boot.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.support.http.ResourceServlet; +import com.alibaba.druid.support.http.StatViewServlet; +import com.alibaba.druid.support.http.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..aa46a1c --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.mybatis.init.DataSourceAutoConfiguration +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/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.factories b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..b545551 --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +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..e6deef2 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/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-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 + + + javax.servlet + javax.servlet-api + compile + + + one.util + streamex + + + org.jooq + jool + + + 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..d6f0949 --- /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 TENANT_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..e46a74d --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/pom.xml @@ -0,0 +1,111 @@ + + + 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 + + + cn.hutool + hutool-all + + + 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 + + + javax.servlet + javax.servlet-api + provided + true + + + + 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..b4c0a3a --- /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.ServletUtil; +import cn.lingniu.framework.plugin.util.json.JsonUtils; +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 javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +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); + ServletUtil.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 ServletUtil.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 ServletUtil.getBody(request); + } + return null; + } + + public static byte[] getBodyBytes(HttpServletRequest request) { + // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取 + if (isJsonRequest(request)) { + return ServletUtil.getBodyBytes(request); + } + return null; + } + + public static String getClientIP(HttpServletRequest request) { + return ServletUtil.getClientIP(request); + } + + public static Map getParamMap(HttpServletRequest request) { + return ServletUtil.getParamMap(request); + } + + public static Map getHeaderMap(HttpServletRequest request) { + return ServletUtil.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..1752f4f --- /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 okhttp3.ConnectionPool; +import okhttp3.EventListener; +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.commons.httpclient.OkHttpClientConnectionPoolFactory; +import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; +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 javax.annotation.PreDestroy; +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, + OkHttpClientConnectionPoolFactory connectionPoolFactory) { + return connectionPoolFactory.create(mircroServiceConfig.getMaxTotal(), mircroServiceConfig.getOkHttp().getTimeToLive(), mircroServiceConfig.getOkHttp().getTimeToLiveUnit()); + } + + @Bean + public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, Optional> okFeignInterceptors, + ConnectionPool connectionPool, MircroServiceConfig mircroServiceConfig) { + okhttp3.OkHttpClient.Builder builder = httpClientFactory.createBuilder(mircroServiceConfig.getOkHttp().getDisableSslValidation()). + 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..d368d9d --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.microservice.common.init.LingniuCloudAutoConfiguration +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-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/META-INF/spring.factories b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..e69de29 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..786ebdd --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/pom.xml @@ -0,0 +1,64 @@ + + + 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 + + + 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 + + + javax.servlet + javax.servlet-api + compile + + + + 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..5093e85 --- /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,41 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * 基础类 + **/ +@Slf4j +public abstract class BasePrometheusAspect { + + /** + * SpelExpressionParser 获取参数值 + */ + protected T getValue(EvaluationContext context, String key, Class clazz) { + SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); + Expression expression = spelExpressionParser.parseExpression(key); + return expression.getValue(context, clazz); + } + + /** + * 获取参数容器 + */ + protected EvaluationContext getContext(Object[] arguments, Method signatureMethod) { + String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(signatureMethod); + if (parameterNames == null) { + throw new IllegalArgumentException("parameterNames is null"); + } + EvaluationContext evaluationContext = new StandardEvaluationContext(); + for (int i = 0; i < arguments.length; i++) { + evaluationContext.setVariable(parameterNames[i], arguments[i]); + } + return evaluationContext; + } +} 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..914b9bd --- /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,75 @@ +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.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 { + + @Autowired + PrometheusService prometheusService; + + @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(); + Method signatureMethod = signature.getMethod(); + String countName = ObjectEmptyUtils.isEmpty(prometheusCounter.name()) ? signatureMethod.getName() : prometheusCounter.name(); + Object val = null; + try { + val = point.proceed(); + if (ObjectEmptyUtils.isNotEmpty(prometheusCounter.parameterKeys())) { + prometheusService.count(countName, () -> buildTags(point, prometheusCounter, "SUCCESS")); + } + } catch (Exception ex) { + if (ObjectEmptyUtils.isNotEmpty(prometheusCounter.parameterKeys())) { + prometheusService.count(countName, () -> buildTags(point, prometheusCounter, "FAILED")); + } + throw ex; + } + return val; + } + + private List buildTags(ProceedingJoinPoint point, PrometheusCounter prometheusCounter, String responseStatus) { + EvaluationContext context = getContext(point.getArgs(), ((MethodSignature) point.getSignature()).getMethod()); + List tags = new ArrayList<>(); + + if (ObjectEmptyUtils.isEmpty(prometheusCounter.labels()) || prometheusCounter.labels().length != prometheusCounter.parameterKeys().length) { + // 使用parameterKeys作为标签名 + for (String parameterKey : prometheusCounter.parameterKeys()) { + if (!ObjectEmptyUtils.isEmpty(parameterKey)) { + String cleanKey = parameterKey.replace("#", ""); + tags.add(Tag.of(cleanKey, getValue(context, parameterKey, String.class))); + } + } + } else { + // 使用labels作为标签名 + for (int i = 0; i < prometheusCounter.labels().length; i++) { + tags.add(Tag.of(prometheusCounter.labels()[i], getValue(context, prometheusCounter.parameterKeys()[i], String.class))); + } + } + tags.add(Tag.of("response", responseStatus)); + return tags; + } + + +} 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..1409f31 --- /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,73 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import cn.lingniu.framework.plugin.prometheus.PrometheusMetrics; +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; +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.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import javax.servlet.http.HttpServletRequest; + +@Aspect +@Component +public class PrometheusMetricsAspect { + + private static final Counter requestTotal = Counter.build().name("api_request_total").labelNames("api").help + ("total request counter of api").register(); + private static final Counter requestError = Counter.build().name("api_request_error_total").labelNames("api").help + ("response error counter of api").register(); + private static final Histogram histogram = Histogram.build().name("api_response_duration_seconds").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 { + String name; + if (StringUtils.isNotEmpty(prometheusMetrics.name())) { + name = prometheusMetrics.name(); + } else { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (requestAttributes == null) { // 如果没有请求上下文,使用方法名作为指标名 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + name = methodSignature.getMethod().getName(); + } else {// 对URI进行安全过滤,防止路径注入 + HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); + String requestURI = request.getRequestURI(); + name = sanitizeUri(requestURI); + } + } + requestTotal.labels(name).inc(); + Histogram.Timer requestTimer = histogram.labels(name).startTimer(); + Object object; + try { + object = joinPoint.proceed(); + } catch (Throwable t) { + requestError.labels(name).inc(); + throw t; + } finally { + requestTimer.observeDuration(); + } + return object; + } + + /** + * 移除可能的危险字符,只保留字母、数字、斜杠、点、连字符和下划线 + */ + private String sanitizeUri(String uri) { + if (uri == null) { + return "unknown"; + } + return uri.replaceAll("[^a-zA-Z0-9/._-]", "_"); + } + +} \ 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/PrometheusService.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusService.java new file mode 100644 index 0000000..51283ea --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusService.java @@ -0,0 +1,56 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import io.micrometer.core.instrument.Tag; + +import java.util.List; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; + +/** + * PrometheusService 接口 + **/ +public interface PrometheusService { + + /** + * 摘要统计指标 + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + * @param amt 统计值,不能为NaN或无穷大 + */ + void summary(String metricName, Supplier> labelsProvider, double amt); + + /** + * 计数指标 + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + */ + void count(String metricName, Supplier> labelsProvider); + + /** + * 观察值指标 + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + * @param amt 观察值,不能为空且必须为有效数值 + * @param 数值类型,必须继承自Number + */ + void gauge(String metricName, Supplier> labelsProvider, T amt); + + /** + * 观察值指标(带转换函数) + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + * @param amt 观察值,不能为空 + * @param valueFunction 值转换函数,不能为空 + * @param 数值类型,必须继承自Number + */ + void gauge(String metricName, Supplier> labelsProvider, T amt, ToDoubleFunction valueFunction); + + /** + * 计时统计 + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + * @param amt 时间值,不能为null且不能为NaN或无穷大 + */ + void timer(String metricName, Supplier> labelsProvider, Double amt); + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusServiceImpl.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusServiceImpl.java new file mode 100644 index 0000000..81f3d61 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusServiceImpl.java @@ -0,0 +1,111 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; + + +@Slf4j +@AllArgsConstructor +public class PrometheusServiceImpl implements PrometheusService { + + /** + * @param metricName + * @param lablesProvider + */ + @Override + public void count(String metricName, Supplier> lablesProvider) { + if (metricName == null || lablesProvider == null) { + throw new IllegalArgumentException("metricName and lablesProvider cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + Metrics.counter(metricName, labels).increment(); + } catch (Exception e) { + throw e; + } + } + + @Override + public void gauge(String metricName, Supplier> lablesProvider, T amt) { + if (metricName == null || lablesProvider == null || amt == null) { + throw new IllegalArgumentException("metricName, lablesProvider, and amt cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + Metrics.gauge(metricName, labels, amt, (l) -> l.doubleValue()); + } catch (Exception e) { + throw e; + } + } + + @Override + public void gauge(String metricName, Supplier> lablesProvider, T amt, ToDoubleFunction valueFunction) { + if (metricName == null || lablesProvider == null || amt == null || valueFunction == null) { + throw new IllegalArgumentException("metricName, lablesProvider, amt, and valueFunction cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + Metrics.gauge(metricName, labels, amt, valueFunction); + } catch (Exception e) { + throw e; + } + } + + + @Override + public void timer(String metricName, Supplier> lablesProvider, Double amt) { + if (metricName == null || lablesProvider == null || amt == null) { + throw new IllegalArgumentException("metricName, lablesProvider, and amt cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + long duration = amt.longValue(); + if (duration < 0) { + throw new IllegalArgumentException("Timer duration cannot be negative: " + amt); + } + Metrics.timer(metricName, labels).record(duration, TimeUnit.MICROSECONDS); + } catch (Exception e) { + throw e; + } + } + + /** + * @param metricName + * @param lablesProvider + */ + @Override + public void summary(String metricName, Supplier> lablesProvider, double amt) { + if (metricName == null || lablesProvider == null) { + throw new IllegalArgumentException("metricName and lablesProvider cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + Metrics.summary(metricName, labels).record(amt); + } catch (Exception e) { + throw e; + } + } + + +} 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..3974475 --- /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,98 @@ +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.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 { + + @Autowired + PrometheusService prometheusService; + + @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(); + String summaryName = ObjectEmptyUtils.isEmpty(prometheusSummary.name()) ? signatureMethod.getName() : prometheusSummary.name(); + + if (ObjectEmptyUtils.isEmpty(prometheusSummary.parameterKeys())) { + return point.proceed(); + } + + long startTime = System.currentTimeMillis(); + boolean success = false; + Object result = null; + + try { + result = point.proceed(); + success = true; + return result; + } finally { + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + List tags = buildTags(point, signatureMethod, prometheusSummary, success ? "SUCCESS" : "FAILED"); + prometheusService.summary(summaryName, () -> tags, duration); + } + } + + private List buildTags(ProceedingJoinPoint point, Method method, PrometheusSummary prometheusSummary, String responseStatus) { + EvaluationContext context = getContext(point.getArgs(), method); + List tags = new ArrayList<>(); + + String[] parameterKeys = prometheusSummary.parameterKeys(); + String[] labels = prometheusSummary.labels(); + + // 构建标签,优先使用labels数组,否则使用parameterKeys作为标签名 + if (ObjectEmptyUtils.isEmpty(labels) || labels.length != parameterKeys.length) { + for (String paramKey : parameterKeys) { + if (!ObjectEmptyUtils.isEmpty(paramKey)) { + String cleanKey = sanitizeTagKey(paramKey.replace("#", "")); + String value = getValue(context, paramKey, String.class); + tags.add(Tag.of(cleanKey, value)); + } + } + } else { + for (int i = 0; i < labels.length; i++) { + String cleanLabel = sanitizeTagKey(labels[i]); + String value = getValue(context, parameterKeys[i], String.class); + tags.add(Tag.of(cleanLabel, value)); + } + } + tags.add(Tag.of("response", responseStatus)); + return tags; + } + + private String sanitizeTagKey(String key) { + // 防止标签键名中的特殊字符造成问题 + if (key == null) { + return "unknown"; + } + // 只保留字母数字下划线连字符,并限制长度 + return key.replaceAll("[^a-zA-Z0-9_-]", "_").substring(0, Math.min(key.length(), 64)); + } + + + +} 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..886bcdc --- /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,81 @@ +package cn.lingniu.framework.plugin.prometheus.init; + + +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.prometheus.aspect.PrometheusService; +import cn.lingniu.framework.plugin.prometheus.aspect.PrometheusServiceImpl; +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import io.micrometer.core.instrument.MeterRegistry; +import io.prometheus.client.CollectorRegistry; +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; + + +@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 + CollectorRegistry metricRegistry() { + return CollectorRegistry.defaultRegistry; + } + + @Bean + @ConditionalOnMissingBean + public PrometheusService prometheusService() { + return new PrometheusServiceImpl(); + } + + +} 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..b3b3b64 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.prometheus.init.PrometheusConfiguration +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/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..08f5db3 --- /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.2.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..86e7c09 --- /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,105 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +import lombok.Data; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.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; + + 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..87ccd54 --- /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,315 @@ +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.common.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 + 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) { + 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; + } + 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) { + 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; + } + 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..4510127 --- /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,208 @@ +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()); + } 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..6047c26 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/pom.xml @@ -0,0 +1,101 @@ + + + 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 + + + + org.apache.httpcomponents + httpclient + + + 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 + + + javax.servlet + javax.servlet-api + provided + true + + + + 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..d471c6f --- /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,242 @@ +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 lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +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..e81adbb --- /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,100 @@ +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 lombok.extern.slf4j.Slf4j; +import org.springframework.util.StopWatch; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +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); + 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..8c61148 --- /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 org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 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..59f2fe7 --- /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,32 @@ +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 lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + + +/** + * API 访问日志 Service 实现类 todo 可以扩展 + */ +@Slf4j +@Service +@Validated +public class ApiAccessLogServiceImpl implements ApiAccessLogService { + + + @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)); + 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..c25fe7d --- /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,25 @@ +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 org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 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..c35871b --- /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,30 @@ +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; + +import javax.validation.Valid; + +/** + * API 访问日志的 API 接口 + */ +public interface ApiAccessLogCommonApi { + + /** + * 创建 API 访问日志 + * + * @param createDTO 创建信息 + */ + void createApiAccessLog(@Valid 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..0759e2f --- /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,29 @@ +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; + +import javax.validation.Valid; + +/** + * API 错误日志的 API 接口 + */ +public interface ApiErrorLogCommonApi { + + /** + * 创建 API 错误日志 + * + * @param createDTO 创建信息 + */ + void createApiErrorLog(@Valid 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..1c9f26a --- /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,89 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 访问日志---后续可以跟用户登陆信息互通 + */ +@Data +public class ApiAccessLogCreateReqDTO { + + /** + * 用户编号 + */ + private Long userId = 0L; + /** + * 用户类型 + */ + private Integer userType = 0; + + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + private String requestParams; + /** + * 响应结果 + */ + private String responseBody; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + /** + * 开始请求时间 + */ + @NotNull(message = "开始请求时间不能为空") + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + @NotNull(message = "结束请求时间不能为空") + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + /** + * 结果码 + */ + @NotNull(message = "错误码不能为空") + 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..c6ab3e2 --- /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,101 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 错误日志 + */ +@Data +public class ApiErrorLogCreateReqDTO { + + /** + * 账号编号 + */ + private Long userId = 0L; + /** + * 用户类型 + */ + private Integer userType = 0; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 异常时间 + */ + @NotNull(message = "异常时间不能为空") + private LocalDateTime exceptionTime; + /** + * 异常名 + */ + @NotNull(message = "异常名不能为空") + private String exceptionName; + /** + * 异常发生的类全名 + */ + @NotNull(message = "异常发生的类全名不能为空") + private String exceptionClassName; + /** + * 异常发生的类文件 + */ + @NotNull(message = "异常发生的类文件不能为空") + private String exceptionFileName; + /** + * 异常发生的方法名 + */ + @NotNull(message = "异常发生的方法名不能为空") + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + */ + @NotNull(message = "异常发生的方法所在行不能为空") + private Integer exceptionLineNumber; + /** + * 异常的栈轨迹异常的栈轨迹 + */ + @NotNull(message = "异常的栈轨迹不能为空") + private String exceptionStackTrace; + /** + * 异常导致的根消息 + */ + @NotNull(message = "异常导致的根消息不能为空") + private String exceptionRootCauseMessage; + /** + * 异常导致的消息 + */ + @NotNull(message = "异常导致的消息不能为空") + 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..d846815 --- /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 lombok.RequiredArgsConstructor; +import org.springframework.util.CollectionUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.http.HttpServletRequest; + +/** + * 可以过滤排除指定的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..9ff0a11 --- /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 org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +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..a7906b2 --- /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,75 @@ +package cn.lingniu.framework.plugin.web.bae.filter; + + +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.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..4a18093 --- /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 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 javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.ValidationException; +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..5b79ec6 --- /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,39 @@ +package cn.lingniu.framework.plugin.web.bae.interceptor; + +import cn.hutool.core.util.ClassUtil; +import cn.lingniu.framework.plugin.core.base.CommonResult; +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; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +@Slf4j +@Order(Ordered.LOWEST_PRECEDENCE + 1000) +public class ResponseCheckInterceptor extends HandlerInterceptorAdapter 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..b50bad0 --- /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 org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +/** + * 专属于 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..55dd661 --- /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,63 @@ +package cn.lingniu.framework.plugin.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 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..f37a613 --- /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,22 @@ +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; + + /** + * 排除的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..8f63174 --- /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,25 @@ +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(); +} 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..1c0bd78 --- /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,83 @@ +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 javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.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..013bd4e --- /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 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 javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +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..62eea00 --- /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 javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; +import javax.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..b1af019 --- /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 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; + +import javax.servlet.Filter; + +@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..c47b0a9 --- /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/**,/swagger-ui.html/**,/swagger-resources/**,/v2/api-docs,/actuator/**,,/favicon.ico" + .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..6d3742b --- /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,74 @@ +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 org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +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.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.client.RestTemplate; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import javax.servlet.Filter; + +@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/resources/META-INF/spring.factories b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..b95b522 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +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 + 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..6e439d9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + cn.lingniu.framework + lingniu-framework-parent + 1.0.0-SNAPSHOT + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + lingniu-framework-dependencies + lingniu-framework-plugin/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 + + + + + + releases + http://nexus.xx.net.cn:8081/repository/releases/ + + + snapshots + http://nexus.xx.net.cn:8081/repository/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