From 59534eb70ae9a001f000c71c4d8f56dea258bfac Mon Sep 17 00:00:00 2001 From: liaozan <378024053@qq.com> Date: Thu, 27 Apr 2023 00:19:05 +0800 Subject: [PATCH] Make modules all in one --- .gitignore | 4 +- commons/common-util/pom.xml | 108 +++++ .../common/CommonAutoConfiguration.java | 17 + .../schbrain/common/util/ApplicationName.java | 15 + .../schbrain/common/util/BeanCopyUtils.java | 63 +++ .../util/ConfigurationPropertiesUtils.java | 47 ++ .../com/schbrain/common/util/EnvUtils.java | 64 +++ .../com/schbrain/common/util/ExcelUtils.java | 130 ++++++ .../com/schbrain/common/util/IdWorker.java | 125 +++++ .../com/schbrain/common/util/InetUtils.java | 105 +++++ .../schbrain/common/util/JacksonUtils.java | 243 ++++++++++ .../com/schbrain/common/util/PageUtils.java | 73 +++ .../common/util/ParameterDiscoverUtils.java | 33 ++ .../com/schbrain/common/util/PortUtils.java | 49 ++ .../com/schbrain/common/util/SpelUtils.java | 39 ++ .../com/schbrain/common/util/StreamUtils.java | 160 +++++++ .../schbrain/common/util/TraceIdUtils.java | 56 +++ .../util/cipher/AES128ECBWithPKCS7.java | 58 +++ .../schbrain/common/util/log/LogEvent.java | 19 + .../common/util/log/LogEventAction.java | 31 ++ .../common/util/log/StatisticLogUtil.java | 17 + .../properties/SchbrainMapPropertySource.java | 24 + .../util/support/ConfigurableProperties.java | 63 +++ .../common/util/support/ValidateSupport.java | 63 +++ .../delay/AbstractDelayMessageListener.java | 73 +++ .../util/support/delay/DelayedQueueUtils.java | 103 +++++ .../support/excel/bean/ExcelReadResult.java | 28 ++ .../excel/exception/ExcelException.java | 21 + .../excel/listener/ExcelBeanReadListener.java | 37 ++ .../listener/ExcelMapDataReadListener.java | 11 + .../excel/listener/ExcelReadListenerBase.java | 84 ++++ .../HierarchicalDataReadListener.java | 91 ++++ .../util/support/jackson/JavaTimeModule.java | 36 ++ .../ObjectMapperModuleConfiguration.java | 29 ++ .../util/support/lock/RedisLockUtils.java | 81 ++++ .../MdcContextPropagationTaskDecorator.java | 25 + .../support/task/ThreadPoolConfiguration.java | 28 ++ .../UnCaughtExceptionHandleThreadFactory.java | 38 ++ .../common/util/support/trace/TraceParam.java | 50 ++ .../util/support/trace/TraceParamAspect.java | 51 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + commons/common/pom.xml | 32 ++ .../common/annotation/IgnoreLogin.java | 18 + .../common/constants/DateTimeFormatters.java | 33 ++ .../common/constants/LogConstants.java | 32 ++ .../common/constants/PageConstants.java | 30 ++ .../constants/ResponseActionConstants.java | 30 ++ .../constants/ResponseCodeConstants.java | 34 ++ .../com/schbrain/common/entity/PageParam.java | 23 + .../common/entity/PaginationInfo.java | 103 +++++ .../schbrain/common/enums/BooleanEnum.java | 34 ++ .../schbrain/common/enums/ValidateEnum.java | 25 + .../common/exception/BaseException.java | 41 ++ .../exception/ParamInvalidException.java | 18 + .../schbrain/common/util/ValidateUtils.java | 193 ++++++++ commons/module-tree/pom.xml | 24 + .../common/module/tree/StructureTreeNode.java | 22 + .../schbrain/common/module/tree/TreeNode.java | 45 ++ .../common/module/tree/TreeNodeProcessor.java | 421 +++++++++++++++++ .../common/module/tree/TreeQueryOption.java | 91 ++++ .../module/tree/constant/TreeConstant.java | 12 + .../common/module/tree/dao/TreeNodeDao.java | 375 +++++++++++++++ .../tree/event/EmptyTreeOperationAware.java | 22 + .../module/tree/event/TreeOperationAware.java | 22 + .../module/tree/event/TreeOperationEvent.java | 10 + commons/pom.xml | 24 + commons/web-common/pom.xml | 58 +++ .../web/WebCommonAutoConfiguration.java | 100 ++++ .../common/web/annotation/BodyParam.java | 41 ++ .../web/annotation/ResponseWrapOption.java | 25 + ...ParamArgumentResolverWebMvcConfigurer.java | 19 + .../BodyParamMethodArgumentResolver.java | 74 +++ .../DefaultGlobalExceptionHandler.java | 250 ++++++++++ .../ExceptionHandlerWebMcvConfigurer.java | 58 +++ .../web/exception/GlobalExceptionHandler.java | 11 + .../exception/GlobalExceptionResolver.java | 139 ++++++ .../common/web/log/RequestLoggingFilter.java | 109 +++++ .../common/web/properties/WebProperties.java | 49 ++ .../web/result/ResponseBodyHandler.java | 81 ++++ .../common/web/result/ResponseDTO.java | 59 +++ .../web/servlet/AllowAllCorsConfigurer.java | 26 ++ ...cterEncodingServletContextInitializer.java | 25 + .../TraceIdInitializeServletListener.java | 28 ++ .../web/support/BaseHandlerInterceptor.java | 65 +++ .../authentication/AbstractAuthenticator.java | 65 +++ .../AuthenticationInterceptor.java | 58 +++ .../support/authentication/Authenticator.java | 24 + ...RateLimitCacheKeyVariablesContributor.java | 20 + .../support/concurrent/RateLimitAspect.java | 119 +++++ ...RateLimitCacheKeyVariablesContributor.java | 16 + .../web/support/concurrent/RateLimiter.java | 44 ++ .../schbrain/common/web/utils/ExcelUtils.java | 51 ++ .../utils/HandlerMethodAnnotationUtils.java | 32 ++ .../common/web/utils/ServletUtils.java | 38 ++ .../spring-configuration-metadata.json | 61 +++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../integration-jenkins-plugin/LICENSE | 201 ++++++++ .../integration-jenkins-plugin/README.md | 26 ++ .../integration-jenkins-plugin/pom.xml | 84 ++++ .../action/ViewBuildScriptAction.java | 61 +++ .../integration/builder/BuilderContext.java | 135 ++++++ .../integration/builder/FileManager.java | 44 ++ .../builder/IntegrationBuilder.java | 280 +++++++++++ .../builder/config/BuildConfig.java | 37 ++ .../builder/config/DeployToK8sConfig.java | 101 ++++ .../builder/config/DockerConfig.java | 182 ++++++++ .../builder/config/MavenConfig.java | 57 +++ .../config/deploy/DeployStyleRadio.java | 31 ++ .../deploy/DeployTemplateComponent.java | 99 ++++ .../deploy/PointDeployFileComponent.java | 41 ++ .../deploy/service/ServiceDeployConfig.java | 87 ++++ .../builder/constants/Constants.java | 56 +++ .../builder/env/BuildEnvContributor.java | 49 ++ .../integration/builder/util/FileUtils.java | 125 +++++ .../integration/builder/util/Logger.java | 46 ++ .../builder/util/TemplateUtils.java | 45 ++ .../action/ViewBuildScriptAction/index.jelly | 10 + .../builder/IntegrationBuilder/config.jelly | 13 + .../config/DeployToK8sConfig/config.jelly | 37 ++ .../builder/config/DockerConfig/config.jelly | 54 +++ .../builder/config/MavenConfig/config.jelly | 16 + .../DeployTemplateComponent/config.jelly | 32 ++ .../PointDeployFileComponent/config.jelly | 12 + .../src/main/resources/index.jelly | 7 + .../integration-maven-plugin/README.md | 59 +++ integration/integration-maven-plugin/pom.xml | 57 +++ .../maven/plugin/mojo/PrepareMojo.java | 183 ++++++++ integration/pom.xml | 22 + pom.xml | 9 + starters/.gitignore | 38 ++ starters/apollo-spring-boot-starter/pom.xml | 27 ++ .../SchbrainApolloAutoConfiguration.java | 24 + ...lloPropertiesEnvironmentPostProcessor.java | 22 + .../SchbrainPropertySourcesProcessor.java | 21 + .../apollo/properties/ApolloProperties.java | 28 ++ .../properties/ApolloPropertiesPreparer.java | 136 ++++++ ...nfigurablePropertiesBeanPostProcessor.java | 23 + .../ConfigurationPropertiesRegistry.java | 69 +++ .../apollo/util/ConfigUtils.java | 133 ++++++ .../apollo/util/PropertySourceOrderUtils.java | 53 +++ .../main/resources/META-INF/app.properties | 5 + .../spring-configuration-metadata.json | 18 + .../main/resources/META-INF/spring.factories | 3 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + starters/cache-spring-boot-starter/pom.xml | 27 ++ .../autoconfigure/cache/CacheUtils.java | 164 +++++++ .../cache/SchbrainCacheAutoConfiguration.java | 35 ++ .../cache/exception/CacheException.java | 15 + .../cache/properties/CacheProperties.java | 27 ++ .../cache/provider/CacheProvider.java | 62 +++ .../cache/provider/CacheProviderDelegate.java | 156 +++++++ .../provider/redis/RedisCacheProvider.java | 149 ++++++ .../SchbrainRedisCacheConfiguration.java | 26 ++ .../spring-configuration-metadata.json | 32 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + starters/dubbo-spring-boot-starter/pom.xml | 83 ++++ .../dubbo/DubboAutoConfiguration.java | 19 + .../dubbo/filter/DubboExceptionFilter.java | 49 ++ .../dubbo/filter/TraceConsumerRpcFilter.java | 22 + .../dubbo/filter/TraceProviderRpcFilter.java | 22 + .../dubbo/properties/DubboProperties.java | 47 ++ .../properties/DubboPropertiesPreparer.java | 74 +++ .../dubbo/org.apache.dubbo.rpc.Filter | 3 + .../spring-configuration-metadata.json | 32 ++ .../main/resources/META-INF/spring.factories | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + starters/logger-spring-boot-starter/pom.xml | 35 ++ .../logger/JsonLoggerInitializer.java | 155 +++++++ .../logger/LoggerAutoConfiguration.java | 21 + .../apollo/DynamicLoggerConfiguration.java | 77 +++ .../LoggingConfigFileChangeListener.java | 57 +++ .../listener/LoggingLevelChangeListener.java | 75 +++ .../logback/LogbackTraceIdConfiguration.java | 20 + .../logback/TraceIdInitializeTurboFilter.java | 23 + .../EventDateStringValueJsonProvider.java | 36 ++ .../logstash/SchbrainLogstashEncoder.java | 18 + .../logstash/SchbrainLogstashFormatter.java | 66 +++ .../properties/LoggerPropertiesPreparer.java | 96 ++++ .../properties/LoggingFileProperties.java | 33 ++ .../LoggingNamespaceProperties.java | 21 + .../logger/util/LoggerUtils.java | 59 +++ .../spring-configuration-metadata.json | 94 ++++ .../main/resources/META-INF/spring.factories | 4 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + starters/mybatis-spring-boot-starter/pom.xml | 47 ++ .../mybatis/MybatisAutoConfiguration.java | 99 ++++ .../mybatis/annotation/BizId.java | 28 ++ .../mybatis/base/BaseEntity.java | 34 ++ .../base/BaseEntityWithLogicDelete.java | 32 ++ .../mybatis/base/BaseMapper.java | 34 ++ .../mybatis/base/BaseService.java | 73 +++ .../mybatis/base/BaseServiceImpl.java | 147 ++++++ .../mybatis/biz/BizIdGenerator.java | 15 + .../mybatis/biz/BizIdHelper.java | 48 ++ .../mybatis/biz/BizIdInjectInterceptor.java | 45 ++ .../autoconfigure/mybatis/biz/BizIdType.java | 35 ++ .../MybatisConfigurationCustomizer.java | 37 ++ .../MybatisPlusGlobalConfigCustomizer.java | 43 ++ .../mybatis/constant/MybatisConstants.java | 42 ++ .../mybatis/constraint/ColumnMeta.java | 37 ++ .../constraint/ColumnMetaRowMapper.java | 30 ++ .../DefaultTableConstraintChecker.java | 126 +++++ .../DefaultTableMetaDataLoader.java | 36 ++ .../constraint/IgnoreConstraintCheck.java | 14 + .../mybatis/constraint/Table.java | 71 +++ .../TableConstraintCheckFailureAnalyzer.java | 20 + .../constraint/TableConstraintChecker.java | 19 + .../constraint/TableMetaDataLoader.java | 17 + .../mybatis/core/BizIdColumnField.java | 55 +++ .../core/LogicDeleteSupportSqlSource.java | 36 ++ .../core/MybatisXmlLanguageDriver.java | 19 + .../DataSourceConnectionPostProcessor.java | 42 ++ .../customizer/DataSourceCustomizer.java | 16 + .../DefaultDataSourceCustomizer.java | 37 ++ .../DataSourcePropertiesExtractor.java | 18 + .../DataSourcePropertiesExtractorSupport.java | 31 ++ .../DruidDataSourcePropertiesExtractor.java | 29 ++ .../HikariDataSourcePropertiesExtractor.java | 27 ++ .../exception/NoSuchRecordException.java | 19 + .../exception/TableConstraintException.java | 31 ++ .../listener/TableConstraintCheckerBean.java | 173 +++++++ .../DataSourceConnectionProperties.java | 67 +++ .../mybatis/properties/MybatisProperties.java | 39 ++ .../injector/DefaultMethodSqlInjector.java | 59 +++ .../mybatis/sql/method/Delete.java | 20 + .../mybatis/sql/method/DeleteBatchByIds.java | 20 + .../mybatis/sql/method/DeleteById.java | 57 +++ .../mybatis/sql/method/DeleteByMap.java | 20 + .../type/InstantToLongTypeHandler.java | 38 ++ .../autoconfigure/mybatis/util/SqlUtils.java | 21 + .../spring-configuration-metadata.json | 113 +++++ .../main/resources/META-INF/spring.factories | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + starters/oss-spring-boot-starter/pom.xml | 27 ++ .../oss/OssAutoConfiguration.java | 29 ++ .../autoconfigure/oss/bean/CopyResult.java | 41 ++ .../autoconfigure/oss/bean/DeleteResult.java | 37 ++ .../oss/bean/DownloadResult.java | 37 ++ .../oss/bean/OssOperationResult.java | 25 + .../oss/bean/UploadCredentials.java | 47 ++ .../autoconfigure/oss/bean/UploadResult.java | 36 ++ .../oss/exception/OssException.java | 21 + .../oss/properties/OssProperties.java | 55 +++ .../autoconfigure/oss/util/OssUtils.java | 390 ++++++++++++++++ .../spring-configuration-metadata.json | 84 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + starters/pom.xml | 28 ++ starters/xxl-job-spring-boot-starter/pom.xml | 27 ++ .../xxl/SchbrainXxlJobExecutor.java | 83 ++++ .../xxl/XxlJobAutoConfiguration.java | 44 ++ .../XxlJobShouldAvailableCondition.java | 29 ++ .../xxl/handler/BaseJobHandler.java | 36 ++ .../xxl/properties/XxlJobProperties.java | 35 ++ .../spring-configuration-metadata.json | 60 +++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + support/pom.xml | 22 + support/schbrain-base-dao/pom.xml | 35 ++ .../com/schbrain/framework/dao/BaseDao.java | 76 +++ .../mybatis/BaseMethodInvocationHandler.java | 65 +++ .../mybatis/CustomizeMapperFactoryBean.java | 52 +++ .../dao/mybatis/annotation/MapperConfig.java | 19 + .../exception/MapperParseException.java | 20 + .../dao/mybatis/mapper/BaseMapper.java | 156 +++++++ .../mybatis/mapper/BaseMapperStatement.java | 437 ++++++++++++++++++ .../mapper/sqlsource/AbstractSqlSource.java | 79 ++++ .../sqlsource/CountByConditionSqlSource.java | 25 + .../sqlsource/DeleteByConditionSqlSource.java | 25 + .../sqlsource/ListByConditionSqlSource.java | 25 + .../sqlsource/UpdateByCompleteSqlSource.java | 33 ++ .../sqlsource/UpdateByConditionSqlSource.java | 46 ++ .../schbrain/framework/dao/util/SQLUtil.java | 49 ++ support/schbrain-spring-support/pom.xml | 36 ++ .../spring/BeanPostProcessorAdapter.java | 113 +++++ .../BootstrapContextListenerComposite.java | 48 ++ .../EnvironmentPostProcessorAdapter.java | 42 ++ ...ultPropertiesEnvironmentPostProcessor.java | 99 ++++ ...ElasticsearchFeatureAutoConfiguration.java | 61 +++ .../main/resources/META-INF/spring.factories | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + 279 files changed, 15545 insertions(+), 1 deletion(-) create mode 100644 commons/common-util/pom.xml create mode 100644 commons/common-util/src/main/java/com/schbrain/common/CommonAutoConfiguration.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/ApplicationName.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/BeanCopyUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/ConfigurationPropertiesUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/EnvUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/ExcelUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/IdWorker.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/InetUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/JacksonUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/PageUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/ParameterDiscoverUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/PortUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/SpelUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/StreamUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/TraceIdUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/cipher/AES128ECBWithPKCS7.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/log/LogEvent.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/log/LogEventAction.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/log/StatisticLogUtil.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/properties/SchbrainMapPropertySource.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/ConfigurableProperties.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/ValidateSupport.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/delay/AbstractDelayMessageListener.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/delay/DelayedQueueUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/excel/bean/ExcelReadResult.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/excel/exception/ExcelException.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelBeanReadListener.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelMapDataReadListener.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelReadListenerBase.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/HierarchicalDataReadListener.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/jackson/JavaTimeModule.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/jackson/ObjectMapperModuleConfiguration.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/lock/RedisLockUtils.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/task/MdcContextPropagationTaskDecorator.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/task/ThreadPoolConfiguration.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/task/UnCaughtExceptionHandleThreadFactory.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/trace/TraceParam.java create mode 100644 commons/common-util/src/main/java/com/schbrain/common/util/support/trace/TraceParamAspect.java create mode 100644 commons/common-util/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 commons/common/pom.xml create mode 100644 commons/common/src/main/java/com/schbrain/common/annotation/IgnoreLogin.java create mode 100644 commons/common/src/main/java/com/schbrain/common/constants/DateTimeFormatters.java create mode 100644 commons/common/src/main/java/com/schbrain/common/constants/LogConstants.java create mode 100644 commons/common/src/main/java/com/schbrain/common/constants/PageConstants.java create mode 100644 commons/common/src/main/java/com/schbrain/common/constants/ResponseActionConstants.java create mode 100644 commons/common/src/main/java/com/schbrain/common/constants/ResponseCodeConstants.java create mode 100644 commons/common/src/main/java/com/schbrain/common/entity/PageParam.java create mode 100644 commons/common/src/main/java/com/schbrain/common/entity/PaginationInfo.java create mode 100644 commons/common/src/main/java/com/schbrain/common/enums/BooleanEnum.java create mode 100644 commons/common/src/main/java/com/schbrain/common/enums/ValidateEnum.java create mode 100644 commons/common/src/main/java/com/schbrain/common/exception/BaseException.java create mode 100644 commons/common/src/main/java/com/schbrain/common/exception/ParamInvalidException.java create mode 100644 commons/common/src/main/java/com/schbrain/common/util/ValidateUtils.java create mode 100644 commons/module-tree/pom.xml create mode 100644 commons/module-tree/src/main/java/com/schbrain/common/module/tree/StructureTreeNode.java create mode 100644 commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeNode.java create mode 100644 commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeNodeProcessor.java create mode 100644 commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeQueryOption.java create mode 100644 commons/module-tree/src/main/java/com/schbrain/common/module/tree/constant/TreeConstant.java create mode 100644 commons/module-tree/src/main/java/com/schbrain/common/module/tree/dao/TreeNodeDao.java create mode 100644 commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/EmptyTreeOperationAware.java create mode 100644 commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/TreeOperationAware.java create mode 100644 commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/TreeOperationEvent.java create mode 100644 commons/pom.xml create mode 100644 commons/web-common/pom.xml create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/WebCommonAutoConfiguration.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/annotation/BodyParam.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/annotation/ResponseWrapOption.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/argument/BodyParamArgumentResolverWebMvcConfigurer.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/argument/BodyParamMethodArgumentResolver.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/exception/DefaultGlobalExceptionHandler.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/exception/ExceptionHandlerWebMcvConfigurer.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/exception/GlobalExceptionHandler.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/exception/GlobalExceptionResolver.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/log/RequestLoggingFilter.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/properties/WebProperties.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/result/ResponseBodyHandler.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/result/ResponseDTO.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/servlet/AllowAllCorsConfigurer.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/servlet/CharacterEncodingServletContextInitializer.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/servlet/TraceIdInitializeServletListener.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/support/BaseHandlerInterceptor.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/AbstractAuthenticator.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/AuthenticationInterceptor.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/Authenticator.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitAspect.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitCacheKeyVariablesContributor.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimiter.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/utils/ExcelUtils.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/utils/HandlerMethodAnnotationUtils.java create mode 100644 commons/web-common/src/main/java/com/schbrain/common/web/utils/ServletUtils.java create mode 100644 commons/web-common/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 commons/web-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 integration/integration-jenkins-plugin/LICENSE create mode 100644 integration/integration-jenkins-plugin/README.md create mode 100644 integration/integration-jenkins-plugin/pom.xml create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/action/ViewBuildScriptAction.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/BuilderContext.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/FileManager.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/IntegrationBuilder.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/BuildConfig.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/DeployToK8sConfig.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/DockerConfig.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/MavenConfig.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployStyleRadio.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployTemplateComponent.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/PointDeployFileComponent.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/service/ServiceDeployConfig.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/constants/Constants.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/env/BuildEnvContributor.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/FileUtils.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/Logger.java create mode 100644 integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/TemplateUtils.java create mode 100644 integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/action/ViewBuildScriptAction/index.jelly create mode 100644 integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/IntegrationBuilder/config.jelly create mode 100644 integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/DeployToK8sConfig/config.jelly create mode 100644 integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/DockerConfig/config.jelly create mode 100644 integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/MavenConfig/config.jelly create mode 100644 integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployTemplateComponent/config.jelly create mode 100644 integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/PointDeployFileComponent/config.jelly create mode 100644 integration/integration-jenkins-plugin/src/main/resources/index.jelly create mode 100644 integration/integration-maven-plugin/README.md create mode 100644 integration/integration-maven-plugin/pom.xml create mode 100644 integration/integration-maven-plugin/src/main/java/com/schbrain/maven/plugin/mojo/PrepareMojo.java create mode 100644 integration/pom.xml create mode 100644 starters/.gitignore create mode 100644 starters/apollo-spring-boot-starter/pom.xml create mode 100644 starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainApolloAutoConfiguration.java create mode 100644 starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainApolloPropertiesEnvironmentPostProcessor.java create mode 100644 starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainPropertySourcesProcessor.java create mode 100644 starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ApolloProperties.java create mode 100644 starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ApolloPropertiesPreparer.java create mode 100644 starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ConfigurablePropertiesBeanPostProcessor.java create mode 100644 starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ConfigurationPropertiesRegistry.java create mode 100644 starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/util/ConfigUtils.java create mode 100644 starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/util/PropertySourceOrderUtils.java create mode 100644 starters/apollo-spring-boot-starter/src/main/resources/META-INF/app.properties create mode 100644 starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring.factories create mode 100644 starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 starters/cache-spring-boot-starter/pom.xml create mode 100644 starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/CacheUtils.java create mode 100644 starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/SchbrainCacheAutoConfiguration.java create mode 100644 starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/exception/CacheException.java create mode 100644 starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/properties/CacheProperties.java create mode 100644 starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/CacheProvider.java create mode 100644 starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/CacheProviderDelegate.java create mode 100644 starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/redis/RedisCacheProvider.java create mode 100644 starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/redis/SchbrainRedisCacheConfiguration.java create mode 100644 starters/cache-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 starters/cache-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 starters/dubbo-spring-boot-starter/pom.xml create mode 100644 starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/DubboAutoConfiguration.java create mode 100644 starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/DubboExceptionFilter.java create mode 100644 starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/TraceConsumerRpcFilter.java create mode 100644 starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/TraceProviderRpcFilter.java create mode 100644 starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/properties/DubboProperties.java create mode 100644 starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/properties/DubboPropertiesPreparer.java create mode 100644 starters/dubbo-spring-boot-starter/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter create mode 100644 starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring.factories create mode 100644 starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 starters/logger-spring-boot-starter/pom.xml create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/JsonLoggerInitializer.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/LoggerAutoConfiguration.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/DynamicLoggerConfiguration.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/listener/LoggingConfigFileChangeListener.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/listener/LoggingLevelChangeListener.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logback/LogbackTraceIdConfiguration.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logback/TraceIdInitializeTurboFilter.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/EventDateStringValueJsonProvider.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/SchbrainLogstashEncoder.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/SchbrainLogstashFormatter.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggerPropertiesPreparer.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggingFileProperties.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggingNamespaceProperties.java create mode 100644 starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/util/LoggerUtils.java create mode 100644 starters/logger-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 starters/logger-spring-boot-starter/src/main/resources/META-INF/spring.factories create mode 100644 starters/logger-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 starters/mybatis-spring-boot-starter/pom.xml create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/MybatisAutoConfiguration.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/annotation/BizId.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseEntity.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseEntityWithLogicDelete.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseMapper.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseService.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseServiceImpl.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdGenerator.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdHelper.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdInjectInterceptor.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdType.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/configuration/MybatisConfigurationCustomizer.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/configuration/MybatisPlusGlobalConfigCustomizer.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constant/MybatisConstants.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/ColumnMeta.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/ColumnMetaRowMapper.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/DefaultTableConstraintChecker.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/DefaultTableMetaDataLoader.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/IgnoreConstraintCheck.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/Table.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableConstraintCheckFailureAnalyzer.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableConstraintChecker.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableMetaDataLoader.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/BizIdColumnField.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/LogicDeleteSupportSqlSource.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/MybatisXmlLanguageDriver.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/DataSourceConnectionPostProcessor.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/customizer/DataSourceCustomizer.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/customizer/DefaultDataSourceCustomizer.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DataSourcePropertiesExtractor.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DataSourcePropertiesExtractorSupport.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DruidDataSourcePropertiesExtractor.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/HikariDataSourcePropertiesExtractor.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/exception/NoSuchRecordException.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/exception/TableConstraintException.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/listener/TableConstraintCheckerBean.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/properties/DataSourceConnectionProperties.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/properties/MybatisProperties.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/injector/DefaultMethodSqlInjector.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/Delete.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteBatchByIds.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteById.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteByMap.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/type/InstantToLongTypeHandler.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/util/SqlUtils.java create mode 100644 starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring.factories create mode 100644 starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 starters/oss-spring-boot-starter/pom.xml create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/OssAutoConfiguration.java create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/CopyResult.java create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/DeleteResult.java create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/DownloadResult.java create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/OssOperationResult.java create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/UploadCredentials.java create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/UploadResult.java create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/exception/OssException.java create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/properties/OssProperties.java create mode 100644 starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/util/OssUtils.java create mode 100644 starters/oss-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 starters/oss-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 starters/pom.xml create mode 100644 starters/xxl-job-spring-boot-starter/pom.xml create mode 100644 starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/SchbrainXxlJobExecutor.java create mode 100644 starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/XxlJobAutoConfiguration.java create mode 100644 starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/condition/XxlJobShouldAvailableCondition.java create mode 100644 starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/handler/BaseJobHandler.java create mode 100644 starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/properties/XxlJobProperties.java create mode 100644 starters/xxl-job-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 starters/xxl-job-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 support/pom.xml create mode 100644 support/schbrain-base-dao/pom.xml create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/BaseDao.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/BaseMethodInvocationHandler.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/CustomizeMapperFactoryBean.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/annotation/MapperConfig.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/exception/MapperParseException.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/BaseMapper.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/BaseMapperStatement.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/sqlsource/AbstractSqlSource.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/sqlsource/CountByConditionSqlSource.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/sqlsource/DeleteByConditionSqlSource.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/sqlsource/ListByConditionSqlSource.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/sqlsource/UpdateByCompleteSqlSource.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/sqlsource/UpdateByConditionSqlSource.java create mode 100644 support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/util/SQLUtil.java create mode 100644 support/schbrain-spring-support/pom.xml create mode 100644 support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/BeanPostProcessorAdapter.java create mode 100644 support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/BootstrapContextListenerComposite.java create mode 100644 support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/EnvironmentPostProcessorAdapter.java create mode 100644 support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/defaults/DefaultPropertiesEnvironmentPostProcessor.java create mode 100644 support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/elasticsearch/ElasticsearchFeatureAutoConfiguration.java create mode 100644 support/schbrain-spring-support/src/main/resources/META-INF/spring.factories create mode 100644 support/schbrain-spring-support/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/.gitignore b/.gitignore index 037fb5f..d46ad6e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ target/ # system ignore .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db + +work/ \ No newline at end of file diff --git a/commons/common-util/pom.xml b/commons/common-util/pom.xml new file mode 100644 index 0000000..d8903f3 --- /dev/null +++ b/commons/common-util/pom.xml @@ -0,0 +1,108 @@ + + + + 4.0.0 + + + com.schbrain.framework + commons + ${revision} + + + com.schbrain.common + common-util + + + + com.schbrain.common + common + + + cn.hutool + hutool-all + + + com.google.guava + guava + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + com.fasterxml.jackson.module + jackson-module-blackbird + + + commons-io + commons-io + + + org.apache.commons + commons-collections4 + + + org.apache.commons + commons-compress + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-pool2 + + + org.bouncycastle + bcprov-jdk18on + + + org.springframework.boot + spring-boot-starter + + + + org.redisson + redisson-spring-boot-starter + true + + + org.apache.skywalking + apm-toolkit-trace + true + + + org.springframework + spring-aop + true + + + org.aspectj + aspectjweaver + true + + + com.baomidou + mybatis-plus-extension + ${mybatis-plus.version} + true + + + com.alibaba + easyexcel + true + + + + \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/CommonAutoConfiguration.java b/commons/common-util/src/main/java/com/schbrain/common/CommonAutoConfiguration.java new file mode 100644 index 0000000..8d814d4 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/CommonAutoConfiguration.java @@ -0,0 +1,17 @@ +package com.schbrain.common; + +import com.schbrain.common.util.support.jackson.ObjectMapperModuleConfiguration; +import com.schbrain.common.util.support.task.ThreadPoolConfiguration; +import com.schbrain.common.util.support.trace.TraceParamAspect; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Import; + +/** + * @author liaozan + * @since 2022/1/11 + */ +@AutoConfiguration +@Import({TraceParamAspect.class, ThreadPoolConfiguration.class, ObjectMapperModuleConfiguration.class}) +public class CommonAutoConfiguration { + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/ApplicationName.java b/commons/common-util/src/main/java/com/schbrain/common/util/ApplicationName.java new file mode 100644 index 0000000..e4ec354 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/ApplicationName.java @@ -0,0 +1,15 @@ +package com.schbrain.common.util; + +import org.springframework.core.env.Environment; + +/** + * @author liaozan + * @since 2021/12/31 + */ +public class ApplicationName { + + public static String get(Environment environment) { + return environment.getRequiredProperty("spring.application.name"); + } + +} diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/BeanCopyUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/BeanCopyUtils.java new file mode 100644 index 0000000..71d59b6 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/BeanCopyUtils.java @@ -0,0 +1,63 @@ +package com.schbrain.common.util; + +import cn.hutool.core.lang.Singleton; +import cn.hutool.core.util.ReflectUtil; +import com.google.common.base.Joiner; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.cglib.beans.BeanCopier; + +import java.util.ArrayList; +import java.util.List; + +/** + * only support the same property type + * + * @author liaozan + * @since 2022/1/24 + */ +public class BeanCopyUtils { + + private static final Joiner CACHE_KEY_JOINER = Joiner.on("#"); + + /** + * copy object list + */ + public static List copyList(List sourceList, Class targetType) { + if (CollectionUtils.isEmpty(sourceList)) { + return new ArrayList<>(0); + } + return StreamUtils.toList(sourceList, source -> copy(source, targetType), false, true); + } + + /** + * copy object + */ + public static Target copy(Source source, Class targetType) { + if (source == null || targetType == null) { + return null; + } + return copy(source, ReflectUtil.newInstanceIfPossible(targetType)); + } + + /** + * copy object + */ + public static Target copy(Source source, Target target) { + if (source == null || target == null) { + return null; + } + BeanCopier copier = getCopier(source.getClass(), target.getClass()); + copier.copy(source, target, null); + return target; + } + + private static BeanCopier getCopier(Class sourceClass, Class targetClass) { + String cacheKey = buildCacheKey(sourceClass, targetClass); + return Singleton.get(cacheKey, () -> BeanCopier.create(sourceClass, targetClass, false)); + } + + private static String buildCacheKey(Class source, Class target) { + return CACHE_KEY_JOINER.join(source.getName(), target.getName()); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/ConfigurationPropertiesUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/ConfigurationPropertiesUtils.java new file mode 100644 index 0000000..1aeb36c --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/ConfigurationPropertiesUtils.java @@ -0,0 +1,47 @@ +package com.schbrain.common.util; + +import cn.hutool.core.bean.BeanUtil; +import com.google.common.base.CaseFormat; +import com.google.common.base.Converter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.annotation.MergedAnnotation; + +import java.util.*; + +/** + * @author liaozan + * @since 2022/1/11 + */ +public class ConfigurationPropertiesUtils { + + private static final Converter DEFAULT_CONVERTER = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_HYPHEN); + + public static Map toMap(Object source) { + return toMap(source, true); + } + + public static Map toMap(Object source, boolean ignoreNull) { + return toMap(source, true, DEFAULT_CONVERTER); + } + + public static Map toMap(Object source, boolean ignoreNull, Converter converter) { + if (source == null) { + return Collections.emptyMap(); + } + + Class sourceClass = source.getClass(); + if (sourceClass.isAnnotationPresent(ConfigurationProperties.class)) { + String prefix = getPrefix(sourceClass); + Map sourceMap = new LinkedHashMap<>(); + return BeanUtil.beanToMap(source, sourceMap, ignoreNull, key -> prefix + "." + converter.convert(key)); + } + return BeanUtil.beanToMap(source); + } + + public static String getPrefix(Class sourceClass) { + ConfigurationProperties configurationProperties = sourceClass.getAnnotation(ConfigurationProperties.class); + MergedAnnotation mergedAnnotation = MergedAnnotation.from(configurationProperties); + return mergedAnnotation.getString(MergedAnnotation.VALUE); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/EnvUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/EnvUtils.java new file mode 100644 index 0000000..7655b5f --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/EnvUtils.java @@ -0,0 +1,64 @@ +package com.schbrain.common.util; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.core.env.Environment; + +/** + * 注意!! 此类获取 profile 的方法不能用在{@link BeanFactoryPostProcessor} 之前 + * + * @author liaozan + * @see cn.hutool.extra.spring.SpringUtil + * @since 2021/12/13 + */ +public class EnvUtils { + + public static final String DEVELOPMENT = "dev"; + public static final String TESTING = "test"; + public static final String PRODUCTION = "prod"; + + public static boolean isDevelopment() { + return isDevelopment(getProfile()); + } + + public static boolean isDevelopment(String profile) { + return DEVELOPMENT.equals(profile); + } + + public static boolean isTesting() { + return isTesting(getProfile()); + } + + public static boolean isTesting(String profile) { + return TESTING.equals(profile); + } + + public static boolean isProduction() { + return isProduction(getProfile()); + } + + public static boolean isProduction(String profile) { + return PRODUCTION.equals(profile); + } + + public static String getProfile() { + Environment environment = SpringUtil.getBean(Environment.class); + return getProfile(environment); + } + + public static String getProfile(Environment environment) { + String[] profiles = environment.getActiveProfiles(); + if (ArrayUtil.isEmpty(profiles)) { + profiles = environment.getDefaultProfiles(); + } + return profiles[0]; + } + + public static boolean runningOnCloudPlatform(Environment environment) { + CloudPlatform cloudPlatform = CloudPlatform.getActive(environment); + return cloudPlatform != null && cloudPlatform != CloudPlatform.NONE; + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/ExcelUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/ExcelUtils.java new file mode 100644 index 0000000..3aa0437 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/ExcelUtils.java @@ -0,0 +1,130 @@ +package com.schbrain.common.util; + +import com.alibaba.excel.EasyExcel; +import com.schbrain.common.util.support.excel.bean.ExcelReadResult; +import com.schbrain.common.util.support.excel.exception.ExcelException; +import com.schbrain.common.util.support.excel.listener.*; +import com.schbrain.common.util.support.excel.listener.HierarchicalDataReadListener.ImportedRecord; +import org.apache.commons.collections4.CollectionUtils; + +import java.io.*; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +/** + * @author liaozan + * @since 2022/1/6 + */ +public class ExcelUtils { + + // file + public static ExcelReadResult> read(File excelFile) { + return getMapExcelReadResult(excelFile); + } + + public static ExcelReadResult readAs(Class contentType, File excelFile) { + return getObjectExcelReadResult(contentType, excelFile); + } + + public static ExcelReadResult readAs(Class contentType, File excelFile, ExcelReadListenerBase readListener) { + EasyExcel.read(excelFile, contentType, readListener).doReadAll(); + return readListener.getReadResult(); + } + + public static List readHierarchicalData(File excelFile) { + HierarchicalDataReadListener readListener = new HierarchicalDataReadListener(); + return readHierarchicalData(excelFile, readListener); + } + + public static List readHierarchicalData(File excelFile, HierarchicalDataReadListener readListener) { + EasyExcel.read(excelFile, readListener).doReadAll(); + return readListener.getImportedRecords(); + } + + // stream + public static ExcelReadResult> read(InputStream inputStream) { + return getMapExcelReadResult(inputStream); + } + + public static ExcelReadResult readAs(Class contentType, InputStream inputStream) { + return getObjectExcelReadResult(contentType, inputStream); + } + + public static ExcelReadResult readAs(Class contentType, InputStream inputStream, ExcelReadListenerBase readListener) { + EasyExcel.read(inputStream, contentType, readListener).doReadAll(); + return readListener.getReadResult(); + } + + public static List readHierarchicalData(InputStream inputStream) { + HierarchicalDataReadListener readListener = new HierarchicalDataReadListener(); + return readHierarchicalData(inputStream, readListener); + } + + public static List readHierarchicalData(InputStream inputStream, HierarchicalDataReadListener readListener) { + EasyExcel.read(inputStream, readListener).doReadAll(); + return readListener.getImportedRecords(); + } + + // write + public static Path writeTo(Path location, List dataList) { + if (CollectionUtils.isEmpty(dataList)) { + throw new ExcelException("DataList is empty"); + } + return writeTo(location, dataList, dataList.get(0).getClass()); + } + + public static Path writeTo(Path location, List dataList, Class head) { + if (CollectionUtils.isEmpty(dataList)) { + throw new ExcelException("DataList is empty"); + } + EasyExcel.write(location.toFile()) + .sheet() + .head(head) + .doWrite(dataList); + return location; + } + + public static OutputStream writeTo(OutputStream outputStream, List dataList) { + if (CollectionUtils.isEmpty(dataList)) { + throw new ExcelException("DataList is empty"); + } + return writeTo(outputStream, dataList, dataList.get(0).getClass()); + } + + public static OutputStream writeTo(OutputStream outputStream, List dataList, Class head) { + if (CollectionUtils.isEmpty(dataList)) { + throw new ExcelException("DataList is empty"); + } + EasyExcel.write(outputStream) + .sheet() + .head(head) + .doWrite(dataList); + return outputStream; + } + + private static ExcelReadResult> getMapExcelReadResult(Object excelFile) { + ExcelMapDataReadListener readListener = new ExcelMapDataReadListener(); + if (excelFile instanceof File) { + EasyExcel.read((File) excelFile, readListener).doReadAll(); + } else if (excelFile instanceof InputStream) { + EasyExcel.read((InputStream) excelFile, readListener).doReadAll(); + } else { + throw new ExcelException("Unsupported excel file"); + } + return readListener.getReadResult(); + } + + private static ExcelReadResult getObjectExcelReadResult(Class contentType, Object excelFile) { + ExcelReadListenerBase readListener = new ExcelBeanReadListener<>(); + if (excelFile instanceof File) { + EasyExcel.read((File) excelFile, contentType, readListener).doReadAll(); + } else if (excelFile instanceof InputStream) { + EasyExcel.read((InputStream) excelFile, contentType, readListener).doReadAll(); + } else { + throw new ExcelException("Unsupported excel file"); + } + return readListener.getReadResult(); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/IdWorker.java b/commons/common-util/src/main/java/com/schbrain/common/util/IdWorker.java new file mode 100644 index 0000000..67959a9 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/IdWorker.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.schbrain.common.util; + +import java.security.SecureRandom; +import java.util.Random; + +/*** + * @author adyliu (imxylz@gmail.com) + * @since 1.0 + */ +public class IdWorker { + + /** + * 生成的自增id的大小减少到18位 + */ + private static final long ID_EPOCH = 1420041600000L; + private static final long workerIdBits = 5L; + private static final long datacenterIdBits = 5L; + private static final long maxWorkerId = ~(-1L << workerIdBits); + private static final long maxDatacenterId = ~(-1L << datacenterIdBits); + private static final long sequenceBits = 12L; + private static final long workerIdShift = sequenceBits; + private static final long datacenterIdShift = sequenceBits + workerIdBits; + private static final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + private static final long sequenceMask = ~(-1L << sequenceBits); + private static final Random r = new SecureRandom(); + // 需要等 r 初始化 + private static final IdWorker INSTANCE = new IdWorker(ID_EPOCH); + private final long workerId; + private final long datacenterId; + private final long idEpoch; + private long lastTimestamp = -1L; + private long sequence; + + public IdWorker(long idEpoch) { + this(r.nextInt((int) maxWorkerId), r.nextInt((int) maxDatacenterId), 0, idEpoch); + } + + public IdWorker(long workerId, long datacenterId, long sequence) { + this(workerId, datacenterId, sequence, 1420041600000L); + } + + public IdWorker(long workerId, long datacenterId, long sequence, long idEpoch) { + this.workerId = workerId; + this.datacenterId = datacenterId; + this.sequence = sequence; + this.idEpoch = idEpoch; + if (workerId < 0 || workerId > maxWorkerId) { + throw new IllegalArgumentException("workerId is illegal: " + workerId); + } + if (datacenterId < 0 || datacenterId > maxDatacenterId) { + throw new IllegalArgumentException("datacenterId is illegal: " + workerId); + } + if (idEpoch >= System.currentTimeMillis()) { + throw new IllegalArgumentException("idEpoch is illegal: " + idEpoch); + } + } + + public static long getId() { + return INSTANCE.nextId(); + } + + public static String getIdStr() { + return String.valueOf(INSTANCE.nextId()); + } + + /** + * get the timestamp (millis second) of id + * + * @param id the nextId + * @return the timestamp of id + */ + public long getIdTimestamp(long id) { + return idEpoch + (id >> timestampLeftShift); + } + + private synchronized long nextId() { + long timestamp = timeGen(); + if (timestamp < lastTimestamp) { + throw new IllegalStateException("Clock moved backwards."); + } + if (lastTimestamp == timestamp) { + sequence = (sequence + 1) & sequenceMask; + if (sequence == 0) { + timestamp = tilNextMillis(lastTimestamp); + } + } else { + sequence = 0; + } + lastTimestamp = timestamp; + return ((timestamp - idEpoch) << timestampLeftShift)// + | (datacenterId << datacenterIdShift)// + | (workerId << workerIdShift)// + | sequence; + } + + private long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + private long timeGen() { + return System.currentTimeMillis(); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/InetUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/InetUtils.java new file mode 100644 index 0000000..0d2727c --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/InetUtils.java @@ -0,0 +1,105 @@ +package com.schbrain.common.util; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import java.util.Enumeration; + +/** + * Copy from SpringCloud + * + * @author liaozan + * @since 2021/11/19 + */ +@Slf4j +public class InetUtils { + + public static HostInfo findFirstNonLoopBackHostInfo() { + InetAddress address = findFirstNonLoopBackAddress(); + if (address != null) { + return convertAddress(address); + } + HostInfo hostInfo = new HostInfo(); + hostInfo.setHostname("localhost"); + hostInfo.setIpAddress("127.0.0.1"); + return hostInfo; + } + + private static InetAddress findFirstNonLoopBackAddress() { + InetAddress result = null; + try { + int lowest = Integer.MAX_VALUE; + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface networkInterface = interfaces.nextElement(); + if (networkInterface.isUp()) { + if (networkInterface.getIndex() < lowest || result == null) { + lowest = networkInterface.getIndex(); + } else { + continue; + } + + Enumeration inetAddresses = networkInterface.getInetAddresses(); + while (inetAddresses.hasMoreElements()) { + InetAddress address = inetAddresses.nextElement(); + if (address instanceof Inet4Address && !address.isLoopbackAddress()) { + result = address; + } + } + } + } + } catch (IOException ex) { + log.error("Cannot get first non-loopBack address", ex); + } + + if (result != null) { + return result; + } + + try { + return InetAddress.getLocalHost(); + } catch (UnknownHostException e) { + log.warn("Unable to retrieve localhost"); + } + + return null; + } + + private static HostInfo convertAddress(final InetAddress address) { + HostInfo hostInfo = new HostInfo(); + String hostname = address.getHostName(); + hostInfo.setHostname(hostname); + hostInfo.setIpAddress(address.getHostAddress()); + return hostInfo; + } + + @Data + public static class HostInfo { + + public static final String NAME = "machineHostInfo"; + + private String ipAddress; + private String hostname; + + @JsonIgnore + public int getIpAddressAsInt() { + InetAddress inetAddress; + String host = this.ipAddress; + if (host == null) { + host = this.hostname; + } + try { + inetAddress = InetAddress.getByName(host); + } catch (final UnknownHostException e) { + throw new IllegalArgumentException(e); + } + return ByteBuffer.wrap(inetAddress.getAddress()).getInt(); + } + + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/JacksonUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/JacksonUtils.java new file mode 100644 index 0000000..3654c24 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/JacksonUtils.java @@ -0,0 +1,243 @@ +package com.schbrain.common.util; + +import cn.hutool.extra.spring.SpringUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.node.NullNode; +import com.schbrain.common.constants.ResponseActionConstants; +import com.schbrain.common.constants.ResponseCodeConstants; +import com.schbrain.common.exception.BaseException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.TypeUtils; + +import java.lang.reflect.ParameterizedType; +import java.util.*; + +import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; + +/** + * @author liaozan + * @since 2021/10/12 + */ +@Slf4j +public class JacksonUtils { + + private static ObjectMapper OBJECT_MAPPER; + private static ObjectMapper PRETTY_OBJECT_MAPPER; + + public static ObjectMapper getObjectMapper() { + if (OBJECT_MAPPER == null) { + // Delay to get ObjectMapper from spring container to keep the same behavior with application + try { + OBJECT_MAPPER = SpringUtil.getBean(ObjectMapper.class).copy(); + } catch (Exception e) { + log.warn("Could not get ObjectMapper from Spring Container, return new instance for use"); + OBJECT_MAPPER = new ObjectMapper(); + } + OBJECT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } + return OBJECT_MAPPER; + } + + public static ObjectMapper getPrettyObjectMapper() { + if (PRETTY_OBJECT_MAPPER == null) { + PRETTY_OBJECT_MAPPER = getObjectMapper().copy().enable(INDENT_OUTPUT); + } + return PRETTY_OBJECT_MAPPER; + } + + public static String toJsonString(Object data) { + return toJsonString(data, false); + } + + public static String toPrettyJsonString(Object data) { + return toJsonString(data, true); + } + + public static String toJsonString(Object data, boolean pretty) { + try { + if (pretty) { + return getPrettyObjectMapper().writeValueAsString(data); + } + return getObjectMapper().writeValueAsString(data); + } catch (Exception e) { + throw new JSONException("Object 转 JSON 出错", e); + } + } + + public static JsonNode getJsonNode(String json) { + try { + if (StringUtils.isBlank(json)) { + return NullNode.getInstance(); + } + return getObjectMapper().readTree(json); + } catch (Exception e) { + throw new JSONException("JSON 转 JsonNode 出错", e); + } + } + + public static T getObjectFromBytes(byte[] bytes, Class type) { + if (ArrayUtils.isEmpty(bytes)) { + return null; + } + try { + return getObjectMapper().readValue(bytes, type); + } catch (Exception e) { + throw new JSONException("Byte 转 Object 出错", e); + } + } + + public static T getObjectFromBytes(byte[] bytes, ParameterizedType type) { + if (ArrayUtils.isEmpty(bytes)) { + return null; + } + try { + JavaType valueType = getObjectMapper().getTypeFactory().constructType(type); + return getObjectMapper().readValue(bytes, valueType); + } catch (Exception e) { + throw new JSONException("Byte 转 Object 出错", e); + } + } + + public static T getObjectFromBytes(byte[] bytes, TypeReference typeReference) { + if (ArrayUtils.isEmpty(bytes)) { + return null; + } + try { + return getObjectMapper().readValue(bytes, typeReference); + } catch (Exception e) { + throw new JSONException("Byte 转 Object 出错", e); + } + } + + public static T getObjectFromBytesWithTypeConstruct(byte[] bytes, Class genericsType, Class... innerTypes) { + if (ArrayUtils.isEmpty(bytes)) { + return null; + } + try { + JavaType valueType = constructType(genericsType, innerTypes); + return getObjectMapper().readValue(bytes, valueType); + } catch (Exception e) { + throw new JSONException("Byte 转 Object 出错", e); + } + } + + public static T getObjectFromJson(String json, Class type) { + if (StringUtils.isBlank(json)) { + return null; + } + try { + return getObjectMapper().readValue(json, type); + } catch (Exception e) { + throw new JSONException("JSON 转 Object 出错", e); + } + } + + public static T getObjectFromJson(String json, ParameterizedType type) { + if (StringUtils.isBlank(json)) { + return null; + } + try { + JavaType valueType = getObjectMapper().getTypeFactory().constructType(type); + return getObjectMapper().readValue(json, valueType); + } catch (Exception e) { + throw new JSONException("JSON 转 Object 出错", e); + } + } + + public static T getObjectFromJson(String json, TypeReference typeReference) { + if (StringUtils.isBlank(json)) { + return null; + } + try { + return getObjectMapper().readValue(json, typeReference); + } catch (Exception e) { + throw new JSONException("JSON 转 Object 出错", e); + } + } + + public static T getObjectFromJsonWithTypeConstruct(String json, Class genericsType, Class... innerTypes) { + if (StringUtils.isBlank(json)) { + return null; + } + try { + JavaType valueType = constructType(genericsType, innerTypes); + return getObjectMapper().readValue(json, valueType); + } catch (Exception e) { + throw new JSONException("JSON 转 Object 出错", e); + } + } + + public static Map getMapFromJson(String json) { + return getMapFromJson(json, Object.class); + } + + public static Map getMapFromJson(String json, Class valueType) { + return getMapFromJson(json, String.class, valueType); + } + + public static Map getMapFromJson(String json, Class keyType, Class valueType) { + if (StringUtils.isBlank(json)) { + return new LinkedHashMap<>(); + } + try { + JavaType mapType = getObjectMapper().getTypeFactory().constructMapType(LinkedHashMap.class, keyType, valueType); + return getObjectMapper().readValue(json, mapType); + } catch (Exception e) { + throw new JSONException("JSON 转 Map 出错", e); + } + } + + public static List getListFromJson(String json, Class itemType) { + if (StringUtils.isBlank(json)) { + return new ArrayList<>(); + } + try { + JavaType listType = getObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, itemType); + return getObjectMapper().readValue(json, listType); + } catch (Exception e) { + throw new JSONException("JSON 转 List 出错", e); + } + } + + public static byte[] writeObjectAsBytes(Object data) { + try { + return getObjectMapper().writeValueAsBytes(data); + } catch (Exception e) { + throw new JSONException("Object 转 Byte 出错", e); + } + } + + public static JavaType constructType(Class genericsType, Class... innerTypes) { + return getObjectMapper().getTypeFactory().constructType(TypeUtils.parameterize(genericsType, innerTypes)); + } + + public static T convertValue(Object fromValue, Class toValueType) { + try { + return getObjectMapper().convertValue(fromValue, toValueType); + } catch (Exception e) { + throw new JSONException("JSON 转换出错", e); + } + } + + public static T convertValue(Object fromValue, JavaType toValueType) { + try { + return getObjectMapper().convertValue(fromValue, toValueType); + } catch (Exception e) { + throw new JSONException("JSON 转换出错", e); + } + } + + public static class JSONException extends BaseException { + + private static final long serialVersionUID = 1656914307906296812L; + + public JSONException(String message, Throwable cause) { + super(message, cause, ResponseCodeConstants.SERVER_ERROR, ResponseActionConstants.ALERT); + } + + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/PageUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/PageUtils.java new file mode 100644 index 0000000..005cbd3 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/PageUtils.java @@ -0,0 +1,73 @@ +package com.schbrain.common.util; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.schbrain.common.entity.PageParam; +import com.schbrain.common.entity.PaginationInfo; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import static com.schbrain.common.constants.PageConstants.*; + +/** + * @author liaozan + * @since 2022/1/7 + */ +public class PageUtils { + + public static IPage fromParam(PageParam pageParam) { + return fromParam(pageParam.getPageIndex(), pageParam.getPageSize(), pageParam.isSearchCount()); + } + + public static IPage fromParam(long pageIndex, long pageSize) { + return fromParam(pageIndex, pageSize, DEFAULT_SEARCH_COUNT); + } + + public static IPage fromParam(long pageIndex, long pageSize, boolean searchCount) { + return Page.of(pageIndex, pageSize, searchCount); + } + + public static PaginationInfo fromResult(IPage page) { + return fromResult(page, page.getRecords()); + } + + public static PaginationInfo fromResult(IPage page, Function mapper) { + return fromResult(page.convert(mapper)); + } + + public static PaginationInfo fromResult(IPage page, List dataList) { + return new PaginationInfo<>(page.getCurrent(), page.getSize(), page.getTotal(), dataList); + } + + public static PaginationInfo fromResult(PaginationInfo info, Function mapper) { + List dataList = StreamUtils.toList(info.getDataList(), mapper); + return fromResult(info, dataList); + } + + public static PaginationInfo fromResult(PaginationInfo info, List dataList) { + return new PaginationInfo<>(info.getPageIndex(), info.getPageSize(), info.getTotalCount(), dataList); + } + + public static PaginationInfo emptyResult(IPage page) { + return new PaginationInfo<>(page.getCurrent(), page.getSize(), DEFAULT_TOTAL_COUNT, Collections.emptyList()); + } + + public static PaginationInfo emptyResult(PageParam pageParam) { + return new PaginationInfo<>(pageParam.getPageIndex(), pageParam.getPageSize(), DEFAULT_TOTAL_COUNT, Collections.emptyList()); + } + + public static PaginationInfo emptyResult(PaginationInfo page) { + return new PaginationInfo<>(page.getPageIndex(), page.getPageSize(), DEFAULT_TOTAL_COUNT, Collections.emptyList()); + } + + public static PageParam toParam(IPage page) { + PageParam pageParam = new PageParam(); + pageParam.setPageIndex(Math.toIntExact(page.getCurrent())); + pageParam.setPageSize(Math.toIntExact(page.getSize())); + pageParam.setSearchCount(page.searchCount()); + return pageParam; + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/ParameterDiscoverUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/ParameterDiscoverUtils.java new file mode 100644 index 0000000..bc00849 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/ParameterDiscoverUtils.java @@ -0,0 +1,33 @@ +package com.schbrain.common.util; + +import cn.hutool.core.util.ArrayUtil; +import com.google.common.collect.Maps; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; + +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @author liaozan + * @since 2022/5/18 + */ +public class ParameterDiscoverUtils { + + private static final ParameterNameDiscoverer DISCOVERER = new DefaultParameterNameDiscoverer(); + + public static Map getMethodArgsMap(Method method, Object[] args) { + String[] parameterNames = DISCOVERER.getParameterNames(method); + if (ArrayUtil.isEmpty(parameterNames)) { + // Return a new instance to avoid external modification causing errors + return new LinkedHashMap<>(); + } + Map argsMap = Maps.newLinkedHashMapWithExpectedSize(parameterNames.length); + for (int i = 0; i < parameterNames.length; i++) { + argsMap.put(parameterNames[i], args[i]); + } + return argsMap; + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/PortUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/PortUtils.java new file mode 100644 index 0000000..e22ec12 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/PortUtils.java @@ -0,0 +1,49 @@ +package com.schbrain.common.util; + +import com.schbrain.common.exception.BaseException; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.ServerSocket; + +/** + * net util + * + * @author xuxueli 2017-11-29 17:00:25 + */ +@Slf4j +public class PortUtils { + + public static int findAvailablePort(int defaultPort) { + int portTmp = defaultPort; + while (portTmp < 65535) { + if (!isPortUsed(portTmp)) { + return portTmp; + } else { + portTmp++; + } + } + throw new BaseException("no available port."); + } + + public static boolean isPortUsed(int port) { + boolean used = false; + ServerSocket serverSocket = null; + try { + serverSocket = new ServerSocket(port); + } catch (IOException e) { + log.warn("current port[{}] is in use", port); + used = true; + } finally { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + log.info(e.getMessage(), e); + } + } + } + return used; + } + +} diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/SpelUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/SpelUtils.java new file mode 100644 index 0000000..406294f --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/SpelUtils.java @@ -0,0 +1,39 @@ +package com.schbrain.common.util; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.expression.*; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SpelUtils { + + private static final ExpressionParser spELParser = new SpelExpressionParser(); + + private static final ConcurrentHashMap expressionCache = new ConcurrentHashMap<>(); + + public static T parse(String express, Map variables, Class valueType) { + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariables(variables); + return parse(express, ctx, valueType); + } + + public static T parse(String express, EvaluationContext context, Class valueType) { + if (StringUtils.isBlank(express)) { + return null; + } + return getExpression(express).getValue(context, valueType); + } + + private static Expression getExpression(String exp) { + Expression expression = expressionCache.get(exp); + if (null == expression) { + expression = spELParser.parseExpression(exp); + expressionCache.put(exp, expression); + } + return expression; + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/StreamUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/StreamUtils.java new file mode 100644 index 0000000..05f9cc0 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/StreamUtils.java @@ -0,0 +1,160 @@ +package com.schbrain.common.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.*; + +/** + * @author liaozan + * @since 2022/8/21 + */ +@Slf4j +public class StreamUtils { + + public static List filterToList(Collection data, Predicate predicate) { + return filter(data, predicate, Collectors.toList()); + } + + public static Set filterToSet(Collection data, Predicate predicate) { + return filter(data, predicate, Collectors.toSet()); + } + + public static > C filter(Collection data, Predicate predicate, Collector collector) { + return Optional.ofNullable(data).orElse(emptyList()).stream().filter(predicate).collect(collector); + } + + public static List toList(Collection data, Function mapper) { + return toList(data, mapper, false); + } + + public static List toList(Collection data, Function mapper, boolean distinct) { + return toList(data, mapper, distinct, false); + } + + public static List toList(Collection data, Function mapper, boolean distinct, boolean ignoreNull) { + return extract(data, mapper, distinct, ignoreNull, Collectors.toList()); + } + + public static Set toSet(Collection data, Function mapper) { + return toSet(data, mapper, false); + } + + public static Set toSet(Collection data, Function mapper, boolean ignoreNull) { + return extract(data, mapper, ignoreNull, false, Collectors.toSet()); + } + + public static Map toMap(Collection data, Function keyMapper) { + return toMap(data, keyMapper, false); + } + + public static Map toMap(Collection data, Function keyMapper, boolean ordered) { + return toMap(data, keyMapper, Function.identity(), ordered); + } + + public static Map toMap(Collection data, Function keyMapper, Function valueMapper) { + return toMap(data, keyMapper, valueMapper, false); + } + + public static Map toMap(Collection data, Function keyMapper, Function valueMapper, boolean ordered) { + Supplier> mapFactory = HashMap::new; + if (ordered) { + mapFactory = LinkedHashMap::new; + } + return toMap(data, keyMapper, valueMapper, mapFactory); + } + + public static > Map toMap(Collection data, Function keyMapper, Function valueMapper, Supplier mapFactory) { + return Optional.ofNullable(data) + .orElse(emptyList()) + .stream() + .collect(Collectors.toMap(keyMapper, valueMapper, (oldValue, newValue) -> { + // Could not get the key when mergeFunction invoke + log.warn("There are multiple values with the same key when toMap, return the old one"); + return oldValue; + }, mapFactory)); + } + + public static Map> groupBy(Collection data, Function mapper) { + return groupBy(data, mapper, false); + } + + public static Map> groupBy(Collection data, Function keyMapper, boolean ignoreNullKey) { + return groupBy(data, keyMapper, Collectors.toList(), ignoreNullKey); + } + + public static Map groupBy(Collection data, Function mapper, Collector collectors) { + return groupBy(data, mapper, collectors, false); + } + + public static Map groupBy(Collection data, Function mapper, Collector collectors, boolean ignoreNullKey) { + return groupBy(data, mapper, Function.identity(), collectors, ignoreNullKey); + } + + public static Map> groupBy(Collection data, Function keyMapper, Function valueMapper) { + return groupBy(data, keyMapper, valueMapper, Collectors.toList(), false); + } + + public static Map> groupBy(Collection data, Function keyMapper, Function valueMapper, boolean ignoreNullKey) { + return groupBy(data, keyMapper, valueMapper, Collectors.toList(), ignoreNullKey); + } + + public static Map groupBy(Collection data, Function keyMapper, Function valueMapper, Collector collector) { + return groupBy(data, keyMapper, valueMapper, collector, false); + } + + public static Map groupBy(Collection data, Function keyMapper, Function valueMapper, Collector collector, boolean ignoreNullKey) { + return groupBy(data, keyMapper, valueMapper, collector, ignoreNullKey, HashMap::new); + } + + public static Map groupBy(Collection data, Function keyMapper, Function valueMapper, Collector collector, boolean ignoreNullKey, Supplier> mapSupplier) { + Stream stream = Optional.ofNullable(data) + .orElse(emptyList()) + .stream(); + if (ignoreNullKey) { + stream = stream.filter(item -> null != keyMapper.apply(item)); + } + return stream.collect(groupingBy(keyMapper, mapSupplier, mapping(valueMapper, collector))); + } + + public static String join(Collection data, CharSequence delimiter) { + return join(data, delimiter, Objects::toString); + } + + public static String join(Collection data, CharSequence delimiter, Function toStringFunction) { + return join(data, delimiter, "", "", toStringFunction); + } + + public static String join(Collection data, CharSequence delimiter, String prefix, String suffix, Function toStringFunction) { + return Optional.ofNullable(data) + .orElse(emptyList()) + .stream().map(toStringFunction).collect(joining(delimiter, prefix, suffix)); + } + + public static R extract(Collection data, Function mapper, boolean distinct, boolean ignoreNull, Collector collector) { + Predicate predicate = null; + if (ignoreNull) { + predicate = Objects::nonNull; + } + return extract(data, mapper, predicate, distinct, collector); + } + + public static R extract(Collection data, Function mapper, Predicate predicate, boolean distinct, Collector collector) { + Stream stream = Optional.ofNullable(data) + .orElse(emptyList()) + .stream() + .map(mapper); + if (distinct) { + stream = stream.distinct(); + } + if (predicate != null) { + stream = stream.filter(predicate); + } + return stream.collect(collector); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/TraceIdUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/TraceIdUtils.java new file mode 100644 index 0000000..f5db764 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/TraceIdUtils.java @@ -0,0 +1,56 @@ +package com.schbrain.common.util; + +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.IdUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.skywalking.apm.toolkit.trace.TraceContext; +import org.slf4j.MDC; + +/** + * @author liaozan + * @since 2021/10/10 + */ +public class TraceIdUtils { + + public static final String TRACE_ID = "traceId"; + + private static final boolean skywalkingTracePresent; + + private static final ThreadLocal TRACE_ID_CONTAINER = InheritableThreadLocal.withInitial(TraceIdUtils::create); + + static { + skywalkingTracePresent = ClassLoaderUtil.isPresent("org.apache.skywalking.apm.toolkit.trace.TraceContext", TraceIdUtils.class.getClassLoader()); + } + + public static String get() { + return TRACE_ID_CONTAINER.get(); + } + + public static void set(String traceId) { + if (traceId == null) { + return; + } + MDC.put(TRACE_ID, traceId); + TRACE_ID_CONTAINER.set(traceId); + } + + public static void clear() { + MDC.remove(TRACE_ID); + TRACE_ID_CONTAINER.remove(); + } + + private static String create() { + String traceId = MDC.get(TRACE_ID); + if (traceId == null) { + if (skywalkingTracePresent) { + traceId = TraceContext.traceId(); + } + if (StringUtils.isBlank(traceId) || "N/A".equals(traceId)) { + traceId = IdUtil.objectId().toUpperCase(); + } + MDC.put(TRACE_ID, traceId); + } + return traceId; + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/cipher/AES128ECBWithPKCS7.java b/commons/common-util/src/main/java/com/schbrain/common/util/cipher/AES128ECBWithPKCS7.java new file mode 100644 index 0000000..0454101 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/cipher/AES128ECBWithPKCS7.java @@ -0,0 +1,58 @@ +package com.schbrain.common.util.cipher; + +import cn.hutool.crypto.symmetric.AES; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.Security; +import java.util.Base64; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@Slf4j +public class AES128ECBWithPKCS7 { + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * encrypt input text + */ + public static String encrypt(String input, String key) { + byte[] encrypted = encryptByte(input, key); + if (null == encrypted) { + return ""; + } + return Base64.getEncoder().encodeToString(encrypted); + } + + public static byte[] encryptByte(String input, String key) { + byte[] encrypted; + try { + encrypted = new AES("ECB", "PKCS7Padding", key.getBytes(UTF_8)).encrypt(input); + } catch (Exception e) { + log.error("AES128ECBWithPKCS7 encrypt ({}) error! ", input, e); + return null; + } + return encrypted; + } + + /** + * decrypt input text + */ + public static String decrypt(String input, String key) { + return decryptByte(Base64.getDecoder().decode(input), key); + } + + public static String decryptByte(byte[] input, String key) { + try { + byte[] output = new AES("ECB", "PKCS7Padding", key.getBytes(UTF_8)).decrypt(input); + return new String(output, UTF_8); + } catch (Exception e) { + log.error("AES128ECBWithPKCS7 decryptByte ({}) error!", new String(input, UTF_8), e); + return ""; + } + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/log/LogEvent.java b/commons/common-util/src/main/java/com/schbrain/common/util/log/LogEvent.java new file mode 100644 index 0000000..0ffaafa --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/log/LogEvent.java @@ -0,0 +1,19 @@ +package com.schbrain.common.util.log; + +import lombok.Data; + +@Data +public class LogEvent { + + /** + * @see com.schbrain.common.constants.LogConstants.ProductTypeEnum + */ + private String product; + + private T eventAction; + + public LogEvent(String product) { + this.product = product; + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/log/LogEventAction.java b/commons/common-util/src/main/java/com/schbrain/common/util/log/LogEventAction.java new file mode 100644 index 0000000..903e3f7 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/log/LogEventAction.java @@ -0,0 +1,31 @@ +package com.schbrain.common.util.log; + +import java.util.HashMap; + +public class LogEventAction extends HashMap { + + private static final long serialVersionUID = -8663962491116732785L; + + private static final String NAME = "name"; + + public LogEventAction(String actionName) { + put(NAME, actionName); + } + + public String getActionName() { + return (String) get(NAME); + } + + public void setActionName(String actionName) { + put(NAME, actionName); + } + + public void setActionParam(Object key, Object value) { + put(key, value); + } + + public Object getActionParam(Object key) { + return get(key); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/log/StatisticLogUtil.java b/commons/common-util/src/main/java/com/schbrain/common/util/log/StatisticLogUtil.java new file mode 100644 index 0000000..7cdd3f2 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/log/StatisticLogUtil.java @@ -0,0 +1,17 @@ +package com.schbrain.common.util.log; + +import com.schbrain.common.util.JacksonUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StatisticLogUtil { + + public static final Logger log = LoggerFactory.getLogger(StatisticLogUtil.class); + + public static void logEvent(LogEvent logEvent) { + if (null != logEvent) { + log.info(JacksonUtils.toJsonString(logEvent)); + } + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/properties/SchbrainMapPropertySource.java b/commons/common-util/src/main/java/com/schbrain/common/util/properties/SchbrainMapPropertySource.java new file mode 100644 index 0000000..1b8c0b8 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/properties/SchbrainMapPropertySource.java @@ -0,0 +1,24 @@ +package com.schbrain.common.util.properties; + +import com.schbrain.common.util.ConfigurationPropertiesUtils; +import org.springframework.core.env.MapPropertySource; + +import java.util.Map; + +/** + * mark class to ensure property order + * + * @author liaozan + * @since 2021/12/6 + */ +public class SchbrainMapPropertySource extends MapPropertySource { + + public SchbrainMapPropertySource(String name, Object source) { + this(name, ConfigurationPropertiesUtils.toMap(source)); + } + + public SchbrainMapPropertySource(String name, Map source) { + super(name, source); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/ConfigurableProperties.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/ConfigurableProperties.java new file mode 100644 index 0000000..7c79ec4 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/ConfigurableProperties.java @@ -0,0 +1,63 @@ +package com.schbrain.common.util.support; + +import com.schbrain.common.util.ConfigurationPropertiesUtils; +import lombok.Data; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.beans.Introspector; + +/** + * WARNING!!! + *

+ * If you want to use a subclass of this class before {@link BeanPostProcessor}, + * please load it through {@link com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils} + * + * @author liaozan + * @since 2022/1/10 + */ +@Data +public abstract class ConfigurableProperties { + + /** + * the namespace of remote config + */ + protected String namespace = getDefaultNamespace(); + + /** + * the prefix of properties + */ + protected String prefix = getPossiblePrefix(); + + /** + * the name of propertySource + */ + protected String name = Introspector.decapitalize(getClass().getSimpleName()); + + public String getDefaultNamespace() { + return "application"; + } + + @SuppressWarnings({"unchecked", "unused"}) + public T bindOrCreate(ConfigurableEnvironment environment, boolean afterMerge) { + return (T) Binder.get(environment, bindHandler()).bindOrCreate(getPrefix(), getClass()); + } + + protected BindHandler bindHandler() { + return BindHandler.DEFAULT; + } + + private String getPossiblePrefix() { + ConfigurationProperties annotation = getClass().getAnnotation(ConfigurationProperties.class); + if (annotation == null) { + String className = ConfigurationProperties.class.getName(); + String errorDetail = getClass().getSimpleName() + " must annotated @" + className + " or overwrite getPrefix method"; + throw new IllegalStateException(errorDetail); + } + return ConfigurationPropertiesUtils.getPrefix(getClass()); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/ValidateSupport.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/ValidateSupport.java new file mode 100644 index 0000000..08d98dc --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/ValidateSupport.java @@ -0,0 +1,63 @@ +package com.schbrain.common.util.support; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; + +/** + * @author liaozan + * @since 2021/3/27 + */ +public interface ValidateSupport { + + default boolean hasText(String text) { + return StringUtils.isNotBlank(text); + } + + default boolean isBlank(String text) { + return !hasText(text); + } + + default boolean isNull(Object object) { + return object == null; + } + + default boolean isNotNull(Object object) { + return !isNull(object); + } + + default boolean isEmpty(Collection collection) { + return CollectionUtils.isEmpty(collection); + } + + default boolean isNotEmpty(Collection collection) { + return !isEmpty(collection); + } + + default boolean isEmpty(Map map) { + return MapUtils.isEmpty(map); + } + + default boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + default String fixNull(String value) { + return fixNull(value, ""); + } + + default List fixNull(List value) { + return fixNull(value, new ArrayList<>()); + } + + default Set fixNull(Set value) { + return fixNull(value, new HashSet<>()); + } + + default T fixNull(T value, T defaultValue) { + return value != null ? value : defaultValue; + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/delay/AbstractDelayMessageListener.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/delay/AbstractDelayMessageListener.java new file mode 100644 index 0000000..5257b4b --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/delay/AbstractDelayMessageListener.java @@ -0,0 +1,73 @@ +package com.schbrain.common.util.support.delay; + +import cn.hutool.core.thread.ThreadUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author lik + * @since 2023/4/17 + */ +@Slf4j +public abstract class AbstractDelayMessageListener implements Runnable, InitializingBean, DisposableBean { + + private final String threadNamePrefix; + + private final String queueName; + + private final String listenerName; + + private final AtomicBoolean started = new AtomicBoolean(false); + + private ExecutorService executor; + + public AbstractDelayMessageListener(String queueName) { + this(null, queueName); + } + + public AbstractDelayMessageListener(String threadNamePrefix, String queueName) { + this.listenerName = getClass().getSimpleName(); + this.threadNamePrefix = threadNamePrefix == null ? listenerName + "#delay-message-" : threadNamePrefix; + this.queueName = queueName; + } + + @Override + public void run() { + try { + while (started.get()) { + Message message = DelayedQueueUtils.takeMsg(queueName); + if (log.isDebugEnabled()) { + log.debug("{} receive message :{}", listenerName, message); + } + onMessage(message); + } + } catch (Exception e) { + log.error("{} occur error: {}", listenerName, e.getMessage(), e); + } + } + + @Override + public void afterPropertiesSet() { + executor = ThreadUtil.newFixedExecutor(1, threadNamePrefix, true); + started.set(true); + executor.execute(this); + log.info("{} start listen...", listenerName); + } + + @Override + public void destroy() { + started.set(false); + executor.shutdown(); + log.info("{} stop listen... ", listenerName); + } + + /** + * for subClass + */ + protected abstract void onMessage(Message message); + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/delay/DelayedQueueUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/delay/DelayedQueueUtils.java new file mode 100644 index 0000000..01b9645 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/delay/DelayedQueueUtils.java @@ -0,0 +1,103 @@ +package com.schbrain.common.util.support.delay; + +import cn.hutool.core.text.StrPool; +import cn.hutool.extra.spring.SpringUtil; +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.ApplicationName; +import org.redisson.api.*; +import org.springframework.beans.BeansException; +import org.springframework.core.env.Environment; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * @author lik + * @since 2023/4/17 + */ +public class DelayedQueueUtils { + + private static final String APPLICATION_NAME = ApplicationName.get(SpringUtil.getBean(Environment.class)); + + private static final ConcurrentHashMap> blockingQueueCache = new ConcurrentHashMap<>(); + + private static final ConcurrentHashMap> delayedQueueCache = new ConcurrentHashMap<>(); + + private static RedissonClient CLIENT; + + public static void offerMsg(T message, String queueName, Long delay, TimeUnit timeUnit) { + batchOfferMsg(List.of(message), queueName, delay, timeUnit); + } + + public static void batchOfferMsg(List messages, String queueName, Long delay, TimeUnit timeUnit) { + if (CollectionUtils.isEmpty(messages)) { + return; + } + RDelayedQueue delayedQueue = getDelayedQueue(queueName); + messages.forEach(msg -> delayedQueue.offer(msg, delay, timeUnit)); + } + + public static T takeMsg(String queueName) { + RBlockingQueue blockingQueue = getBlockingQueue(queueName); + try { + return blockingQueue.take(); + } catch (InterruptedException e) { + throw new BaseException("redis blocking queue thread has been interrupted", e); + } + } + + public static Boolean removeMsg(String queueName, T msg) { + if (msg == null) { + return false; + } + RDelayedQueue delayedQueue = getDelayedQueue(queueName); + return delayedQueue.remove(msg); + } + + public static void removeQueue(String queueName) { + String wrappedQueueName = withPrefix(queueName); + RBlockingQueue blockingQueue = blockingQueueCache.get(wrappedQueueName); + RDelayedQueue delayedQueue = delayedQueueCache.get(wrappedQueueName); + if (delayedQueue != null) { + delayedQueue.destroy(); + delayedQueueCache.remove(wrappedQueueName); + } + if (blockingQueue != null) { + blockingQueue.delete(); + blockingQueueCache.remove(wrappedQueueName); + } + } + + @SuppressWarnings("unchecked") + private static RDelayedQueue getDelayedQueue(String queueName) { + String cacheKey = withPrefix(queueName); + return (RDelayedQueue) delayedQueueCache.computeIfAbsent(cacheKey, key -> { + RBlockingQueue blockingQueue = getBlockingQueue(queueName); + return getClient().getDelayedQueue(blockingQueue); + }); + } + + @SuppressWarnings("unchecked") + private static RBlockingQueue getBlockingQueue(String queueName) { + String cacheKey = withPrefix(queueName); + return (RBlockingQueue) blockingQueueCache.computeIfAbsent(cacheKey, key -> getClient().getBlockingQueue(cacheKey)); + } + + private static RedissonClient getClient() { + if (CLIENT == null) { + try { + CLIENT = SpringUtil.getBean(RedissonClient.class); + } catch (BeansException e) { + throw new BaseException("Could not get RedissonClient, please put it into Spring container", e); + } + } + return CLIENT; + } + + private static String withPrefix(String queueName) { + return String.join(StrPool.COLON, APPLICATION_NAME, "delay-queue", queueName); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/bean/ExcelReadResult.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/bean/ExcelReadResult.java new file mode 100644 index 0000000..f257830 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/bean/ExcelReadResult.java @@ -0,0 +1,28 @@ +package com.schbrain.common.util.support.excel.bean; + +import com.google.common.collect.Table; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * @author liaozan + * @since 2022/1/6 + */ +@Data +public class ExcelReadResult { + + private List dataList; + + private Map headMap; + + private Table errors; + + private String errorsAsString; + + public boolean hasError() { + return !errors.isEmpty(); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/exception/ExcelException.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/exception/ExcelException.java new file mode 100644 index 0000000..27de685 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/exception/ExcelException.java @@ -0,0 +1,21 @@ +package com.schbrain.common.util.support.excel.exception; + +import com.schbrain.common.exception.BaseException; + +/** + * @author liaozan + * @since 2022/1/6 + */ +public class ExcelException extends BaseException { + + private static final long serialVersionUID = -2338463360273587530L; + + public ExcelException(String message) { + super(message); + } + + public ExcelException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelBeanReadListener.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelBeanReadListener.java new file mode 100644 index 0000000..ba6945d --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelBeanReadListener.java @@ -0,0 +1,37 @@ +package com.schbrain.common.util.support.excel.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.read.metadata.holder.ReadSheetHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; + +import javax.validation.ConstraintViolation; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author liaozan + * @since 2022/1/7 + */ +@Slf4j +public class ExcelBeanReadListener extends ExcelReadListenerBase { + + @Override + protected boolean validate(T data, AnalysisContext context) { + Set> violations = getValidator().validate(data); + if (CollectionUtils.isEmpty(violations)) { + return true; + } + collectErrorMsg(context, violations); + return false; + } + + protected void collectErrorMsg(AnalysisContext context, Set> violations) { + String errorMsg = violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(",")); + ReadSheetHolder currentSheet = context.readSheetHolder(); + String sheetName = currentSheet.getSheetName(); + Integer rowIndex = currentSheet.getRowIndex(); + getErrors().put(sheetName, rowIndex + 1, errorMsg); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelMapDataReadListener.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelMapDataReadListener.java new file mode 100644 index 0000000..8bb7ccd --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelMapDataReadListener.java @@ -0,0 +1,11 @@ +package com.schbrain.common.util.support.excel.listener; + +import java.util.Map; + +/** + * @author liaozan + * @since 2022/1/6 + */ +public class ExcelMapDataReadListener extends ExcelReadListenerBase> { + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelReadListenerBase.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelReadListenerBase.java new file mode 100644 index 0000000..680a017 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/ExcelReadListenerBase.java @@ -0,0 +1,84 @@ +package com.schbrain.common.util.support.excel.listener; + +import cn.hutool.extra.spring.SpringUtil; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.schbrain.common.util.support.excel.bean.ExcelReadResult; +import com.schbrain.common.util.support.excel.exception.ExcelException; +import lombok.*; + +import javax.validation.Validator; +import java.util.*; + +/** + * @author liaozan + * @since 2022/1/6 + */ +@Getter(AccessLevel.PROTECTED) +@Setter(AccessLevel.PROTECTED) +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class ExcelReadListenerBase extends AnalysisEventListener { + + protected final Validator validator = SpringUtil.getBean(Validator.class); + + protected List dataList = new LinkedList<>(); + protected Map headers = new HashMap<>(); + protected Table errors = HashBasedTable.create(); + + protected boolean terminateOnValidateFail = false; + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + this.headers = headMap; + } + + @Override + public void invoke(T data, AnalysisContext context) { + boolean validated = validate(data, context); + if (!validated) { + if (isTerminateOnValidateFail()) { + throw new ExcelException(getErrorMsg()); + } + } + this.dataList.add(data); + } + + @Override + public void onException(Exception exception, AnalysisContext context) { + throw new ExcelException(exception.getMessage(), exception); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + } + + public ExcelReadResult getReadResult() { + ExcelReadResult readResult = new ExcelReadResult<>(); + readResult.setDataList(dataList); + readResult.setHeadMap(headers); + readResult.setErrors(errors); + readResult.setErrorsAsString(getErrorMsg()); + return readResult; + } + + protected String getErrorMsg() { + StringBuilder msgBuilder = new StringBuilder(); + errors.rowMap().forEach((sheetName, rows) -> { + msgBuilder.append("sheet: [ ").append(sheetName).append(" ] "); + rows.forEach((rowIndex, error) -> { + String formattedErrorMsg = String.format("第%d行: [ %s ] ", rowIndex, error); + msgBuilder.append(formattedErrorMsg); + }); + msgBuilder.append("\n"); + }); + return msgBuilder.toString(); + } + + protected boolean validate(T data, AnalysisContext context) { + return true; + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/HierarchicalDataReadListener.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/HierarchicalDataReadListener.java new file mode 100644 index 0000000..97e6bd9 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/excel/listener/HierarchicalDataReadListener.java @@ -0,0 +1,91 @@ +package com.schbrain.common.util.support.excel.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.schbrain.common.util.support.excel.exception.ExcelException; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; + +/** + * 层级数据 excel 读取类 + * + * @author liaozan + * @since 2022/3/29 + */ +@Slf4j +public class HierarchicalDataReadListener extends ExcelReadListenerBase> { + + private final List importedRecords = new LinkedList<>(); + + private final Table coordinateTable = HashBasedTable.create(); + + @Override + public void invoke(Map data, AnalysisContext context) { + super.invoke(data, context); + Integer rowIndex = context.readRowHolder().getRowIndex(); + data.forEach((columnIndex, value) -> { + if (StringUtils.isNotBlank(value)) { + buildImportedRow(rowIndex, columnIndex, value); + } + }); + } + + public List getImportedRecords() { + return importedRecords; + } + + @Override + protected boolean validate(Map data, AnalysisContext context) { + Integer rowIndex = context.readRowHolder().getRowIndex(); + if (MapUtils.isEmpty(data)) { + throw new ExcelException(String.format("第 %d 行未读到数据", rowIndex + 1)); + } + if (rowIndex == 0) { + if (StringUtils.isBlank(data.get(0))) { + throw new ExcelException("第一行第一列未读到数据"); + } + } + return true; + } + + protected void buildImportedRow(Integer rowIndex, Integer columnIndex, String text) { + ImportedRecord importedRecord = new ImportedRecord(); + importedRecord.setText(text); + coordinateTable.put(rowIndex, columnIndex, importedRecord); + if (columnIndex == 0) { + importedRecords.add(importedRecord); + } else { + int currentRowIndex = rowIndex; + // 定位到前一列的单元格 + Map prevColumn = coordinateTable.column(columnIndex - 1); + // 从当前行往上找,找到非空的记录,即视为 parent + ImportedRecord parent = prevColumn.get(currentRowIndex); + while (parent == null && currentRowIndex > 0) { + parent = prevColumn.get(--currentRowIndex); + } + if (parent == null) { + throw new ExcelException("数据格式错误,请对比模板调整"); + } + parent.getChildren().add(importedRecord); + } + } + + @Data + public static class ImportedRecord { + + private String text; + private List children = new LinkedList<>(); + + public boolean hasChildren() { + return CollectionUtils.isNotEmpty(children); + } + + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/jackson/JavaTimeModule.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/jackson/JavaTimeModule.java new file mode 100644 index 0000000..50d8da0 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/jackson/JavaTimeModule.java @@ -0,0 +1,36 @@ +package com.schbrain.common.util.support.jackson; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.deser.*; +import com.fasterxml.jackson.datatype.jsr310.ser.*; +import com.schbrain.common.constants.DateTimeFormatters; + +import java.time.*; + +/** + * @author liaozan + * @since 2022/8/12 + */ +public class JavaTimeModule extends SimpleModule { + + private static final long serialVersionUID = -1848725752934764693L; + + public JavaTimeModule() { + this.setup(); + } + + protected void setup() { + this.addSerializer(YearMonth.class, new YearMonthSerializer(DateTimeFormatters.YEAR_MONTH)); + this.addSerializer(MonthDay.class, new MonthDaySerializer(DateTimeFormatters.MONTH_DATE)); + this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatters.DATE)); + this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatters.TIME)); + this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatters.DATE_TIME)); + + this.addDeserializer(YearMonth.class, new YearMonthDeserializer(DateTimeFormatters.YEAR_MONTH)); + this.addDeserializer(MonthDay.class, new MonthDayDeserializer(DateTimeFormatters.MONTH_DATE)); + this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatters.DATE)); + this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatters.TIME)); + this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatters.DATE_TIME)); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/jackson/ObjectMapperModuleConfiguration.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/jackson/ObjectMapperModuleConfiguration.java new file mode 100644 index 0000000..41eb140 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/jackson/ObjectMapperModuleConfiguration.java @@ -0,0 +1,29 @@ +package com.schbrain.common.util.support.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.blackbird.BlackbirdModule; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author liaozan + * @since 2022/1/11 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(ObjectMapper.class) +public class ObjectMapperModuleConfiguration { + + @Bean + public BlackbirdModule blackbirdModule() { + return new BlackbirdModule(); + } + + @Bean + @ConditionalOnMissingBean + public JavaTimeModule javaTimeModule() { + return new JavaTimeModule(); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/lock/RedisLockUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/lock/RedisLockUtils.java new file mode 100644 index 0000000..8c98267 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/lock/RedisLockUtils.java @@ -0,0 +1,81 @@ +package com.schbrain.common.util.support.lock; + +import cn.hutool.core.text.StrPool; +import cn.hutool.extra.spring.SpringUtil; +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.ApplicationName; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.BeansException; +import org.springframework.core.env.Environment; + +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +/** + * @author liaozan + * @since 2023-03-15 + */ +@Slf4j +public class RedisLockUtils { + + private static final String APPLICATION_NAME = ApplicationName.get(SpringUtil.getBean(Environment.class)); + + private static RedissonClient CLIENT; + + public static void executeWithLock(String lockName, Runnable action) { + executeWithLock(lockName, Duration.ofSeconds(3), action); + } + + public static void executeWithLock(String lockName, Duration timeout, Runnable action) { + executeWithLock(lockName, timeout, () -> { + action.run(); + return null; + }); + } + + public static T executeWithLock(String lockName, Callable action) { + return executeWithLock(lockName, Duration.ofSeconds(3), action); + } + + public static T executeWithLock(String lockName, Duration timeout, Callable action) { + RLock lock = getClient().getLock(withPrefix(lockName)); + boolean locked; + try { + locked = lock.tryLock(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new BaseException("Lock thread has been interrupted", e); + } + if (!locked) { + throw new BaseException(String.format("Lock cannot be acquired within %d seconds", timeout.toSeconds())); + } + + try { + return action.call(); + } catch (Exception e) { + throw new BaseException(e.getMessage(), e); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + private static RedissonClient getClient() { + if (CLIENT == null) { + try { + CLIENT = SpringUtil.getBean(RedissonClient.class); + } catch (BeansException e) { + throw new BaseException("Could not get RedissonClient, please put it into Spring container", e); + } + } + return CLIENT; + } + + private static String withPrefix(String lockName) { + return String.join(StrPool.COLON, APPLICATION_NAME, "lock", lockName); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/task/MdcContextPropagationTaskDecorator.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/task/MdcContextPropagationTaskDecorator.java new file mode 100644 index 0000000..fa3422f --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/task/MdcContextPropagationTaskDecorator.java @@ -0,0 +1,25 @@ +package com.schbrain.common.util.support.task; + +import org.slf4j.MDC; +import org.springframework.core.task.TaskDecorator; + +import java.util.Map; + +/** + * @author liaozan + * @since 2022/1/11 + */ +public class MdcContextPropagationTaskDecorator implements TaskDecorator { + + @Override + public Runnable decorate(Runnable runnable) { + Map contextMap = MDC.getCopyOfContextMap(); + return () -> { + if (contextMap != null) { + MDC.setContextMap(contextMap); + } + runnable.run(); + }; + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/task/ThreadPoolConfiguration.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/task/ThreadPoolConfiguration.java new file mode 100644 index 0000000..b214c47 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/task/ThreadPoolConfiguration.java @@ -0,0 +1,28 @@ +package com.schbrain.common.util.support.task; + +import org.springframework.boot.task.TaskExecutorCustomizer; +import org.springframework.boot.task.TaskSchedulerCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author liaozan + * @since 2021/11/22 + */ +@Configuration(proxyBeanMethods = false) +public class ThreadPoolConfiguration { + + @Bean + public TaskExecutorCustomizer mdcSupportTaskExecutorCustomizer() { + return taskExecutor -> { + taskExecutor.setTaskDecorator(new MdcContextPropagationTaskDecorator()); + taskExecutor.setThreadFactory(new UnCaughtExceptionHandleThreadFactory(taskExecutor)); + }; + } + + @Bean + public TaskSchedulerCustomizer mdcSupportTaskSchedulerCustomizer() { + return taskScheduler -> taskScheduler.setThreadFactory(new UnCaughtExceptionHandleThreadFactory(taskScheduler)); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/task/UnCaughtExceptionHandleThreadFactory.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/task/UnCaughtExceptionHandleThreadFactory.java new file mode 100644 index 0000000..59e4bb6 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/task/UnCaughtExceptionHandleThreadFactory.java @@ -0,0 +1,38 @@ +package com.schbrain.common.util.support.task; + +import lombok.extern.slf4j.Slf4j; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.ThreadFactory; + +/** + * @author liaozan + * @since 2022/1/11 + */ +public class UnCaughtExceptionHandleThreadFactory implements ThreadFactory { + + private final ThreadFactory delegate; + + public UnCaughtExceptionHandleThreadFactory(ThreadFactory delegate) { + this.delegate = delegate; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = delegate.newThread(runnable); + thread.setUncaughtExceptionHandler(new LoggingUnCaughtExceptionHandler()); + return thread; + } + + @Slf4j + public static class LoggingUnCaughtExceptionHandler implements UncaughtExceptionHandler { + + @Override + public void uncaughtException(Thread thread, Throwable throwable) { + log.warn("uncaughtException on {}", thread.getName(), throwable); + thread.interrupt(); + } + + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/trace/TraceParam.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/trace/TraceParam.java new file mode 100644 index 0000000..bf41f41 --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/trace/TraceParam.java @@ -0,0 +1,50 @@ +package com.schbrain.common.util.support.trace; + +import com.schbrain.common.util.JacksonUtils; + +import java.lang.annotation.*; +import java.lang.reflect.Method; +import java.util.Map; + +import static com.schbrain.common.util.ParameterDiscoverUtils.getMethodArgsMap; + +/** + * Please use it only for method + *

+ * Trace the parameter of method which annotated with this annotation + * + * @author liaozan + * @since 2022/3/31 + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TraceParam { + + FormatShape shape() default FormatShape.PRETTY_JSON; + + enum FormatShape { + + RAW { + @Override + protected String format0(Map argsMap) { + return argsMap.toString(); + } + }, + + PRETTY_JSON { + @Override + public String format0(Map argsMap) { + return JacksonUtils.toPrettyJsonString(argsMap); + } + }; + + public String format(Method method, Object[] args) { + Map methodArgsMap = getMethodArgsMap(method, args); + return format0(methodArgsMap); + } + + protected abstract String format0(Map argsMap); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/support/trace/TraceParamAspect.java b/commons/common-util/src/main/java/com/schbrain/common/util/support/trace/TraceParamAspect.java new file mode 100644 index 0000000..a1f9f9d --- /dev/null +++ b/commons/common-util/src/main/java/com/schbrain/common/util/support/trace/TraceParamAspect.java @@ -0,0 +1,51 @@ +package com.schbrain.common.util.support.trace; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.LoggerFactory; +import org.springframework.aop.Advisor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + +import java.lang.reflect.Method; + +/** + * @author liaozan + * @since 2022/3/31 + */ +@Slf4j +@Aspect +@ConditionalOnClass({Advisor.class, Aspect.class}) +public class TraceParamAspect { + + @Before("@annotation(traceParam)") + public void tracedMethod(JoinPoint joinPoint, TraceParam traceParam) { + try { + tracingParam(joinPoint, traceParam); + } catch (Exception e) { + log.warn("Could not extract args for method annotated with @{}", TraceParam.class.getSimpleName(), e); + } + } + + protected void tracingParam(JoinPoint joinPoint, TraceParam annotation) { + if (!(joinPoint instanceof MethodSignature)) { + return; + } + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + Class declaringType = methodSignature.getDeclaringType(); + Method method = methodSignature.getMethod(); + Object[] args = joinPoint.getArgs(); + + String formattedArgs = annotation.shape().format(method, args); + String content = format(method, formattedArgs); + + LoggerFactory.getLogger(declaringType).info(content); + } + + protected String format(Method method, Object formattedArgs) { + return String.format("%s\n%s", method.toGenericString(), formattedArgs); + } + +} \ No newline at end of file diff --git a/commons/common-util/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/commons/common-util/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..95db354 --- /dev/null +++ b/commons/common-util/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.common.CommonAutoConfiguration \ No newline at end of file diff --git a/commons/common/pom.xml b/commons/common/pom.xml new file mode 100644 index 0000000..267736a --- /dev/null +++ b/commons/common/pom.xml @@ -0,0 +1,32 @@ + + + + 4.0.0 + + + com.schbrain.framework + commons + ${revision} + + + com.schbrain.common + common + + + + org.projectlombok + lombok + + + org.slf4j + slf4j-api + + + jakarta.validation + jakarta.validation-api + + + + \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/annotation/IgnoreLogin.java b/commons/common/src/main/java/com/schbrain/common/annotation/IgnoreLogin.java new file mode 100644 index 0000000..2a3b44d --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/annotation/IgnoreLogin.java @@ -0,0 +1,18 @@ +package com.schbrain.common.annotation; + +import java.lang.annotation.*; + +/** + * 免登注解 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface IgnoreLogin { + /** + * 是否忽略登录 + * @return + */ + boolean ignore() default true; + +} diff --git a/commons/common/src/main/java/com/schbrain/common/constants/DateTimeFormatters.java b/commons/common/src/main/java/com/schbrain/common/constants/DateTimeFormatters.java new file mode 100644 index 0000000..61b44bd --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/constants/DateTimeFormatters.java @@ -0,0 +1,33 @@ +package com.schbrain.common.constants; + +import java.time.format.DateTimeFormatter; + +import static java.time.ZoneId.systemDefault; +import static java.time.format.DateTimeFormatter.ofPattern; + +/** + * @author liaozan + * @since 2021/10/15 + */ +@SuppressWarnings("unused") +public class DateTimeFormatters { + + public static final String YEAR_MONTH_PATTERN = "yyyy-MM"; + public static final String MONTH_DATE_PATTERN = "MM-dd"; + public static final String DATE_PATTERN = "yyyy-MM-dd"; + public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + public static final String TIME_PATTERN = "HH:mm:ss"; + + public static final DateTimeFormatter YEAR_MONTH = ofPattern(YEAR_MONTH_PATTERN).withZone(systemDefault()); + public static final DateTimeFormatter MONTH_DATE = ofPattern(MONTH_DATE_PATTERN).withZone(systemDefault()); + public static final DateTimeFormatter DATE = ofPattern(DATE_PATTERN).withZone(systemDefault()); + public static final DateTimeFormatter DATE_TIME = ofPattern(DATE_TIME_PATTERN).withZone(systemDefault()); + public static final DateTimeFormatter TIME = ofPattern(TIME_PATTERN).withZone(systemDefault()); + + public static final String YEAR_MONTH_WITH_SLASH_PATTERN = "yyyy/MM"; + public static final String DATE_WITH_SLASH_PATTERN = "yyyy/MM/dd"; + + public static final DateTimeFormatter YEAR_MONTH_WITH_SLASH = ofPattern(YEAR_MONTH_WITH_SLASH_PATTERN).withZone(systemDefault()); + public static final DateTimeFormatter DATE_WITH_SLASH = ofPattern(DATE_WITH_SLASH_PATTERN).withZone(systemDefault()); + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/constants/LogConstants.java b/commons/common/src/main/java/com/schbrain/common/constants/LogConstants.java new file mode 100644 index 0000000..19c71e3 --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/constants/LogConstants.java @@ -0,0 +1,32 @@ +package com.schbrain.common.constants; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.NoSuchElementException; + +public class LogConstants { + + @Getter + @AllArgsConstructor + public enum ProductTypeEnum { + JSC("jsc", "驾驶舱"), + ZS("zs", "中枢"), + ZYB("zyb", "作业宝"), + SZ("sz", "数治"); + + private final String type; + + private final String desc; + + public static ProductTypeEnum valueOfType(String type) { + for (ProductTypeEnum productTypeEnum : values()) { + if (productTypeEnum.getType().equals(type)) { + return productTypeEnum; + } + } + throw new NoSuchElementException(type); + } + } + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/constants/PageConstants.java b/commons/common/src/main/java/com/schbrain/common/constants/PageConstants.java new file mode 100644 index 0000000..3bd6ec6 --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/constants/PageConstants.java @@ -0,0 +1,30 @@ +package com.schbrain.common.constants; + +/** + * @author liaozan + * @since 2022/8/12 + */ +public class PageConstants { + + /** + * 分页时是否 count + */ + public static final boolean DEFAULT_SEARCH_COUNT = true; + /** + * 默认页码数 + */ + public static final int DEFAULT_PAGE_INDEX = 1; + /** + * 默认分页大小 + */ + public static final int DEFAULT_PAGE_SIZE = 20; + /** + * 默认总页数 + */ + public static final long DEFAULT_TOTAL_PAGE_COUNT = 1; + /** + * 默认总记录数 + */ + public static final long DEFAULT_TOTAL_COUNT = 0; + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/constants/ResponseActionConstants.java b/commons/common/src/main/java/com/schbrain/common/constants/ResponseActionConstants.java new file mode 100644 index 0000000..57fcda3 --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/constants/ResponseActionConstants.java @@ -0,0 +1,30 @@ +package com.schbrain.common.constants; + +/** + * @author liwu + * @since 2019/3/29 + */ +public class ResponseActionConstants { + + /** + * 业务无异常时统一返回0 + */ + public static final int NO_ACTION = 0; + /** + * 忽略异常 + */ + public static final int IGNORE = -1; + /** + * 弹框 + */ + public static final int ALERT = -2; + /** + * toast + */ + public static final int TOAST = -3; + /** + * 弹框,点击确定后刷新页面 + */ + public static final int ALERT_REFRESH = -4; + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/constants/ResponseCodeConstants.java b/commons/common/src/main/java/com/schbrain/common/constants/ResponseCodeConstants.java new file mode 100644 index 0000000..ef0bdd7 --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/constants/ResponseCodeConstants.java @@ -0,0 +1,34 @@ +package com.schbrain.common.constants; + +/** + * @author liwu + * @since 2019/3/29 + */ +public class ResponseCodeConstants { + + /** + * 成功 + */ + public static final int SUCCESS = 0; + /** + * 服务器错误,空指针、数组越界等非业务代码抛出异常 + */ + public static final int SERVER_ERROR = -1; + /** + * 非法请求,参数异常、参数格式错误等接口的请求非法性抛出的通用错误 + */ + public static final int PARAM_INVALID = -2; + /** + * 无权限 + */ + public static final int ACCESS_DENIED = -3; + /** + * 用户未登录,且该接口需要登录 + */ + public static final int LOGIN_REQUIRED = -4; + /** + * 系统维护 + */ + public static final int SYSTEM_MAINTENANCE = -5; + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/entity/PageParam.java b/commons/common/src/main/java/com/schbrain/common/entity/PageParam.java new file mode 100644 index 0000000..4dcc0b1 --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/entity/PageParam.java @@ -0,0 +1,23 @@ +package com.schbrain.common.entity; + +import com.schbrain.common.constants.PageConstants; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author liaozan + * @since 2022/1/7 + */ +@Data +public class PageParam implements Serializable { + + private static final long serialVersionUID = 4760680296146863368L; + + private int pageIndex = PageConstants.DEFAULT_PAGE_INDEX; + + private int pageSize = PageConstants.DEFAULT_PAGE_SIZE; + + private boolean searchCount = PageConstants.DEFAULT_SEARCH_COUNT; + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/entity/PaginationInfo.java b/commons/common/src/main/java/com/schbrain/common/entity/PaginationInfo.java new file mode 100644 index 0000000..e5f946e --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/entity/PaginationInfo.java @@ -0,0 +1,103 @@ +package com.schbrain.common.entity; + +import lombok.*; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import static com.schbrain.common.constants.PageConstants.*; + +/** + * @author liaozan + * @since 2021/10/15 + */ +@Data +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PaginationInfo implements Serializable { + + public static final long serialVersionUID = 1320105164315113239L; + + /** + * 页索引 + */ + private long pageIndex = DEFAULT_PAGE_INDEX; + /** + * 每个页面大小 + */ + private long pageSize = DEFAULT_PAGE_SIZE; + /** + * 当前结果集记录数量 + */ + private long currentPageSize = DEFAULT_PAGE_INDEX; + /** + * 总页面数量 + */ + private long totalPageCount = DEFAULT_TOTAL_COUNT; + /** + * 满足条件的记录数量 + */ + private long totalCount = DEFAULT_TOTAL_COUNT; + /** + * 是否有前一页 + */ + private boolean hasPrevPage = false; + /** + * 是否有下一页 + */ + private boolean hasNextPage = false; + /** + * 结果集, Use new ArrayList() instead of collections.emptyList() to prevent errors when users edit it later + */ + private List dataList = new ArrayList<>(0); + + public PaginationInfo(long pageIndex, long pageSize, long totalCount) { + this(pageIndex, pageSize, totalCount, new ArrayList<>(0)); + } + + public PaginationInfo(long pageIndex, long pageSize, long totalCount, List dataList) { + this.setPageIndex(pageIndex); + this.setPageSize(pageSize); + this.setTotalCount(totalCount); + this.setDataList(dataList); + } + + public void setPageIndex(long pageIndex) { + if (pageIndex <= 0) { + pageIndex = DEFAULT_PAGE_INDEX; + } + this.pageIndex = pageIndex; + } + + public void setPageSize(long pageSize) { + if (pageSize <= 0) { + pageSize = DEFAULT_PAGE_SIZE; + } + this.pageSize = pageSize; + } + + public void setTotalCount(long totalCount) { + if (totalCount < 0L) { + totalCount = DEFAULT_TOTAL_COUNT; + } + + if (totalCount == 0L) { + this.totalPageCount = DEFAULT_TOTAL_PAGE_COUNT; + } else { + this.totalPageCount = (totalCount - 1L) / this.pageSize + 1L; + } + + this.hasPrevPage = this.pageIndex > DEFAULT_PAGE_INDEX; + this.hasNextPage = this.pageIndex < totalPageCount; + this.totalCount = totalCount; + } + + public void setDataList(List dataList) { + if (dataList == null) { + dataList = new ArrayList<>(0); + } + this.dataList = dataList; + this.currentPageSize = dataList.size(); + } + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/enums/BooleanEnum.java b/commons/common/src/main/java/com/schbrain/common/enums/BooleanEnum.java new file mode 100644 index 0000000..8d345b5 --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/enums/BooleanEnum.java @@ -0,0 +1,34 @@ +package com.schbrain.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author huangxi + * @since 2022/08/23 + */ +@Getter +@AllArgsConstructor +public enum BooleanEnum { + + /** + * true + */ + TRUE(1), + /** + * false + */ + FALSE(0); + + private final Integer value; + + public static boolean validate(Integer value) { + for (BooleanEnum booleanEnum : values()) { + if (booleanEnum.getValue().equals(value)) { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/enums/ValidateEnum.java b/commons/common/src/main/java/com/schbrain/common/enums/ValidateEnum.java new file mode 100644 index 0000000..51080de --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/enums/ValidateEnum.java @@ -0,0 +1,25 @@ +package com.schbrain.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author huangxi + * @since 2022/08/23 + */ +@Getter +@AllArgsConstructor +public enum ValidateEnum { + + /** + * 有效 + */ + VALID(0), + /** + * 无效 + */ + INVALID(-1); + + private final Integer value; + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/exception/BaseException.java b/commons/common/src/main/java/com/schbrain/common/exception/BaseException.java new file mode 100644 index 0000000..4692ef7 --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/exception/BaseException.java @@ -0,0 +1,41 @@ +package com.schbrain.common.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import static com.schbrain.common.constants.ResponseActionConstants.ALERT; +import static com.schbrain.common.constants.ResponseCodeConstants.SERVER_ERROR; + +/** + * @author liaozan + * @since 2021/10/15 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class BaseException extends RuntimeException { + + private static final long serialVersionUID = 6235672740816644251L; + + protected final int code; + + protected final int action; + + public BaseException(String message) { + this(message, null); + } + + public BaseException(String message, Throwable throwable) { + this(message, throwable, SERVER_ERROR, ALERT); + } + + public BaseException(String message, int code, int action) { + this(message, null, code, action); + } + + public BaseException(String message, Throwable cause, int code, int action) { + super(message, cause); + this.code = code; + this.action = action; + } + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/exception/ParamInvalidException.java b/commons/common/src/main/java/com/schbrain/common/exception/ParamInvalidException.java new file mode 100644 index 0000000..ac6ad22 --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/exception/ParamInvalidException.java @@ -0,0 +1,18 @@ +package com.schbrain.common.exception; + +import static com.schbrain.common.constants.ResponseActionConstants.ALERT; +import static com.schbrain.common.constants.ResponseCodeConstants.PARAM_INVALID; + +/** + * @author liwu + * @since 2019/3/29 + */ +public class ParamInvalidException extends BaseException { + + private static final long serialVersionUID = -4015658097738003486L; + + public ParamInvalidException(String message) { + super(message, PARAM_INVALID, ALERT); + } + +} \ No newline at end of file diff --git a/commons/common/src/main/java/com/schbrain/common/util/ValidateUtils.java b/commons/common/src/main/java/com/schbrain/common/util/ValidateUtils.java new file mode 100644 index 0000000..ca4939e --- /dev/null +++ b/commons/common/src/main/java/com/schbrain/common/util/ValidateUtils.java @@ -0,0 +1,193 @@ +package com.schbrain.common.util; + +import com.schbrain.common.exception.ParamInvalidException; + +import java.util.*; + +public class ValidateUtils { + + /** + * Constructor. This class should not normally be instantiated. + */ + private ValidateUtils() { + } + + public static void isTrue(boolean expression) { + isTrue(expression, "操作有误"); + } + + public static void isTrue(boolean expression, String message) { + if (!expression) { + throw new ParamInvalidException(message); + } + } + + public static void isFalse(boolean expression) { + isFalse(expression, "操作有误"); + } + + public static void isFalse(boolean expression, String message) { + if (expression) { + throw new ParamInvalidException(message); + } + } + + public static void notNull(Object object) { + notNull(object, "The validated object is null"); + } + + public static void notNull(Object object, String message) { + if (object == null) { + throw new ParamInvalidException(message); + } + } + + public static void isNull(Object object) { + isNull(object, "The validated object is not null"); + } + + public static void isNull(Object object, String message) { + if (object != null) { + throw new ParamInvalidException(message); + } + } + + public static void notEmpty(String value) { + notEmpty(value, "The validated string is empty"); + } + + public static void notEmpty(String value, String message) { + if (null == value || value.isBlank()) { + throw new ParamInvalidException(message); + } + } + + public static void isEmpty(String value) { + isEmpty(value, "The validated string is not empty"); + } + + public static void isEmpty(String value, String message) { + if (value != null && !value.isEmpty()) { + throw new ParamInvalidException(message); + } + } + + public static void notEmpty(Object[] array) { + notEmpty(array, "The validated array is empty"); + } + + public static void notEmpty(Object[] array, String message) { + if (array == null || array.length == 0) { + throw new ParamInvalidException(message); + } + } + + public static void isEmpty(Object[] array) { + isEmpty(array, "The validated array is not empty"); + } + + public static void isEmpty(Object[] array, String message) { + if (array != null && array.length != 0) { + throw new ParamInvalidException(message); + } + } + + public static void notEmpty(Collection collection) { + notEmpty(collection, "The validated collection is empty"); + } + + public static void notEmpty(Collection collection, String message) { + if (collection == null || collection.isEmpty()) { + throw new ParamInvalidException(message); + } + } + + public static void isEmpty(Collection collection) { + isEmpty(collection, "The validated collection is not empty"); + } + + public static void isEmpty(Collection collection, String message) { + if (collection != null && !collection.isEmpty()) { + throw new ParamInvalidException(message); + } + } + + public static void notEmpty(Map map) { + notEmpty(map, "The validated map is empty"); + } + + public static void notEmpty(Map map, String message) { + if (map == null || map.isEmpty()) { + throw new ParamInvalidException(message); + } + } + + public static void isEmpty(Map map) { + isEmpty(map, "The validated map is not empty"); + } + + public static void isEmpty(Map map, String message) { + if (map != null && !map.isEmpty()) { + throw new ParamInvalidException(message); + } + } + + public static void noNullElements(Object[] array) { + notNull(array); + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + throw new ParamInvalidException("The validated array contains null element at index: " + i); + } + } + } + + public static void noNullElements(Object[] array, String message) { + notNull(array); + for (Object item : array) { + if (item == null) { + throw new ParamInvalidException(message); + } + } + } + + public static void noNullElements(Collection collection, String message) { + notNull(collection); + for (Object item : collection) { + if (item == null) { + throw new ParamInvalidException(message); + } + } + } + + public static void noNullElements(Collection collection) { + notNull(collection); + int i = 0; + for (Iterator it = collection.iterator(); it.hasNext(); i++) { + if (it.next() == null) { + throw new ParamInvalidException("The validated collection contains null element at index: " + i); + } + } + } + + public static void allElementsOfType(Collection collection, Class clazz, String message) { + notNull(collection); + notNull(clazz); + for (Object item : collection) { + if (!clazz.isInstance(item)) { + throw new ParamInvalidException(message); + } + } + } + + public static void allElementsOfType(Collection collection, Class clazz) { + notNull(collection); + notNull(clazz); + int i = 0; + for (Iterator it = collection.iterator(); it.hasNext(); i++) { + if (!clazz.isInstance(it.next())) { + throw new ParamInvalidException("The validated collection contains an element not of type " + clazz.getName() + " at index: " + i); + } + } + } + +} \ No newline at end of file diff --git a/commons/module-tree/pom.xml b/commons/module-tree/pom.xml new file mode 100644 index 0000000..0ebc03d --- /dev/null +++ b/commons/module-tree/pom.xml @@ -0,0 +1,24 @@ + + + + 4.0.0 + + + com.schbrain.framework + commons + ${revision} + + + com.schbrain.common + module-tree + + + + com.schbrain.framework + schbrain-base-dao + + + + \ No newline at end of file diff --git a/commons/module-tree/src/main/java/com/schbrain/common/module/tree/StructureTreeNode.java b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/StructureTreeNode.java new file mode 100644 index 0000000..f705ba1 --- /dev/null +++ b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/StructureTreeNode.java @@ -0,0 +1,22 @@ +package com.schbrain.common.module.tree; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 结构化的树节点 + * + * @author hzchengyi + * @since 2019/1/21 + */ +@Data +public class StructureTreeNode implements Serializable { + + private static final long serialVersionUID = -7732621737666937981L; + + private NODE node; + private List> children; + +} \ No newline at end of file diff --git a/commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeNode.java b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeNode.java new file mode 100644 index 0000000..fa4326f --- /dev/null +++ b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeNode.java @@ -0,0 +1,45 @@ +package com.schbrain.common.module.tree; + +/** + * @author hzchengyi + * @since 2019/1/21 + */ +public interface TreeNode { + + Long getId(); + + void setId(Long id); + + Long getRelateId(); + + void setRelateId(Long relateId); + + Long getParentId(); + + void setParentId(Long parentId); + + Integer getDepth(); + + void setDepth(Integer depth); + + Integer getLft(); + + void setLft(Integer lft); + + Integer getRgt(); + + void setRgt(Integer rgt); + + Integer getValidate(); + + void setValidate(Integer validate); + + Long getDeleteVersion(); + + void setDeleteVersion(Long deleteVersion); + + default boolean isLeaf() { + return getRgt() == getLft() + 1; + } + +} \ No newline at end of file diff --git a/commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeNodeProcessor.java b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeNodeProcessor.java new file mode 100644 index 0000000..7e8bd08 --- /dev/null +++ b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeNodeProcessor.java @@ -0,0 +1,421 @@ +package com.schbrain.common.module.tree; + +import com.github.pagehelper.Page; +import com.schbrain.common.module.tree.constant.TreeConstant; +import com.schbrain.common.module.tree.dao.TreeNodeDao; +import com.schbrain.common.module.tree.event.TreeOperationAware; +import com.schbrain.common.module.tree.event.TreeOperationEvent; +import com.schbrain.framework.dao.BaseDao; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +/** + * 节点树处理器 + * + * @author hzchengyi + * @since 2019/1/21 + */ +public class TreeNodeProcessor { + + private final TreeNodeDao treeNodeDao; + private final TreeOperationAware operationHandler; + + public TreeNodeProcessor(BaseDao baseDao, TreeOperationAware operationHandler) { + this.treeNodeDao = new TreeNodeDao<>(baseDao); + this.operationHandler = operationHandler; + } + + public List listByNode(NODE node) { + return treeNodeDao.listByNode(node); + } + + public NODE getById(Long nodeId) { + return treeNodeDao.getById(nodeId); + } + + public List listByIds(List nodeIds) { + return treeNodeDao.listByIds(nodeIds); + } + + public Integer getDepth(Long nodeId) { + NODE node = treeNodeDao.getById(nodeId); + if (node == null) { + return null; + } + return node.getDepth(); + } + + public Integer countChildren(Long relateId, Long nodeId, TreeQueryOption option) { + NODE node = treeNodeDao.getById(nodeId); + if (node == null) { + return null; + } + + return treeNodeDao.countChildren(relateId, node, option); + } + + public Map countChildren(Long relateId, List nodeIds, TreeQueryOption option) { + Map result = new HashMap<>(); + for (Long nodeId : nodeIds) { + result.put(nodeId, countChildren(relateId, nodeId, option)); + } + return result; + } + + @Transactional + public NODE addNode(Long parentId, Long preBroNodeId, NODE newNode) { + operationHandler.before(TreeOperationEvent.ADD, Collections.singletonList(newNode)); + + treeNodeDao.addNode(parentId, preBroNodeId, newNode); + + operationHandler.after(TreeOperationEvent.ADD, Collections.singletonList(newNode)); + return newNode; + } + + @Transactional + public NODE createTree(NODE newNode) { + if (treeNodeDao.countByRelateId(newNode.getRelateId()) > 0) { + // relateId已经存在,不能创建新树 + return null; + } + + return addNode(null, null, newNode); + } + + public Boolean isSubNode(NODE parent, NODE child) { + return parent.getRelateId().equals(child.getRelateId()) + && parent.getLft() < child.getLft() + && parent.getRgt() > child.getRgt(); + } + + public Boolean isRelateTo(Long relateId, List nodes) { + for (NODE node : nodes) { + if (!node.getRelateId().equals(relateId)) { + return false; + } + } + return true; + } + + public Boolean areBros(List nodes) { + if (nodes == null || nodes.isEmpty()) { + return false; + } + + Long parentId = nodes.get(0).getParentId(); + for (NODE node : nodes) { + if (!parentId.equals(node.getParentId())) { + return false; + } + } + return true; + } + + @Transactional + public Boolean moveBros(List nodes, NODE parent) { + operationHandler.before(TreeOperationEvent.MOVE, nodes); + + if (null == nodes || nodes.isEmpty() || null == parent) { + return false; + } + if (nodes.get(0).getParentId() <= 0) { + return false; + } + if (!areBros(nodes)) { + return false; + } + Long relateId = parent.getRelateId(); + if (null == relateId) { + return false; + } + if (!isRelateTo(relateId, nodes)) { + return false; + } + NODE srcParent = treeNodeDao.getById(nodes.get(0).getParentId()); + if (null == srcParent) { + return false; + } + NODE targetParent = parent; + // 同级目录下移动,不做任何操作 + if (srcParent.getId().equals(targetParent.getId())) { + return true; + } + List nodeIdList = new ArrayList<>(nodes.size()); + for (NODE node : nodes) { + nodeIdList.add(node.getId()); + // 判断新的parent不是node的子节点以及parent不是节点本身 + if (isSubNode(node, targetParent) || node.getId().equals(targetParent.getId())) { + return false; + } + } + int totalNodes = 0; + Map nodeCountMap = new HashMap<>(nodes.size()); + for (Long nodeId : nodeIdList) { + Integer nodeCount = countChildren(relateId, nodeId, TreeQueryOption.instance().queryIncludeSelf()); + if (null == nodeCount) { + nodeCountMap.put(nodeId, 0); + } else { + int intValue = nodeCount; + nodeCountMap.put(nodeId, intValue); + totalNodes += intValue; + } + } + if (nodes.size() > 1) { + // 将节点进行排序 + nodes.sort(Comparator.comparingInt(TreeNode::getLft)); + } + NODE farLeftNode = nodes.get(0); + TreeQueryOption queryOption = TreeQueryOption.instance().queryExcludeSelf().queryDirectChildren(); + Integer srcParentChildrenCount = treeNodeDao.countChildren(relateId, srcParent, queryOption); + // 如果不是移动srcParent下面的所有节点 + if (srcParentChildrenCount > nodes.size()) { + // 将所有节点按顺序移动到最右边 + for (NODE node : nodes) { + treeNodeDao.moveNodeToFarRight(node.getId(), nodeCountMap.get(node.getId()), srcParent); + } + // 这个时候farLeftNode发生了变化,重新获取 + farLeftNode = treeNodeDao.getById(nodes.get(0).getId()); + // targetParent也可能发生了变化,重新获取 + targetParent = treeNodeDao.getById(targetParent.getId()); + } + + Long tempRelateId = farLeftNode.getId() * -1; + // 将相关节点独立出来 + for (NODE node : nodes) { + // 将节点的relateId更新成tempRelateId + TreeQueryOption option = TreeQueryOption.instance().queryIncludeSelf(); + treeNodeDao.updateSubTreeBySql("SET relate_id = " + tempRelateId, relateId, node.getId(), option); + } + // 把parentId改成targetParent的id + treeNodeDao.updateParentId(nodeIdList, targetParent.getId()); + // 执行移动 + move(relateId, tempRelateId, farLeftNode, totalNodes, srcParent, targetParent); + operationHandler.after(TreeOperationEvent.MOVE, nodes); + return true; + } + + public Boolean isRoot(NODE node) { + return node.getParentId().equals(TreeConstant.ROOT_PARENT_ID); + } + + @Transactional + public Boolean delete(Long relateId, List nodeIdList) { + List nodeList = listByIds(nodeIdList); + if (CollectionUtils.isEmpty(nodeList)) { + return false; + } + if (nodeList.size() != nodeIdList.size()) { + return false; + } + for (NODE node : nodeList) { + if (!node.getRelateId().equals(relateId)) { + return false; + } + } + operationHandler.before(TreeOperationEvent.DELETE, nodeList); + + for (NODE node : nodeList) { + delete(relateId, node); + } + + operationHandler.after(TreeOperationEvent.DELETE, nodeList); + return true; + } + + public List listParent(Long relateId, NODE node, TreeQueryOption option) { + return treeNodeDao.listParent(relateId, node, option); + } + + public StructureTreeNode convert2StructureTree(Long relateId) { + List nodes = batchListNode(relateId); + Map> nodeSubNodeMap = getNodeSubNodeMap(nodes); + NODE root = nodeSubNodeMap.get(TreeConstant.ROOT_PARENT_ID).get(0); + + return constructTree(root, nodeSubNodeMap); + } + + public Boolean update(Long relateId, NODE node) { + node.setValidate(null); + node.setDeleteVersion(null); + node.setLft(null); + node.setRgt(null); + node.setRelateId(null); + node.setParentId(null); + return treeNodeDao.updateNodeById(node); + } + + public Integer getTreeHeight(Long relateId, Long nodeId) { + NODE node = treeNodeDao.getById(nodeId); + if (node == null) { + return 0; + } + NODE maxDepthSubLeaf = treeNodeDao.getMaxDepthSubLeaf(relateId, node); + if (maxDepthSubLeaf == null) { + return 0; + } + + return maxDepthSubLeaf.getDepth() - node.getDepth() + 1; + } + + public Page pageByParent(Long parentId, Integer pageIndex, Integer pageSize) { + return treeNodeDao.pageByParent(parentId, pageIndex, pageSize); + } + + public List listByParent(Long parentId) { + return treeNodeDao.listByParent(parentId); + } + + public List listByParent(Long parentId, Integer levelCount) { + return treeNodeDao.listByParent(parentId, levelCount); + } + + @Transactional + public void repairLeftAndRight(Long rootId) { + doRepairLeftAndRight(getById(rootId), null, null, null, null); + } + + public int updateByIds(NODE updateNode, List nodeIds) { + // 此接口不允许更新树结构相关的字段 + allTreeFiledSetNull(updateNode); + return treeNodeDao.updateNodeByIds(updateNode, nodeIds); + } + + public List listNode(List nodeIds) { + return treeNodeDao.listByIds(nodeIds); + } + + private void move(Long relateId, Long tempRelateId, NODE farLeftNode, int nodeCount, NODE srcParent, NODE targetParent) { + int minLeft, maxLeft, minRight, maxRight, lrDiff; + NODE farRightOfTargetParent = treeNodeDao.getFarRightNode(targetParent.getRelateId(), targetParent.getId()); + if ((farRightOfTargetParent == null && farLeftNode.getLft() < targetParent.getLft()) || + (farRightOfTargetParent != null && farLeftNode.getLft() < farRightOfTargetParent.getRgt())) { + // 往右边移 + minLeft = srcParent.getRgt(); + maxLeft = targetParent.getRgt(); + if (null != farRightOfTargetParent) { + maxLeft = farRightOfTargetParent.getRgt(); + } + minRight = srcParent.getRgt(); + maxRight = targetParent.getRgt(); + lrDiff = -1 * nodeCount * 2; + } else { + // 往左边移 + minLeft = targetParent.getRgt(); + if (null != farRightOfTargetParent) { + minLeft = farRightOfTargetParent.getRgt(); + } + maxLeft = srcParent.getRgt(); + minRight = targetParent.getRgt(); + maxRight = srcParent.getRgt(); + lrDiff = nodeCount * 2; + } + + treeNodeDao.updateLeftWithRang(relateId, lrDiff, minLeft, false, maxLeft, false); + treeNodeDao.updateRightWithRang(relateId, lrDiff, minRight, true, maxRight, false); + // 执行上面两句操作以后,targetParent和farRightOfTargetParent的left和right都发生了变化,所以需要重新获取 + if (null == farRightOfTargetParent) { + targetParent = treeNodeDao.getById(targetParent.getId()); + lrDiff = targetParent.getLft() - farLeftNode.getLft() + 1; + } else { + farRightOfTargetParent = treeNodeDao.getFarRightNode(targetParent.getRelateId(), targetParent.getId()); + lrDiff = farRightOfTargetParent.getRgt() - farLeftNode.getLft() + 1; + } + // 更新要移动节点的left、right、depth,并恢复relateId + int depthDiff = targetParent.getDepth() - srcParent.getDepth(); + treeNodeDao.updateLRAndDepthAndRelateIdWithRelateId(tempRelateId, relateId, lrDiff, depthDiff); + } + + private void delete(Long relateId, NODE node) { + treeNodeDao.deleteSubTree(relateId, node); + } + + private List batchListNode(Long relateId) { + int pageIndex = 1; + Integer pageSize = 500; + Page page = treeNodeDao.page(relateId, pageIndex, pageSize, "id desc"); + List result = new LinkedList<>(page); + while (pageIndex < page.getPages()) { + page = treeNodeDao.page(relateId, ++pageIndex, pageSize, "id desc"); + result.addAll(page); + } + return result; + } + + private Map> getNodeSubNodeMap(List nodes) { + Map> map = new HashMap<>(); + for (NODE node : nodes) { + if (map.containsKey(node.getParentId())) { + map.get(node.getParentId()).add(node); + } else { + List nodeLevel = new LinkedList<>(); + nodeLevel.add(node); + map.put(node.getParentId(), nodeLevel); + } + } + + return map; + } + + private StructureTreeNode constructTree(NODE node, Map> nodeSubNodeMap) { + StructureTreeNode structureNode = new StructureTreeNode<>(); + structureNode.setNode(node); + + if (nodeSubNodeMap.containsKey(node.getId())) { + List childrenNode = nodeSubNodeMap.get(node.getId()); + List> children = new ArrayList<>(); + for (NODE childNode : childrenNode) { + children.add(constructTree(childNode, nodeSubNodeMap)); + } + children.sort(Comparator.comparingInt(e -> e.getNode().getLft())); + structureNode.setChildren(children); + } else { + structureNode.setChildren(new LinkedList<>()); + } + + return structureNode; + } + + private void doRepairLeftAndRight(NODE node, Long parentId, Integer parentLeft, Integer parentDepth, NODE preBro) { + int left, right, depth; + if (null == parentId) { + left = 1; + depth = 1; + } else { + left = parentLeft + 1; + depth = parentDepth + 1; + } + if (null != preBro) { + left = preBro.getRgt() + 1; + } + List children = treeNodeDao.listByParent(node.getId()); + if (null == children || children.isEmpty()) { + right = left + 1; + } else { + NODE preNode = null; + for (NODE child : children) { + doRepairLeftAndRight(child, node.getId(), left, depth, preNode); + preNode = child; + } + right = preNode.getRgt() + 1; + } + if (left != node.getLft() || right != node.getRgt() || depth != node.getDepth()) { + treeNodeDao.updateLRAndDepth(left, right, depth, node.getId()); + node.setLft(left); + node.setRgt(right); + node.setDepth(depth); + } + } + + private void allTreeFiledSetNull(NODE node) { + node.setValidate(null); + node.setRelateId(null); + node.setParentId(null); + node.setDepth(null); + node.setDeleteVersion(null); + node.setLft(null); + node.setRgt(null); + } + +} \ No newline at end of file diff --git a/commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeQueryOption.java b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeQueryOption.java new file mode 100644 index 0000000..2242fbc --- /dev/null +++ b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/TreeQueryOption.java @@ -0,0 +1,91 @@ +package com.schbrain.common.module.tree; + +/** + * @author hzchengyi + * @since 2019/1/21 + */ +public class TreeQueryOption { + + /** + * 包括节点自身 + */ + public static final int TREE_QUERY_SELF_INCLUDE = 0; + /** + * 不包括节点自身 + */ + public static final int TREE_QUERY_SELF_EXCLUDE = 1; + /** + * 只包含直接子节点 + */ + public static final int TREE_QUERY_CHILDREN_DIRECT = 0; + /** + * 包含所有子节点 + */ + public static final int TREE_QUERY_CHILDREN_ALL = 1; + /** + * 深度排序-从根到叶子节点 + */ + public static final int TREE_QUERY_DEPTH_ORDER_ROOT_2_LEAF = 0; + /** + * 深度排序-从叶子节点到根 + */ + public static final int TREE_QUERY_DEPTH_ORDER_LEAF_2_ROOT = 1; + + private int selfIncludeMode; + private int childrenMode; + private int depthOrder; + + private TreeQueryOption() { + } + + public static TreeQueryOption instance() { + TreeQueryOption option = new TreeQueryOption(); + option.selfIncludeMode = TREE_QUERY_SELF_EXCLUDE; + option.childrenMode = TREE_QUERY_CHILDREN_ALL; + option.depthOrder = TREE_QUERY_DEPTH_ORDER_ROOT_2_LEAF; + return option; + } + + public Integer getSelfIncludeMode() { + return this.selfIncludeMode; + } + + public Integer getChildrenMode() { + return this.childrenMode; + } + + public Integer getDepthOrder() { + return this.depthOrder; + } + + public TreeQueryOption queryIncludeSelf() { + this.selfIncludeMode = TREE_QUERY_SELF_INCLUDE; + return this; + } + + public TreeQueryOption queryExcludeSelf() { + this.selfIncludeMode = TREE_QUERY_SELF_EXCLUDE; + return this; + } + + public TreeQueryOption queryDirectChildren() { + this.childrenMode = TREE_QUERY_CHILDREN_DIRECT; + return this; + } + + public TreeQueryOption queryAllChildren() { + this.childrenMode = TREE_QUERY_CHILDREN_ALL; + return this; + } + + public TreeQueryOption depthOrderRoot2Leaf() { + this.depthOrder = TREE_QUERY_DEPTH_ORDER_ROOT_2_LEAF; + return this; + } + + public TreeQueryOption depthOrderLeaf2Root() { + this.depthOrder = TREE_QUERY_DEPTH_ORDER_LEAF_2_ROOT; + return this; + } + +} \ No newline at end of file diff --git a/commons/module-tree/src/main/java/com/schbrain/common/module/tree/constant/TreeConstant.java b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/constant/TreeConstant.java new file mode 100644 index 0000000..788ca02 --- /dev/null +++ b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/constant/TreeConstant.java @@ -0,0 +1,12 @@ +package com.schbrain.common.module.tree.constant; + +/** + * @author hzchengyi + * @since 2019/1/21 + */ +public class TreeConstant { + + public static final Long ROOT_PARENT_ID = -1L; + public static final Long NODE_DELETE_VERSION_DEFAULT = 0L; + +} \ No newline at end of file diff --git a/commons/module-tree/src/main/java/com/schbrain/common/module/tree/dao/TreeNodeDao.java b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/dao/TreeNodeDao.java new file mode 100644 index 0000000..e430ff6 --- /dev/null +++ b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/dao/TreeNodeDao.java @@ -0,0 +1,375 @@ +package com.schbrain.common.module.tree.dao; + +import com.github.pagehelper.Page; +import com.schbrain.common.enums.ValidateEnum; +import com.schbrain.common.module.tree.TreeNode; +import com.schbrain.common.module.tree.TreeQueryOption; +import com.schbrain.common.module.tree.constant.TreeConstant; +import com.schbrain.framework.dao.BaseDao; +import com.schbrain.framework.dao.util.SQLUtil; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; + +/** + * @author hzchengyi + * @since 2019/1/21 + */ +public class TreeNodeDao { + + private final BaseDao baseDao; + + public TreeNodeDao(BaseDao baseDao) { + this.baseDao = baseDao; + } + + public List listByNode(NODE node) { + return baseDao.listByObject(node); + } + + public NODE getById(Long id) { + return baseDao.getOneByCondition("id = #{id} AND validate = #{validate}", id, ValidateEnum.VALID.getValue()); + } + + public List listByIds(List ids) { + return baseDao.listByCondition("validate = #{validate} AND " + SQLUtil.buidInClause("id", Long.class, ids), ValidateEnum.VALID.getValue()); + } + + public Integer countParent(Long relateId, NODE node, TreeQueryOption option) { + StringBuilder condition = new StringBuilder(); + List params = new LinkedList<>(); + + params.add(relateId); + condition + .append("relate_id = #{relateId} ") + .append(" AND ").append(getParentLeftRange(node, option, params)) + .append(" AND ").append(getParentRightRange(node, option, params)) + .append(" AND validate = #{validate}"); + params.add(ValidateEnum.VALID.getValue()); + return baseDao.getCountByCondition(condition.toString(), params.toArray()); + } + + public Integer countChildren(Long relateId, NODE node, TreeQueryOption option) { + StringBuilder condition = new StringBuilder(); + List params = new LinkedList<>(); + + params.add(relateId); + condition + .append("relate_id = #{relateId}") + .append(" AND ").append(getChildrenLeftRange(node, option, params)) + .append(" AND ").append(getChildrenRightRange(node, option, params)) + .append(" AND validate = #{validate}"); + params.add(ValidateEnum.VALID.getValue()); + if (option.getChildrenMode().equals(TreeQueryOption.TREE_QUERY_CHILDREN_DIRECT)) { + // 只查询直接子节点 + condition.append(" AND parent_id = #{parentId} "); + params.add(node.getId()); + } + + return baseDao.getCountByCondition(condition.toString(), params.toArray()); + } + + public Integer countByRelateId(Long relateId) { + return baseDao.getCountByCondition("relate_id = #{relateId} AND validate = #{validate}", relateId, ValidateEnum.VALID.getValue()); + } + + public boolean addNode(Long parentId, Long preBroNodeId, NODE newNode) { + newNode.setValidate(ValidateEnum.VALID.getValue()); + newNode.setDeleteVersion(TreeConstant.NODE_DELETE_VERSION_DEFAULT); + NODE parent = null; + if (null != parentId) { + parent = getById(parentId); + } + NODE preBroNode = null; + if (null != preBroNodeId) { + preBroNode = getById(preBroNodeId); + } + + // 如果parent和preBroNode都为null,说明是根节点 + if (null == parent && null == preBroNode) { + newNode.setLft(1); + newNode.setRgt(2); + newNode.setDepth(1); + newNode.setParentId(TreeConstant.ROOT_PARENT_ID); + return baseDao.add(newNode); + } + // 如果parent不为null并且preBroNode为null,则代表该节点直接为该父节点下的第一个子节点 + if (null == preBroNode) { + // 说明是父节点的第一个子节点 + newNode.setLft(parent.getLft() + 1); + newNode.setRgt(newNode.getLft() + 1); + newNode.setDepth(parent.getDepth() + 1); + newNode.setParentId(parent.getId()); + // 增加父节点右边所有节点的left和right,留出空位 + increaseNodesLeftAndRight(newNode.getRelateId(), parent.getLft(), parent.getLft(), false, 2); + // 添加节点 + return baseDao.add(newNode); + } + // 处理非唯一叶子节点的情况 + newNode.setLft(preBroNode.getRgt() + 1); + newNode.setRgt(newNode.getLft() + 1); + newNode.setDepth(preBroNode.getDepth()); + newNode.setParentId(preBroNode.getParentId()); + // 增加兄弟节点右边所有节点的left和right,留出空位 + increaseNodesLeftAndRight(preBroNode.getRelateId(), preBroNode.getRgt(), preBroNode.getRgt(), false, 2); + // 添加节点 + return baseDao.add(newNode); + } + + public int deleteSubTree(Long relateId, NODE node) { + if (null == node) { + return 0; + } + if (!relateId.equals(node.getRelateId())) { + return 0; + } + Long deleteVersion = System.currentTimeMillis(); + + String updateSql = "UPDATE " + baseDao.getTableName() + + " SET validate = #{validate} , delete_version = #{deleteVersion} WHERE" + + " relate_id = #{relateId} AND lft >= #{lft} AND rgt <= #{rgt} AND validate = #{validate}"; + int nodeCount = baseDao.updateByCompleteSql( + updateSql, ValidateEnum.INVALID.getValue(), deleteVersion, + node.getRelateId(), node.getLft(), node.getRgt(), ValidateEnum.VALID.getValue()); + // 更新右边节点的left和right + return decreaseNodesLeftAndRight(node.getRelateId(), node.getRgt(), node.getRgt(), nodeCount * 2); + } + + public List listParent(Long relateId, NODE node, TreeQueryOption option) { + StringBuilder condition = new StringBuilder(); + List params = new LinkedList<>(); + params.add(relateId); + condition + .append("relate_id = #{relateId}") + .append(" AND ").append(getParentLeftRange(node, option, params)) + .append(" AND ").append(getParentRightRange(node, option, params)) + .append(" AND validate = #{validate} "); + params.add(ValidateEnum.VALID.getValue()); + condition.append(getOrderBy(option)); + return baseDao.listByCondition(condition.toString(), params.toArray()); + } + + public Page page(Long relateId, Integer pageIndex, Integer pageSize, String orderCondition) { + String condition = "relate_id = #{relateId} AND validate = #{validate}"; + if (StringUtils.isNotBlank(orderCondition)) { + condition += " order by " + orderCondition; + } + return baseDao.pageByCondition(pageIndex, pageSize, condition, relateId, ValidateEnum.VALID.getValue()); + } + + public boolean updateNodeById(NODE node) { + return baseDao.updateById(node); + } + + public NODE getMaxDepthSubLeaf(Long relateId, NODE node) { + String condition = " relate_id = #{relateId} AND lft >= #{lft} AND rgt <= #{rgt} AND validate = #{validate} ORDER BY depth DESC"; + return baseDao.getOneByCondition(condition, relateId, node.getLft(), node.getRgt(), ValidateEnum.VALID.getValue()); + } + + public int updateSubTreeBySql(String updateSql, Long relateId, Long nodeId, TreeQueryOption option) { + NODE node = getById(nodeId); + if (node == null) { + return 0; + } + + List params = new LinkedList<>(); + String sql = "UPDATE " + baseDao.getTableName() + " " + updateSql + + " WHERE " + getChildrenLeftRange(node, option, params) + + " AND " + getChildrenRightRange(node, option, params) + + " AND relate_id = #{relateId} AND validate = #{validate}"; + params.add(relateId); + params.add(ValidateEnum.VALID.getValue()); + return baseDao.updateByCompleteSql(sql, params.toArray()); + } + + public NODE getFarRightNode(Long relateId, Long parentId) { + String condition = "relate_id = #{relateId} AND parent_id = #{parentId} AND validate = #{validate} ORDER BY lft DESC limit 1"; + return baseDao.getOneByCondition(condition, relateId, parentId, ValidateEnum.VALID.getValue()); + } + + public void moveNodeToFarRight(Long nodeId, Integer nodeCount, NODE parent) { + NODE node = getById(nodeId); + // 1.更新节点及所有子节点的relateId + Long tempRelateId = nodeId * -1; + StringBuilder set = new StringBuilder(); + set.append("SET relate_id = ").append(tempRelateId); + updateSubTreeBySql(set.toString(), parent.getRelateId(), nodeId, TreeQueryOption.instance().queryIncludeSelf()); + // 2.右边的节点更新left和right + set.delete(0, set.length()); + set.append("UPDATE ") + .append(baseDao.getTableName()).append(" ") + .append("SET lft = lft - ").append(nodeCount * 2) + .append(", rgt = rgt - ").append(nodeCount * 2) + .append(" WHERE lft > #{lft} AND rgt < #{rgt} AND relate_id = #{relateId} AND validate = #{validate}"); + baseDao.updateByCompleteSql(set.toString(), node.getRgt(), parent.getRgt(), parent.getRelateId(), ValidateEnum.VALID.getValue()); + // 3.更新节点及所有子节点的left和right,恢复relateId + set.delete(0, set.length()); + NODE farRightNode = getFarRightNode(parent.getRelateId(), parent.getId()); + int increment = farRightNode.getRgt() - node.getLft() + 1; + updateLRAndDepthAndRelateIdWithRelateId(tempRelateId, parent.getRelateId(), increment, 0); + } + + public int updateLeftWithRang(Long relateId, int diff, Integer minLeft, boolean includeMinLeft, Integer maxLeft, boolean includeMaxLeft) { + String sql = "UPDATE " + baseDao.getTableName() + " " + + "SET lft = lft + " + diff + " WHERE lft " + + (includeMinLeft ? ">=" : ">") + " " + minLeft + " AND lft " + + (includeMaxLeft ? "<=" : "<") + " " + maxLeft + + " AND relate_id = #{relateId} AND validate = #{validate}"; + return baseDao.updateByCompleteSql(sql, relateId, ValidateEnum.VALID.getValue()); + } + + public int updateRightWithRang(Long relateId, int diff, Integer minRight, boolean includeMinRight, + Integer maxRight, boolean includeMaxRight) { + String sql = "UPDATE " + baseDao.getTableName() + " " + + "SET rgt = rgt + " + diff + " WHERE rgt " + + (includeMinRight ? ">=" : ">") + " " + minRight + " AND rgt " + + (includeMaxRight ? "<=" : "<") + " " + maxRight + + " AND relate_id = #{relateId} AND validate = #{validate}"; + return baseDao.updateByCompleteSql(sql, relateId, ValidateEnum.VALID.getValue()); + } + + public int updateLRAndDepthAndRelateIdWithRelateId(Long oldRelateId, Long newRelateId, int lrDiff, int depthDiff) { + String sql = "UPDATE " + baseDao.getTableName() + " " + + "SET relate_id = #{relateId}" + ", lft = lft + " + lrDiff + + ", rgt = rgt + " + lrDiff + + ", depth = depth + " + depthDiff + + " WHERE relate_id = #{relateId} AND validate = #{validate}"; + return baseDao.updateByCompleteSql(sql, newRelateId, oldRelateId, ValidateEnum.VALID.getValue()); + } + + public Page pageByParent(Long parentId, Integer pageIndex, Integer pageSize) { + return baseDao.pageByCondition(pageIndex, pageSize, "parent_id = #{parentId} order by lft", parentId); + } + + public int updateParentId(List nodeIds, Long parentId) { + String sql = "UPDATE " + baseDao.getTableName() + " SET parent_id = #{parentId} WHERE " + SQLUtil.buidInClause("id", Long.class, nodeIds); + return baseDao.updateByCompleteSql(sql, parentId); + } + + public List listByParent(Long parentId) { + String condition = "parent_id = #{parentId} AND validate = #{validate} order by lft ASC"; + return baseDao.listByCondition(condition, parentId, ValidateEnum.VALID.getValue()); + } + + public List listByParent(Long parentId, Integer levelCount) { + NODE parent = getById(parentId); + if (null == parent) { + return Collections.emptyList(); + } + int depth = parent.getDepth() + levelCount; + String condition = "relate_id = #{relateId} AND lft > #{lft} AND rgt < #{rgt} AND depth <= #{depth} AND validate = #{validate} order by lft ASC"; + return baseDao.listByCondition(condition, parent.getRelateId(), parent.getLft(), parent.getRgt(), depth, ValidateEnum.VALID.getValue()); + } + + public int updateLRAndDepth(Integer left, Integer right, Integer depth, Long nodeId) { + String sql = "UPDATE " + baseDao.getTableName() + " SET lft = #{lft}, rgt = #{rgt}, depth = #{depth} WHERE id = #{id}"; + return baseDao.updateByCompleteSql(sql, left, right, depth, nodeId); + } + + public int updateNodeByIds(NODE updateNode, List nodeIds) { + return baseDao.updateByCondition(updateNode, SQLUtil.buidInClause("id", Long.class, nodeIds)); + } + + private String getParentLeftRange(NODE node, TreeQueryOption option, List params) { + params.add(node.getLft()); + switch (option.getSelfIncludeMode()) { + case TreeQueryOption.TREE_QUERY_SELF_INCLUDE: + return " lft <= #{lft} "; + case TreeQueryOption.TREE_QUERY_SELF_EXCLUDE: + return " lft < #{lft} "; + default: + // never goes here + throw new IllegalArgumentException("param option invalid."); + } + } + + private String getParentRightRange(NODE node, TreeQueryOption option, List params) { + params.add(node.getRgt()); + switch (option.getSelfIncludeMode()) { + case TreeQueryOption.TREE_QUERY_SELF_INCLUDE: + return " rgt >= #{rgt} "; + case TreeQueryOption.TREE_QUERY_SELF_EXCLUDE: + return " rgt > #{rgt} "; + default: + // never goes here + throw new IllegalArgumentException("param option invalid."); + } + } + + private String getChildrenLeftRange(NODE node, TreeQueryOption option, List params) { + StringBuilder condition = new StringBuilder(); + params.add(node.getLft()); + switch (option.getSelfIncludeMode()) { + case TreeQueryOption.TREE_QUERY_SELF_INCLUDE: + condition.append(" lft >= #{lft} "); + break; + case TreeQueryOption.TREE_QUERY_SELF_EXCLUDE: + condition.append(" lft > #{lft} "); + break; + } + + return condition.toString(); + } + + private String getChildrenRightRange(NODE node, TreeQueryOption option, List params) { + StringBuilder condition = new StringBuilder(); + params.add(node.getRgt()); + switch (option.getSelfIncludeMode()) { + case TreeQueryOption.TREE_QUERY_SELF_INCLUDE: + condition.append(" rgt <= #{rgt} "); + break; + case TreeQueryOption.TREE_QUERY_SELF_EXCLUDE: + condition.append(" rgt < #{rgt} "); + break; + } + + return condition.toString(); + } + + /** + * 如果是父节点的left和right,则includeRight为true,因为父节点的right值也需要更新; + * 如果是兄弟节点的left和right,则include为false,因为系统节点的right值不需要更新。 + */ + private int increaseNodesLeftAndRight(Long relateId, Integer left, Integer right, boolean includeRight, Integer increment) { + StringBuilder sql = new StringBuilder(); + sql.append("UPDATE ") + .append(baseDao.getTableName()) + .append(" SET lft = lft + ").append(increment) + .append(" WHERE relate_id = #{relateId} AND lft > #{lft} AND validate = #{validate}"); + baseDao.updateByCompleteSql(sql.toString(), relateId, left, ValidateEnum.VALID.getValue()); + sql.delete(0, sql.length()); + sql.append("UPDATE ").append(baseDao.getTableName()) + .append(" SET rgt = rgt + ").append(increment) + .append(" WHERE relate_id = #{relateId} AND rgt ") + .append(includeRight ? ">=" : ">") + .append(" #{rgt} AND validate = #{validate}"); + return baseDao.updateByCompleteSql(sql.toString(), relateId, right, ValidateEnum.VALID.getValue()); + } + + private int decreaseNodesLeftAndRight(Long relateId, Integer left, Integer right, Integer decrement) { + StringBuilder sql = new StringBuilder(); + sql.append("UPDATE ") + .append(baseDao.getTableName()) + .append(" set lft = lft - ").append(decrement) + .append(" WHERE relate_id = #{relateId} AND lft > #{lft} AND validate = #{validate}"); + baseDao.updateByCompleteSql(sql.toString(), relateId, left, ValidateEnum.VALID.getValue()); + sql.delete(0, sql.length()); + sql.append("UPDATE ").append(baseDao.getTableName()) + .append(" set rgt = rgt - ").append(decrement) + .append(" WHERE relate_id = #{relateId} AND rgt > #{rgt} AND validate = #{validate}"); + return baseDao.updateByCompleteSql(sql.toString(), relateId, right, ValidateEnum.VALID.getValue()); + } + + private String getOrderBy(TreeQueryOption option) { + switch (option.getDepthOrder()) { + case TreeQueryOption.TREE_QUERY_DEPTH_ORDER_ROOT_2_LEAF: + return " ORDER BY lft ASC"; + case TreeQueryOption.TREE_QUERY_DEPTH_ORDER_LEAF_2_ROOT: + return " ORDER BY lft DESC"; + default: + // never goes here + throw new RuntimeException("查询错误"); + } + } + +} \ No newline at end of file diff --git a/commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/EmptyTreeOperationAware.java b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/EmptyTreeOperationAware.java new file mode 100644 index 0000000..745adec --- /dev/null +++ b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/EmptyTreeOperationAware.java @@ -0,0 +1,22 @@ +package com.schbrain.common.module.tree.event; + +import com.schbrain.common.module.tree.TreeNode; + +import java.util.List; + +/** + * Created by hzchengyi on 2019/1/21. + */ +public class EmptyTreeOperationAware implements TreeOperationAware { + + @Override + public void before(TreeOperationEvent event, List nodes) { + + } + + @Override + public void after(TreeOperationEvent event, List nodes) { + + } + +} \ No newline at end of file diff --git a/commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/TreeOperationAware.java b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/TreeOperationAware.java new file mode 100644 index 0000000..5061874 --- /dev/null +++ b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/TreeOperationAware.java @@ -0,0 +1,22 @@ +package com.schbrain.common.module.tree.event; + +import com.schbrain.common.module.tree.TreeNode; + +import java.util.List; + +/** + * Created by hzchengyi on 2019/1/21. + */ +public interface TreeOperationAware { + + /** + * 执行操作之前调用 + */ + void before(TreeOperationEvent event, List nodes); + + /** + * 执行操作之后调用 + */ + void after(TreeOperationEvent event, List nodes); + +} \ No newline at end of file diff --git a/commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/TreeOperationEvent.java b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/TreeOperationEvent.java new file mode 100644 index 0000000..85aa659 --- /dev/null +++ b/commons/module-tree/src/main/java/com/schbrain/common/module/tree/event/TreeOperationEvent.java @@ -0,0 +1,10 @@ +package com.schbrain.common.module.tree.event; + +/** + * Created by hzchengyi on 2019/1/21. + */ +public enum TreeOperationEvent { + ADD, + DELETE, + MOVE; +} \ No newline at end of file diff --git a/commons/pom.xml b/commons/pom.xml new file mode 100644 index 0000000..7f5bebf --- /dev/null +++ b/commons/pom.xml @@ -0,0 +1,24 @@ + + + + 4.0.0 + + + com.schbrain.framework + schbrain-parent + ${revision} + + + commons + pom + + + common + web-common + common-util + module-tree + + + \ No newline at end of file diff --git a/commons/web-common/pom.xml b/commons/web-common/pom.xml new file mode 100644 index 0000000..93f10eb --- /dev/null +++ b/commons/web-common/pom.xml @@ -0,0 +1,58 @@ + + + + 4.0.0 + + + com.schbrain.framework + commons + ${revision} + + + com.schbrain.common + web-common + + + + com.schbrain.common + common-util + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-configuration-processor + + + + org.springframework + spring-tx + + + + + org.springframework.boot + spring-boot-starter-aop + true + + + org.springframework.boot + spring-boot-starter-data-redis + true + + + com.alibaba + easyexcel + true + + + + \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/WebCommonAutoConfiguration.java b/commons/web-common/src/main/java/com/schbrain/common/web/WebCommonAutoConfiguration.java new file mode 100644 index 0000000..dbfd23b --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/WebCommonAutoConfiguration.java @@ -0,0 +1,100 @@ +package com.schbrain.common.web; + +import com.schbrain.common.web.argument.BodyParamArgumentResolverWebMvcConfigurer; +import com.schbrain.common.web.exception.*; +import com.schbrain.common.web.log.RequestLoggingFilter; +import com.schbrain.common.web.properties.WebProperties; +import com.schbrain.common.web.result.ResponseBodyHandler; +import com.schbrain.common.web.servlet.*; +import com.schbrain.common.web.support.authentication.AuthenticationInterceptor; +import com.schbrain.common.web.support.authentication.Authenticator; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.boot.autoconfigure.condition.*; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +/** + * @author liaozan + * @since 2021/11/19 + */ +@AutoConfiguration +@ConditionalOnWebApplication +@EnableConfigurationProperties(WebProperties.class) +public class WebCommonAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(Authenticator.class) + public AuthenticationInterceptor defaultAuthenticationInterceptor(Authenticator authenticator) { + return new AuthenticationInterceptor(authenticator); + } + + @Bean + @ConditionalOnMissingBean + public GlobalExceptionHandler defaultGlobalExceptionHandler() { + return new DefaultGlobalExceptionHandler(); + } + + @Bean + @ConditionalOnMissingBean + public ExceptionHandlerWebMcvConfigurer defaultExceptionHandlerWebMcvConfigurer(WebProperties webProperties, GlobalExceptionHandler exceptionHandler) { + return new ExceptionHandlerWebMcvConfigurer(webProperties, exceptionHandler); + } + + @Bean + @ConditionalOnMissingBean + public BodyParamArgumentResolverWebMvcConfigurer defaultBodyParamArgumentResolverWebMvcConfigurer() { + return new BodyParamArgumentResolverWebMvcConfigurer(); + } + + @Bean + @Lazy + @ConditionalOnMissingBean + public RestTemplate restTemplate(ObjectProvider restTemplateBuilder) { + RestTemplateBuilder builder = restTemplateBuilder.getIfAvailable(); + if (builder == null) { + return new RestTemplate(); + } + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + public ResponseBodyHandler defaultResponseBodyHandler(WebProperties properties, BeanFactory beanFactory) { + List basePackages = AutoConfigurationPackages.get(beanFactory); + return new ResponseBodyHandler(properties, basePackages); + } + + @Bean + @ConditionalOnMissingBean + public TraceIdInitializeServletListener traceIdInitializeServletListener() { + return new TraceIdInitializeServletListener(); + } + + @Bean + @ConditionalOnMissingBean + public CharacterEncodingServletContextInitializer characterEncodingServletContextInitializer(WebProperties webProperties) { + return new CharacterEncodingServletContextInitializer(webProperties.getEncoding()); + } + + @Bean + @ConditionalOnMissingBean + public RequestLoggingFilter requestLoggingFilter(WebProperties properties) { + return new RequestLoggingFilter(properties); + } + + @Bean + @ConditionalOnMissingBean + public AllowAllCorsConfigurer allowAllCorsConfigurer() { + return new AllowAllCorsConfigurer(); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/annotation/BodyParam.java b/commons/web-common/src/main/java/com/schbrain/common/web/annotation/BodyParam.java new file mode 100644 index 0000000..54d4f5c --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/annotation/BodyParam.java @@ -0,0 +1,41 @@ +package com.schbrain.common.web.annotation; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ValueConstants; + +import java.lang.annotation.*; + +/** + * @author liaozan + * @see RequestParam + * @since 2022-12-02 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface BodyParam { + + /** + * @see RequestParam#value() + */ + @AliasFor("name") + String value() default ""; + + /** + * @see RequestParam#name() + */ + @AliasFor("value") + String name() default ""; + + /** + * @see RequestParam#required() + */ + boolean required() default true; + + /** + * @see RequestParam#defaultValue() + */ + String defaultValue() default ValueConstants.DEFAULT_NONE; + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/annotation/ResponseWrapOption.java b/commons/web-common/src/main/java/com/schbrain/common/web/annotation/ResponseWrapOption.java new file mode 100644 index 0000000..37392cc --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/annotation/ResponseWrapOption.java @@ -0,0 +1,25 @@ +package com.schbrain.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @author liaozan + * @see com.schbrain.common.web.result.ResponseDTO + * @since 2022/8/29 + */ +@Documented +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ResponseWrapOption { + + /** + * 是否忽略返回值处理 + */ + boolean ignore() default true; + + /** + * 是否忽略异常处理 + */ + boolean ignoreException() default true; + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/argument/BodyParamArgumentResolverWebMvcConfigurer.java b/commons/web-common/src/main/java/com/schbrain/common/web/argument/BodyParamArgumentResolverWebMvcConfigurer.java new file mode 100644 index 0000000..a68cbff --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/argument/BodyParamArgumentResolverWebMvcConfigurer.java @@ -0,0 +1,19 @@ +package com.schbrain.common.web.argument; + +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +/** + * @author liaozan + * @since 2022-12-02 + */ +public class BodyParamArgumentResolverWebMvcConfigurer implements WebMvcConfigurer { + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new BodyParamMethodArgumentResolver()); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/argument/BodyParamMethodArgumentResolver.java b/commons/web-common/src/main/java/com/schbrain/common/web/argument/BodyParamMethodArgumentResolver.java new file mode 100644 index 0000000..61d7559 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/argument/BodyParamMethodArgumentResolver.java @@ -0,0 +1,74 @@ +package com.schbrain.common.web.argument; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.schbrain.common.util.JacksonUtils; +import com.schbrain.common.web.annotation.BodyParam; +import lombok.Setter; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author liaozan + * @since 2022-12-02 + */ +@Setter +public class BodyParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + private static final String METHOD_BODY_CACHE_KEY = BodyParamMethodArgumentResolver.class.getName() + ".bodyParamCache"; + + private ObjectMapper objectMapper; + + public BodyParamMethodArgumentResolver() { + this(JacksonUtils.getObjectMapper()); + } + + public BodyParamMethodArgumentResolver(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(BodyParam.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + BodyParam annotation = parameter.getParameterAnnotation(BodyParam.class); + Assert.notNull(annotation, "annotation should never be null"); + return new NamedValueInfo(annotation.name(), annotation.required(), annotation.defaultValue()); + } + + @Override + @Nullable + protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + JsonNode paramNode = getParamNode(request); + JsonNode parameterValue = paramNode.get(name); + if (parameterValue == null || parameterValue.isNull()) { + return null; + } + Class parameterType = parameter.getParameterType(); + return objectMapper.convertValue(parameterValue, parameterType); + } + + private JsonNode getParamNode(NativeWebRequest nativeWebRequest) throws IOException { + HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class); + Assert.state(request != null, "request must not be null"); + JsonNode paramNode = (JsonNode) request.getAttribute(METHOD_BODY_CACHE_KEY); + if (paramNode == null) { + InputStream inputStream = StreamUtils.nonClosing(request.getInputStream()); + paramNode = objectMapper.readTree(inputStream); + request.setAttribute(METHOD_BODY_CACHE_KEY, paramNode); + } + return paramNode; + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/exception/DefaultGlobalExceptionHandler.java b/commons/web-common/src/main/java/com/schbrain/common/web/exception/DefaultGlobalExceptionHandler.java new file mode 100644 index 0000000..ff0a986 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/exception/DefaultGlobalExceptionHandler.java @@ -0,0 +1,250 @@ +package com.schbrain.common.web.exception; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.EnvUtils; +import com.schbrain.common.web.result.ResponseDTO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.validator.internal.engine.path.PathImpl; +import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.util.ClassUtils; +import org.springframework.validation.*; +import org.springframework.web.*; +import org.springframework.web.bind.*; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.AsyncRequestTimeoutException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.sql.SQLException; +import java.util.*; + +import static com.schbrain.common.constants.ResponseCodeConstants.*; + +/** + * @author liaozan + * @since 2019/10/14 + */ +@Slf4j +@ResponseBody +@ResponseStatus(HttpStatus.OK) +public class DefaultGlobalExceptionHandler implements GlobalExceptionHandler { + + /************************************* Base Exception Handing *************************************/ + @ExceptionHandler(BaseException.class) + public ResponseDTO handleBaseException(BaseException ex) { + return loggingThenBuildResponse(ex, ex.getCode()); + } + + /************************************* Common Exception Handing *************************************/ + @ExceptionHandler(Throwable.class) + public ResponseDTO handleAll(Throwable ex) { + return loggingThenBuildResponse(ex, SERVER_ERROR); + } + + @ExceptionHandler(NullPointerException.class) + public ResponseDTO handleNullPointerException(NullPointerException ex) { + return loggingThenBuildResponse(ex, SERVER_ERROR); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseDTO handleIllegalArgumentException(IllegalArgumentException ex) { + return loggingThenBuildResponse(ex, SERVER_ERROR); + } + + @ExceptionHandler(IllegalStateException.class) + public ResponseDTO handleIllegalStateException(IllegalStateException ex) { + return loggingThenBuildResponse(ex, SERVER_ERROR); + } + + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseDTO handleNoHandlerFoundException(NoHandlerFoundException ex) { + return loggingThenBuildResponse(ex, PARAM_INVALID); + } + + @ExceptionHandler(AsyncRequestTimeoutException.class) + public ResponseDTO handleAsyncRequestTimeoutException(AsyncRequestTimeoutException ex) { + return loggingThenBuildResponse(ex, SERVER_ERROR); + } + + /************************************* SQL Exception Handing *************************************/ + @ExceptionHandler(SQLException.class) + public ResponseDTO handleSQLException(SQLException ex) { + return loggingThenBuildResponse(ex, SERVER_ERROR); + } + + @ExceptionHandler(DataAccessException.class) + public ResponseDTO handleDataAccessException(DataAccessException ex) { + return loggingThenBuildResponse(ex, SERVER_ERROR); + } + + /************************************* Http Request Exception Handing *************************************/ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseDTO handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex) { + String errorMsg = StrUtil.format("不支持该HTTP方法: {}, 请使用 {}", ex.getMethod(), Arrays.toString(ex.getSupportedMethods())); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public ResponseDTO handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException ex) { + String errorMsg = StrUtil.format("不支持该媒体类型: {}, 请使用 {}", ex.getContentType(), ex.getSupportedMediaTypes()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(HttpMediaTypeNotAcceptableException.class) + public ResponseDTO handleHttpMediaTypeNotAcceptableException(HttpMediaTypeNotAcceptableException ex) { + String errorMsg = StrUtil.format("不支持的媒体类型, 请使用 {}", ex.getSupportedMediaTypes()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + /************************************* Method Parameter Exception Handing *************************************/ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseDTO handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { + String errorMsg = StrUtil.format("参数解析失败, {}", ex.getMessage()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseDTO handlerMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) { + Object value = ex.getValue(); + String variableName = ex.getName(); + Class requiredTypeClass = ex.getRequiredType(); + String requiredType = ClassUtils.getQualifiedName(requiredTypeClass == null ? Object.class : requiredTypeClass); + String providedType = ClassUtils.getDescriptiveType(value); + String errorMsg = StrUtil.format("参数类型不匹配, 参数名: {}, 需要: {}, 传入: {} 的 {}", variableName, requiredType, providedType, value); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(MissingPathVariableException.class) + public ResponseDTO handleMissingPathVariableException(MissingPathVariableException ex) { + String errorMsg = StrUtil.format("丢失路径参数, 参数名: {}, 参数类型: {}", ex.getVariableName(), ex.getParameter().getParameterType()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(MissingRequestCookieException.class) + public ResponseDTO handleMissingRequestCookieException(MissingRequestCookieException ex) { + String errorMsg = StrUtil.format("丢失Cookie参数, 参数名: {}, 参数类型: {}", ex.getCookieName(), ex.getParameter().getParameterType()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseDTO handleMissingRequestHeaderException(MissingRequestHeaderException ex) { + String errorMsg = StrUtil.format("丢失Header参数, 参数名: {}, 参数类型: {}", ex.getHeaderName(), ex.getParameter().getParameterType()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(MissingServletRequestPartException.class) + public ResponseDTO handleMissingServletRequestPartException(MissingServletRequestPartException ex) { + String errorMsg = StrUtil.format("丢失参数: {}", ex.getRequestPartName()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseDTO handleServletRequestParameterException(MissingServletRequestParameterException ex) { + String errorMsg = StrUtil.format("丢失Query参数, 参数名: {}, 参数类型: {}", ex.getParameterName(), ex.getParameterType()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(ServletRequestBindingException.class) + public ResponseDTO handleServletRequestBindingException(ServletRequestBindingException ex) { + String errorMsg = StrUtil.format("参数绑定失败: {}", ex.getMessage()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + /************************************* Parameter Binding Exception Handing *************************************/ + @ExceptionHandler(BindException.class) + public ResponseDTO handleBindException(BindException ex) { + String errorMsg = buildBindingErrorMsg(ex.getBindingResult()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseDTO handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + String errorMsg = buildBindingErrorMsg(ex.getBindingResult()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseDTO handleConstraintViolationException(ConstraintViolationException ex) { + String errorMsg = buildBindingErrorMsg(ex.getConstraintViolations()); + log.error(errorMsg); + return buildResponse(ex, PARAM_INVALID, errorMsg); + } + + protected ResponseDTO loggingThenBuildResponse(Throwable throwable, int errorCode) { + Throwable rootCause = ExceptionUtil.getRootCause(throwable); + logError(rootCause); + return buildResponse(rootCause, errorCode, rootCause.getMessage()); + } + + protected ResponseDTO buildResponse(Throwable throwable, int errorCode, String message) { + boolean production = EnvUtils.isProduction(); + ResponseDTO responseDTO = getExceptionResponseMapping(throwable, production); + if (responseDTO != null) { + return responseDTO; + } + if (production) { + return ResponseDTO.error("系统错误", errorCode); + } + if (StringUtils.isBlank(message)) { + return ResponseDTO.error("系统错误", errorCode); + } + return ResponseDTO.error(message, errorCode); + } + + protected ResponseDTO getExceptionResponseMapping(Throwable throwable, boolean production) { + return null; + } + + protected String buildBindingErrorMsg(BindingResult bindingResult) { + String prefix = "参数验证失败: "; + StringJoiner joiner = new StringJoiner(", "); + for (ObjectError error : bindingResult.getAllErrors()) { + String errorMessage = Optional.ofNullable(error.getDefaultMessage()).orElse("验证失败"); + String source; + if (error instanceof FieldError) { + source = ((FieldError) error).getField(); + } else { + source = error.getObjectName(); + } + joiner.add(source + " " + errorMessage); + } + return prefix + joiner; + } + + protected String buildBindingErrorMsg(Set> constraintViolations) { + String prefix = "参数验证失败: "; + StringJoiner joiner = new StringJoiner(", "); + for (ConstraintViolation violation : constraintViolations) { + PathImpl propertyPath = (PathImpl) violation.getPropertyPath(); + joiner.add(propertyPath.asString() + " " + violation.getMessage()); + } + return prefix + joiner; + } + + protected void logError(Throwable throwable) { + String exMsg = ExceptionUtil.getMessage(throwable); + log.error(exMsg, throwable); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/exception/ExceptionHandlerWebMcvConfigurer.java b/commons/web-common/src/main/java/com/schbrain/common/web/exception/ExceptionHandlerWebMcvConfigurer.java new file mode 100644 index 0000000..5334b6b --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/exception/ExceptionHandlerWebMcvConfigurer.java @@ -0,0 +1,58 @@ +package com.schbrain.common.web.exception; + +import com.schbrain.common.web.properties.WebProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; + +import java.util.List; + +/** + * @author liaozan + * @since 2022/8/29 + */ +@Slf4j +public class ExceptionHandlerWebMcvConfigurer implements WebMvcConfigurer { + + private final WebProperties webProperties; + private final GlobalExceptionHandler globalExceptionHandler; + + public ExceptionHandlerWebMcvConfigurer(WebProperties webProperties, GlobalExceptionHandler globalExceptionHandler) { + this.webProperties = webProperties; + this.globalExceptionHandler = globalExceptionHandler; + } + + @Override + public void extendHandlerExceptionResolvers(List resolvers) { + if (!webProperties.isEnableGlobalExceptionHandler()) { + log.warn("Global exception handing is disabled"); + return; + } + + ExceptionHandlerExceptionResolver adviceExceptionResolver = null; + for (HandlerExceptionResolver resolver : resolvers) { + if (resolver instanceof ExceptionHandlerExceptionResolver) { + adviceExceptionResolver = (ExceptionHandlerExceptionResolver) resolver; + break; + } + } + + if (adviceExceptionResolver == null) { + log.warn("ExceptionHandlerExceptionResolver is not exist, ignore global exception handing"); + return; + } + + addGlobalExceptionResolver(resolvers, adviceExceptionResolver); + } + + protected void addGlobalExceptionResolver(List resolvers, ExceptionHandlerExceptionResolver adviceExceptionResolver) { + int index = resolvers.indexOf(adviceExceptionResolver) + 1; + resolvers.add(index, createGlobalExceptionResolver(adviceExceptionResolver)); + } + + protected GlobalExceptionResolver createGlobalExceptionResolver(ExceptionHandlerExceptionResolver adviceExceptionResolver) { + return new GlobalExceptionResolver(adviceExceptionResolver, webProperties, globalExceptionHandler); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/exception/GlobalExceptionHandler.java b/commons/web-common/src/main/java/com/schbrain/common/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..4138a43 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,11 @@ +package com.schbrain.common.web.exception; + +/** + * 标记接口 + * + * @author liaozan + * @since 2022/10/26 + */ +public interface GlobalExceptionHandler { + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/exception/GlobalExceptionResolver.java b/commons/web-common/src/main/java/com/schbrain/common/web/exception/GlobalExceptionResolver.java new file mode 100644 index 0000000..1d92eff --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/exception/GlobalExceptionResolver.java @@ -0,0 +1,139 @@ +package com.schbrain.common.web.exception; + +import com.schbrain.common.web.annotation.ResponseWrapOption; +import com.schbrain.common.web.properties.WebProperties; +import com.schbrain.common.web.utils.HandlerMethodAnnotationUtils; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; +import org.springframework.web.method.support.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author liaozan + * @since 2022/8/30 + */ +@Slf4j +@Data +@EqualsAndHashCode(callSuper = true) +public class GlobalExceptionResolver extends AbstractHandlerMethodExceptionResolver { + + private final WebProperties webProperties; + private final HandlerMethodArgumentResolverComposite argumentResolverComposite; + private final HandlerMethodReturnValueHandlerComposite returnValueHandlerComposite; + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64); + + private GlobalExceptionHandler exceptionHandler; + private ExceptionHandlerMethodResolver handlerMethodResolver; + + public GlobalExceptionResolver(ExceptionHandlerExceptionResolver handlerMethodResolver, WebProperties webProperties, + GlobalExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + this.webProperties = webProperties; + this.handlerMethodResolver = new ExceptionHandlerMethodResolver(exceptionHandler.getClass()); + this.argumentResolverComposite = handlerMethodResolver.getArgumentResolvers(); + this.returnValueHandlerComposite = handlerMethodResolver.getReturnValueHandlers(); + } + + @Override + protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) { + if (!webProperties.isWrapResponse()) { + return false; + } + + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + ResponseWrapOption responseWrapOption = HandlerMethodAnnotationUtils.getAnnotation(handlerMethod, ResponseWrapOption.class); + + if (responseWrapOption == null) { + return true; + } + return Boolean.FALSE.equals(responseWrapOption.ignoreException()); + } + return true; + } + + @Override + protected final ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerMethod handlerMethod, Exception exception) { + ServletInvocableHandlerMethod exceptionHandlerMethod = createExceptionHandlerMethod(exception, handlerMethod, exceptionHandler); + if (exceptionHandlerMethod == null) { + return null; + } + + ServletWebRequest webRequest = new ServletWebRequest(request, response); + return doResolveException(webRequest, exceptionHandlerMethod, getArguments(exception, handlerMethod)); + } + + protected final ModelAndView doResolveException(ServletWebRequest webRequest, ServletInvocableHandlerMethod targetMethod, Object[] arguments) { + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); + try { + targetMethod.invokeAndHandle(webRequest, mavContainer, arguments); + } catch (Exception e) { + log.warn("Failure in @ExceptionHandler " + targetMethod, e); + return null; + } + if (mavContainer.isRequestHandled()) { + return new ModelAndView(); + } + return null; + } + + protected ServletInvocableHandlerMethod createExceptionHandlerMethod(Exception exception, @Nullable HandlerMethod handlerMethod, Object handler) { + Method targetMethod = resolveTargetMethod(exception, handlerMethod); + if (targetMethod == null) { + return null; + } + ServletInvocableHandlerMethod exceptionHandlerMethod = new ServletInvocableHandlerMethod(handler, targetMethod); + exceptionHandlerMethod.setHandlerMethodArgumentResolvers(argumentResolverComposite); + exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(returnValueHandlerComposite); + return exceptionHandlerMethod; + } + + protected Method resolveTargetMethod(Exception exception, @Nullable HandlerMethod handlerMethod) { + Method resolvedMethod = null; + if (handlerMethod != null) { + Class handlerType = handlerMethod.getBeanType(); + resolvedMethod = getHandlerMethodResolver(handlerType).resolveMethod(exception); + } + if (resolvedMethod == null) { + resolvedMethod = handlerMethodResolver.resolveMethod(exception); + } + return resolvedMethod; + } + + private ExceptionHandlerMethodResolver getHandlerMethodResolver(Class handlerType) { + return exceptionHandlerCache.computeIfAbsent(handlerType, key -> new ExceptionHandlerMethodResolver(handlerType)); + } + + /** + * copy from spring + */ + private Object[] getArguments(Exception exception, HandlerMethod handlerMethod) { + List exceptions = new ArrayList<>(); + Throwable exToExpose = exception; + while (exToExpose != null) { + exceptions.add(exToExpose); + Throwable cause = exToExpose.getCause(); + exToExpose = (cause != exToExpose ? cause : null); + } + Object[] arguments = new Object[exceptions.size() + 1]; + exceptions.toArray(arguments); + arguments[arguments.length - 1] = handlerMethod; + return arguments; + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/log/RequestLoggingFilter.java b/commons/web-common/src/main/java/com/schbrain/common/web/log/RequestLoggingFilter.java new file mode 100644 index 0000000..2fda0ce --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/log/RequestLoggingFilter.java @@ -0,0 +1,109 @@ +package com.schbrain.common.web.log; + +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.ArrayUtil; +import com.schbrain.common.web.properties.WebProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.servlet.filter.OrderedFilter; +import org.springframework.core.Ordered; +import org.springframework.util.StringUtils; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.WebUtils; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +/** + * 请求日志拦截器 + */ +@Slf4j +public class RequestLoggingFilter extends OncePerRequestFilter implements OrderedFilter { + + private final WebProperties webProperties; + + public RequestLoggingFilter(WebProperties webProperties) { + this.webProperties = webProperties; + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + if (shouldSkip(request)) { + chain.doFilter(request, response); + return; + } + + request = wrapRequestIfRequired(request); + long startTime = System.currentTimeMillis(); + try { + chain.doFilter(request, response); + } finally { + long endTime = System.currentTimeMillis(); + log.info(buildLogContent(request, startTime, endTime)); + } + } + + protected boolean shouldSkip(HttpServletRequest request) { + if (!webProperties.isEnableRequestLogging()) { + return true; + } + return CorsUtils.isPreFlightRequest(request); + } + + protected String buildLogContent(HttpServletRequest request, long startTime, long endTime) { + long cost = endTime - startTime; + String method = request.getMethod(); + String requestUri = request.getRequestURI(); + String queryString = request.getQueryString(); + String body = getRequestBody(request); + StringBuilder builder = new StringBuilder(); + builder.append("requestUri: ").append(method).append(StrPool.C_SPACE).append(requestUri); + if (StringUtils.hasText(queryString)) { + builder.append(", queryString: ").append(queryString); + } + if (StringUtils.hasText(body)) { + builder.append(", body: ").append(body); + } + builder.append(", startTime: ").append(startTime); + builder.append(", endTime: ").append(endTime); + builder.append(", cost: ").append(cost).append("ms"); + return builder.toString(); + } + + protected String getRequestBody(HttpServletRequest request) { + ContentCachingRequestWrapper nativeRequest = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); + if (nativeRequest == null) { + return null; + } + byte[] content = nativeRequest.getContentAsByteArray(); + if (ArrayUtil.isEmpty(content)) { + return null; + } + String charset = nativeRequest.getCharacterEncoding(); + try { + return new String(content, charset); + } catch (UnsupportedEncodingException e) { + log.warn("unsupported charset {} detect during convert body to String", charset, e); + return null; + } + } + + protected HttpServletRequest wrapRequestIfRequired(HttpServletRequest request) { + if (request instanceof ContentCachingRequestWrapper) { + return request; + } else { + return new ContentCachingRequestWrapper(request); + } + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/properties/WebProperties.java b/commons/web-common/src/main/java/com/schbrain/common/web/properties/WebProperties.java new file mode 100644 index 0000000..e6b0838 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/properties/WebProperties.java @@ -0,0 +1,49 @@ +package com.schbrain.common.web.properties; + +import com.schbrain.common.util.support.ConfigurableProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.nio.charset.StandardCharsets; + +/** + * @author liaozan + * @since 2022/8/29 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ConfigurationProperties(prefix = "schbrain.web") +public class WebProperties extends ConfigurableProperties { + + /** + * whether to enable the request logging + */ + private boolean enableRequestLogging = true; + + /** + * whether to enable the response wrap + */ + private boolean wrapResponse = true; + + /** + * whether to enable the global exception handing + */ + private boolean enableGlobalExceptionHandler = true; + + /** + * encoding for request/response + */ + private String encoding = StandardCharsets.UTF_8.name(); + + /** + * authenticationVariableName for login auth + */ + private String authenticationVariableName = "token"; + + @Override + public String getDefaultNamespace() { + return "web-common"; + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/result/ResponseBodyHandler.java b/commons/web-common/src/main/java/com/schbrain/common/web/result/ResponseBodyHandler.java new file mode 100644 index 0000000..a01f0e5 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/result/ResponseBodyHandler.java @@ -0,0 +1,81 @@ +package com.schbrain.common.web.result; + +import com.schbrain.common.web.annotation.ResponseWrapOption; +import com.schbrain.common.web.properties.WebProperties; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author liaozan + * @since 2021/10/15 + */ +@RestControllerAdvice +public class ResponseBodyHandler implements ResponseBodyAdvice { + + private final WebProperties webProperties; + private final List basePackages; + private final Map methodCache; + + public ResponseBodyHandler(WebProperties webProperties, List basePackages) { + this.webProperties = webProperties; + this.basePackages = basePackages; + this.methodCache = new ConcurrentHashMap<>(); + } + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + if (!webProperties.isWrapResponse()) { + return false; + } + return methodCache.computeIfAbsent(returnType.getMethod(), this::shouldApply); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + return beforeBodyWrite(body); + } + + protected Object beforeBodyWrite(Object body) { + if (body instanceof ResponseDTO) { + return body; + } + if (body == null) { + return ResponseDTO.success(); + } else { + return ResponseDTO.success(body); + } + } + + protected boolean shouldApply(Method targetMethod) { + if (targetMethod == null) { + return false; + } + + Class declaringClass = targetMethod.getDeclaringClass(); + + ResponseWrapOption responseWrapOption = targetMethod.getAnnotation(ResponseWrapOption.class); + if (responseWrapOption == null) { + responseWrapOption = declaringClass.getAnnotation(ResponseWrapOption.class); + } + + if (responseWrapOption != null) { + return !responseWrapOption.ignore(); + } + + String packageName = declaringClass.getPackage().getName(); + return basePackages.stream().anyMatch(packageName::startsWith); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/result/ResponseDTO.java b/commons/web-common/src/main/java/com/schbrain/common/web/result/ResponseDTO.java new file mode 100644 index 0000000..850f14b --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/result/ResponseDTO.java @@ -0,0 +1,59 @@ +package com.schbrain.common.web.result; + +import com.schbrain.common.constants.ResponseActionConstants; +import com.schbrain.common.constants.ResponseCodeConstants; +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.TraceIdUtils; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author liaozan + * @since 2021/10/15 + */ +@Data +public class ResponseDTO implements Serializable { + + private static final long serialVersionUID = 8559474982311419998L; + + private int code; + private int action; + private String message; + private T data; + private String uuid = TraceIdUtils.get(); + + public static ResponseDTO success() { + return success(null); + } + + public static ResponseDTO success(T data) { + ResponseDTO result = new ResponseDTO<>(); + result.setMessage(null); + result.setCode(ResponseCodeConstants.SUCCESS); + result.setAction(ResponseActionConstants.NO_ACTION); + result.setData(data); + return result; + } + + public static ResponseDTO error(String message) { + return error(message, ResponseCodeConstants.SERVER_ERROR); + } + + public static ResponseDTO error(String message, int code) { + return error(message, code, ResponseActionConstants.ALERT); + } + + public static ResponseDTO error(BaseException exception) { + return error(exception.getMessage(), exception.getCode(), exception.getAction()); + } + + public static ResponseDTO error(String message, int code, int action) { + ResponseDTO result = new ResponseDTO<>(); + result.setMessage(message); + result.setCode(code); + result.setAction(action); + return result; + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/servlet/AllowAllCorsConfigurer.java b/commons/web-common/src/main/java/com/schbrain/common/web/servlet/AllowAllCorsConfigurer.java new file mode 100644 index 0000000..320aa0f --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/servlet/AllowAllCorsConfigurer.java @@ -0,0 +1,26 @@ +package com.schbrain.common.web.servlet; + +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.time.Duration; + +/** + * @author liaozan + * @since 2022/11/19 + */ +public class AllowAllCorsConfigurer implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowCredentials(false) + .allowedOrigins(CorsConfiguration.ALL) + .allowedHeaders(CorsConfiguration.ALL) + .allowedMethods(CorsConfiguration.ALL) + .exposedHeaders(CorsConfiguration.ALL) + .maxAge(Duration.ofHours(1).toSeconds()); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/servlet/CharacterEncodingServletContextInitializer.java b/commons/web-common/src/main/java/com/schbrain/common/web/servlet/CharacterEncodingServletContextInitializer.java new file mode 100644 index 0000000..9dd3822 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/servlet/CharacterEncodingServletContextInitializer.java @@ -0,0 +1,25 @@ +package com.schbrain.common.web.servlet; + +import org.springframework.boot.web.servlet.ServletContextInitializer; + +import javax.servlet.ServletContext; + +/** + * @author liaozan + * @since 2022/11/11 + */ +public class CharacterEncodingServletContextInitializer implements ServletContextInitializer { + + private final String encoding; + + public CharacterEncodingServletContextInitializer(String encoding) { + this.encoding = encoding; + } + + @Override + public void onStartup(ServletContext servletContext) { + servletContext.setRequestCharacterEncoding(encoding); + servletContext.setResponseCharacterEncoding(encoding); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/servlet/TraceIdInitializeServletListener.java b/commons/web-common/src/main/java/com/schbrain/common/web/servlet/TraceIdInitializeServletListener.java new file mode 100644 index 0000000..c9f57c8 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/servlet/TraceIdInitializeServletListener.java @@ -0,0 +1,28 @@ +package com.schbrain.common.web.servlet; + +import com.schbrain.common.util.TraceIdUtils; +import org.springframework.web.context.request.RequestContextListener; + +import javax.servlet.ServletRequestEvent; + +/** + * @author liaozan + * @since 2021/12/8 + */ +public class TraceIdInitializeServletListener extends RequestContextListener { + + @Override + public void requestInitialized(ServletRequestEvent event) { + super.requestInitialized(event); + // Make sure the traceId is initialized + TraceIdUtils.get(); + } + + @Override + public void requestDestroyed(ServletRequestEvent event) { + super.requestDestroyed(event); + // Make sure the traceId can be cleared + TraceIdUtils.clear(); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/BaseHandlerInterceptor.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/BaseHandlerInterceptor.java new file mode 100644 index 0000000..2e018d0 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/BaseHandlerInterceptor.java @@ -0,0 +1,65 @@ +package com.schbrain.common.web.support; + +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author liaozan + * @since 2022/11/11 + */ +public class BaseHandlerInterceptor implements AsyncHandlerInterceptor { + + @Override + public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (CorsUtils.isPreFlightRequest(request)) { + return true; + } + if (handler instanceof HandlerMethod) { + return preHandle(request, response, (HandlerMethod) handler); + } + return true; + } + + @Override + public final void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + if (handler instanceof HandlerMethod) { + postHandle(request, response, (HandlerMethod) handler, modelAndView); + } + } + + @Override + public final void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + if (handler instanceof HandlerMethod) { + afterCompletion(request, response, (HandlerMethod) handler, ex); + } + } + + @Override + public final void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (handler instanceof HandlerMethod) { + afterConcurrentHandlingStarted(request, response, (HandlerMethod) handler); + } + } + + protected boolean preHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) throws Exception { + return true; + } + + protected void postHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler, ModelAndView modelAndView) throws Exception { + + } + + protected void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler, Exception ex) throws Exception { + + } + + protected void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) { + + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/AbstractAuthenticator.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/AbstractAuthenticator.java new file mode 100644 index 0000000..5132f1b --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/AbstractAuthenticator.java @@ -0,0 +1,65 @@ +package com.schbrain.common.web.support.authentication; + +import cn.hutool.extra.spring.SpringUtil; +import com.schbrain.common.annotation.IgnoreLogin; +import com.schbrain.common.web.properties.WebProperties; +import com.schbrain.common.web.utils.HandlerMethodAnnotationUtils; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.Assert; +import org.springframework.web.method.HandlerMethod; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author liaozan + * @since 2022/11/12 + */ +@Data +@Slf4j +public abstract class AbstractAuthenticator implements Authenticator { + + private final String authenticationVariableName; + + public AbstractAuthenticator() { + this(SpringUtil.getBean(WebProperties.class).getAuthenticationVariableName()); + } + + public AbstractAuthenticator(String authenticationVariableName) { + Assert.hasText(authenticationVariableName, "authenticationVariableName must not be empty"); + this.authenticationVariableName = authenticationVariableName; + } + + @Override + public boolean validate(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) { + boolean ignore = HandlerMethodAnnotationUtils.hasAnnotation(handler, IgnoreLogin.class); + if (ignore) { + return true; + } + String authentication = getAuthentication(request); + if (StringUtils.isBlank(authentication)) { + return false; + } + return doValidate(authentication, request, response, handler); + } + + protected boolean doValidate(String authentication, HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) { + return true; + } + + @Nullable + protected String getAuthentication(HttpServletRequest request) { + String authentication = request.getHeader(authenticationVariableName); + if (StringUtils.isBlank(authentication)) { + authentication = request.getParameter(authenticationVariableName); + } + if (StringUtils.isBlank(authentication)) { + log.warn("Can not get authentication from request, authenticationVariableName: {}", authenticationVariableName); + } + return authentication; + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/AuthenticationInterceptor.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/AuthenticationInterceptor.java new file mode 100644 index 0000000..3375ccf --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/AuthenticationInterceptor.java @@ -0,0 +1,58 @@ +package com.schbrain.common.web.support.authentication; + +import cn.hutool.extra.servlet.ServletUtil; +import com.schbrain.common.util.JacksonUtils; +import com.schbrain.common.web.result.ResponseDTO; +import com.schbrain.common.web.support.BaseHandlerInterceptor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static com.schbrain.common.constants.ResponseActionConstants.ALERT; +import static com.schbrain.common.constants.ResponseCodeConstants.LOGIN_REQUIRED; + +/** + * @author liaozan + * @since 2022/11/11 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AuthenticationInterceptor extends BaseHandlerInterceptor { + + private Authenticator authenticator; + + public AuthenticationInterceptor(Authenticator authenticator) { + Assert.notNull(authenticator, "authenticator must not be null"); + this.authenticator = authenticator; + } + + @Override + protected boolean preHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler) { + boolean validated = authenticator.validate(request, response, handler); + if (validated) { + return true; + } + writeResult(response, buildAccessDeniedResponse()); + return false; + } + + @Override + protected void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler, Exception ex) { + authenticator.afterCompletion(request, response, handler, ex); + } + + protected void writeResult(HttpServletResponse response, ResponseDTO result) { + String resultString = JacksonUtils.toJsonString(result); + ServletUtil.write(response, resultString, MediaType.APPLICATION_JSON_VALUE); + } + + protected ResponseDTO buildAccessDeniedResponse() { + return ResponseDTO.error("未获取到认证信息, 请尝试重新登录", LOGIN_REQUIRED, ALERT); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/Authenticator.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/Authenticator.java new file mode 100644 index 0000000..0a45535 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/authentication/Authenticator.java @@ -0,0 +1,24 @@ +package com.schbrain.common.web.support.authentication; + +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author liaozan + * @since 2022/11/11 + */ +public interface Authenticator { + + /** + * 校验当前请求是否合法 + */ + boolean validate(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler); + + /** + * 请求完成后的回调 + */ + void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler, Exception exception); + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java new file mode 100644 index 0000000..29660bf --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java @@ -0,0 +1,20 @@ +package com.schbrain.common.web.support.concurrent; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; + +import java.util.Collections; +import java.util.Map; + +/** + * @author liaozan + * @since 2022/5/5 + */ +public class NoOpRateLimitCacheKeyVariablesContributor implements RateLimitCacheKeyVariablesContributor { + + @Override + public Map contribute(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) { + return Collections.emptyMap(); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitAspect.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitAspect.java new file mode 100644 index 0000000..e3c9ef8 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitAspect.java @@ -0,0 +1,119 @@ +package com.schbrain.common.web.support.concurrent; + +import cn.hutool.extra.spring.SpringUtil; +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.*; +import com.schbrain.common.web.utils.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.Advice; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.BeanUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.core.env.Environment; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.BoundValueOperations; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author liaozan + * @see com.schbrain.common.web.support.concurrent.RateLimiter + * @since 2022/5/5 + */ +@Slf4j +@Aspect +@ConditionalOnBean(StringRedisTemplate.class) +@ConditionalOnClass({Advice.class, RedisConnectionFactory.class}) +public class RateLimitAspect { + + private final Map, RateLimitCacheKeyVariablesContributor> contributorMap = new ConcurrentHashMap<>(); + + private final String keyPrefix; + + private final StringRedisTemplate stringRedisTemplate; + + public RateLimitAspect(Environment environment, StringRedisTemplate stringRedisTemplate) { + this.keyPrefix = ApplicationName.get(environment); + this.stringRedisTemplate = stringRedisTemplate; + } + + @Before("@annotation(rateLimiter)") + public void beforeExecute(JoinPoint joinPoint, RateLimiter rateLimiter) { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + doRateLimit(rateLimiter, joinPoint, methodSignature); + } + + protected void doRateLimit(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) { + Map variables = prepareVariables(rateLimiter, joinPoint, signature); + + String cacheKey = SpelUtils.parse(rateLimiter.cacheKey(), variables, String.class); + if (cacheKey == null) { + throw new BaseException("cacheKey should not be null"); + } + + String formattedCacheKey = formatCacheKey(cacheKey); + BoundValueOperations rateLimitOps = stringRedisTemplate.boundValueOps(formattedCacheKey); + + try { + long accessCount = Optional.ofNullable(rateLimitOps.increment()).orElse(1L); + if (accessCount > rateLimiter.permits()) { + throw new BaseException("访问频次太快,请稍后再试。"); + } + + if (accessCount <= 1) { + rateLimitOps.expire(rateLimiter.expireTime(), rateLimiter.unit()); + } + } catch (Exception ex) { + log.error("RateLimit encountered an unknown error, remove cacheKey: {}", formattedCacheKey, ex); + // Remove cacheKey to prevent the cache from never expiring + stringRedisTemplate.delete(formattedCacheKey); + throw ex; + } + } + + protected Map createEvaluationVariables(MethodSignature methodSignature, Object[] args) { + Map variables = new HashMap<>(); + Map methodArgsMap = ParameterDiscoverUtils.getMethodArgsMap(methodSignature.getMethod(), args); + if (!CollectionUtils.isEmpty(methodArgsMap)) { + variables.putAll(methodArgsMap); + } + + variables.put("request", ServletUtils.getRequest()); + variables.put("response", ServletUtils.getResponse()); + variables.put("applicationContext", SpringUtil.getApplicationContext()); + variables.put("beanFactory", SpringUtil.getBeanFactory()); + variables.put("args", args); + return variables; + } + + protected String formatCacheKey(String cacheKey) { + return "rateLimit:" + keyPrefix + ":" + cacheKey; + } + + private Map prepareVariables(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) { + Map variables = createEvaluationVariables(signature, joinPoint.getArgs()); + Class contributorClass = rateLimiter.contributor(); + if (contributorClass == null || contributorClass == NoOpRateLimitCacheKeyVariablesContributor.class) { + return variables; + } + + RateLimitCacheKeyVariablesContributor contributor = contributorMap.get(contributorClass); + if (contributor == null) { + contributor = BeanUtils.instantiateClass(contributorClass); + contributorMap.put(contributorClass, contributor); + } + Map contributeVariables = contributor.contribute(rateLimiter, joinPoint, signature); + if (!CollectionUtils.isEmpty(contributeVariables)) { + variables.putAll(contributeVariables); + } + return variables; + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitCacheKeyVariablesContributor.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitCacheKeyVariablesContributor.java new file mode 100644 index 0000000..1664c1f --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitCacheKeyVariablesContributor.java @@ -0,0 +1,16 @@ +package com.schbrain.common.web.support.concurrent; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; + +import java.util.Map; + +/** + * @author liaozan + * @since 2022/5/5 + */ +public interface RateLimitCacheKeyVariablesContributor { + + Map contribute(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature); + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimiter.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimiter.java new file mode 100644 index 0000000..8308c79 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimiter.java @@ -0,0 +1,44 @@ +package com.schbrain.common.web.support.concurrent; + +import org.aspectj.lang.reflect.MethodSignature; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * @author liaozan + * @since 2022/5/5 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimiter { + + /** + * 过期时间 + */ + long expireTime() default 10; + + /** + * 过期时间单位 + */ + TimeUnit unit() default TimeUnit.SECONDS; + + /** + * 过期时间内允许的许可数 + */ + int permits() default 3; + + /** + * 缓存的 key,使用 spel 进行解析 + *

+ * 可用的变量 {@link RateLimitAspect#createEvaluationVariables(MethodSignature, Object[])} + */ + String cacheKey(); + + /** + * evaluation variables contributor + */ + Class contributor() default NoOpRateLimitCacheKeyVariablesContributor.class; + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/utils/ExcelUtils.java b/commons/web-common/src/main/java/com/schbrain/common/web/utils/ExcelUtils.java new file mode 100644 index 0000000..acea460 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/utils/ExcelUtils.java @@ -0,0 +1,51 @@ +package com.schbrain.common.web.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import com.schbrain.common.util.support.excel.exception.ExcelException; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.util.CollectionUtils; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @author liaozan + * @since 2022/8/24 + */ +public class ExcelUtils extends com.schbrain.common.util.ExcelUtils { + + public static void writeToResponse(List dataList, String fileName) { + if (CollectionUtils.isEmpty(dataList)) { + throw new ExcelException("DataList is empty"); + } + writeToResponse(dataList, dataList.get(0).getClass(), fileName); + } + + public static void writeToResponse(List dataList, Class head, String fileName) { + if (CollectionUtils.isEmpty(dataList)) { + throw new ExcelException("DataList is empty"); + } + try { + HttpServletResponse response = ServletUtils.getResponse(); + ContentDisposition contentDisposition = ContentDisposition + .attachment() + .filename(fileName, StandardCharsets.UTF_8) + .build(); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()); + ServletOutputStream outputStream = response.getOutputStream(); + EasyExcel.write(outputStream) + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .sheet() + .head(head) + .doWrite(dataList); + } catch (IOException e) { + throw new ExcelException("Excel download fail", e); + } + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/utils/HandlerMethodAnnotationUtils.java b/commons/web-common/src/main/java/com/schbrain/common/web/utils/HandlerMethodAnnotationUtils.java new file mode 100644 index 0000000..f0f0780 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/utils/HandlerMethodAnnotationUtils.java @@ -0,0 +1,32 @@ +package com.schbrain.common.web.utils; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.web.method.HandlerMethod; + +import javax.annotation.Nullable; +import java.lang.annotation.Annotation; + +/** + * @author liaozan + * @since 2022-12-15 + */ +public class HandlerMethodAnnotationUtils { + + @Nullable + public static T getAnnotation(HandlerMethod handlerMethod, Class annotationType) { + T annotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), annotationType); + if (annotation == null) { + annotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), annotationType); + } + return annotation; + } + + public static boolean hasAnnotation(HandlerMethod handlerMethod, Class annotationType) { + boolean hasAnnotation = AnnotatedElementUtils.hasAnnotation(handlerMethod.getMethod(), annotationType); + if (!hasAnnotation) { + hasAnnotation = AnnotatedElementUtils.hasAnnotation(handlerMethod.getBeanType(), annotationType); + } + return hasAnnotation; + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/utils/ServletUtils.java b/commons/web-common/src/main/java/com/schbrain/common/web/utils/ServletUtils.java new file mode 100644 index 0000000..d3397a6 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/utils/ServletUtils.java @@ -0,0 +1,38 @@ +package com.schbrain.common.web.utils; + +import com.schbrain.common.exception.BaseException; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author liaozan + * @since 2022/1/6 + */ +public class ServletUtils { + + public static HttpServletRequest getRequest() { + ServletRequestAttributes requestAttributes = getRequestAttributes(); + if (requestAttributes == null) { + throw new BaseException("No HttpServletRequest available"); + } + return requestAttributes.getRequest(); + } + + public static HttpServletResponse getResponse() { + ServletRequestAttributes requestAttributes = getRequestAttributes(); + if (requestAttributes == null) { + throw new BaseException("No HttpServletResponse available"); + } + return requestAttributes.getResponse(); + } + + @Nullable + public static ServletRequestAttributes getRequestAttributes() { + return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + } + +} \ No newline at end of file diff --git a/commons/web-common/src/main/resources/META-INF/spring-configuration-metadata.json b/commons/web-common/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..b90d57b --- /dev/null +++ b/commons/web-common/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,61 @@ +{ + "groups": [ + { + "name": "schbrain.web", + "type": "com.schbrain.common.web.properties.WebProperties", + "sourceType": "com.schbrain.common.web.properties.WebProperties" + } + ], + "properties": [ + { + "name": "schbrain.web.authentication-variable-name", + "type": "java.lang.String", + "description": "authenticationVariableName for login auth", + "sourceType": "com.schbrain.common.web.properties.WebProperties", + "defaultValue": "token" + }, + { + "name": "schbrain.web.enable-global-exception-handler", + "type": "java.lang.Boolean", + "description": "whether to enable the global exception handing", + "sourceType": "com.schbrain.common.web.properties.WebProperties", + "defaultValue": true + }, + { + "name": "schbrain.web.enable-request-logging", + "type": "java.lang.Boolean", + "description": "whether to enable the request logging", + "sourceType": "com.schbrain.common.web.properties.WebProperties", + "defaultValue": true + }, + { + "name": "schbrain.web.encoding", + "type": "java.lang.String", + "description": "encoding for request\/response", + "sourceType": "com.schbrain.common.web.properties.WebProperties" + }, + { + "name": "schbrain.web.name", + "type": "java.lang.String", + "sourceType": "com.schbrain.common.web.properties.WebProperties" + }, + { + "name": "schbrain.web.namespace", + "type": "java.lang.String", + "sourceType": "com.schbrain.common.web.properties.WebProperties" + }, + { + "name": "schbrain.web.prefix", + "type": "java.lang.String", + "sourceType": "com.schbrain.common.web.properties.WebProperties" + }, + { + "name": "schbrain.web.wrap-response", + "type": "java.lang.Boolean", + "description": "whether to enable the response wrap", + "sourceType": "com.schbrain.common.web.properties.WebProperties", + "defaultValue": true + } + ], + "hints": [] +} \ No newline at end of file diff --git a/commons/web-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/commons/web-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..dd7da0c --- /dev/null +++ b/commons/web-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.common.web.WebCommonAutoConfiguration \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/LICENSE b/integration/integration-jenkins-plugin/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/integration/integration-jenkins-plugin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/integration/integration-jenkins-plugin/README.md b/integration/integration-jenkins-plugin/README.md new file mode 100644 index 0000000..ef290c8 --- /dev/null +++ b/integration/integration-jenkins-plugin/README.md @@ -0,0 +1,26 @@ +### How to develop + +* add the following content to your maven settings.xml +* active jenkins profile on your ide + +```xml + + jenkins + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + ``` + +### How to build + +run `mvn clean package -Dmaven.test.skip` in terminal, plugin will be stored in the target directory named `integration-jenkins-plugin.hpi`, upload it to your jenkins server diff --git a/integration/integration-jenkins-plugin/pom.xml b/integration/integration-jenkins-plugin/pom.xml new file mode 100644 index 0000000..f4937ad --- /dev/null +++ b/integration/integration-jenkins-plugin/pom.xml @@ -0,0 +1,84 @@ + + + + 4.0.0 + + + org.jenkins-ci.plugins + plugin + + 4.61 + + + + com.schbrain.ci.jenkins.plugins + integration-jenkins-plugin + 1.0 + hpi + + + integration-1.0 + + + + 2.397 + + + + + org.apache.velocity + velocity-engine-core + 2.3 + + + org.apache.commons + commons-lang3 + 3.12.0 + + + org.jenkins-ci.plugins + git + 5.0.0 + + + org.jenkins-ci.tools + git-parameter + 0.9.18 + + + org.jenkins-ci.plugins + localization-zh-cn + 1.0.24 + + + + + + + maven-enforcer-plugin + + + display-info + none + + + + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/action/ViewBuildScriptAction.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/action/ViewBuildScriptAction.java new file mode 100644 index 0000000..7c82e40 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/action/ViewBuildScriptAction.java @@ -0,0 +1,61 @@ +package com.schbrain.ci.jenkins.plugins.integration.action; + +import hudson.FilePath; +import hudson.model.*; +import jenkins.model.RunAction2; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import java.io.File; + +/** + * @author liaozan + * @since 2022/2/16 + */ +public class ViewBuildScriptAction implements RunAction2 { + + private final String buildScriptDir; + + private transient Run run; + + public ViewBuildScriptAction(File buildScriptDir) { + this.buildScriptDir = buildScriptDir.getAbsolutePath(); + } + + @Override + public String getIconFileName() { + return "document.png"; + } + + @Override + public String getDisplayName() { + return "构建脚本"; + } + + @Override + public String getUrlName() { + return "build-scripts"; + } + + @Override + public void onAttached(Run run) { + this.run = run; + } + + @Override + public void onLoad(Run run) { + this.run = run; + } + + public Run getRun() { + return run; + } + + @SuppressWarnings({"unused", "rawtypes"}) + public DirectoryBrowserSupport doList(StaplerRequest request, StaplerResponse response) { + AbstractBuild build = (AbstractBuild) request.findAncestor(AbstractBuild.class).getObject(); + FilePath filePath = new FilePath(new File(buildScriptDir)); + return new DirectoryBrowserSupport(build, filePath, "构建脚本", "folder.png", true); + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/BuilderContext.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/BuilderContext.java new file mode 100644 index 0000000..10f84b1 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/BuilderContext.java @@ -0,0 +1,135 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder; + +import com.schbrain.ci.jenkins.plugins.integration.builder.env.BuildEnvContributor; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.Logger; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.TemplateUtils; +import hudson.*; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; +import hudson.tasks.Shell; + +import java.io.IOException; +import java.util.Objects; + +/** + * @author zhangdd on 2022/1/21 + */ +public class BuilderContext { + + private final AbstractBuild build; + private final Launcher launcher; + private final FilePath workspace; + private final BuildListener listener; + private final Logger logger; + private final EnvVars envVars; + + private boolean imageHasBeenBuilt; + + private BuilderContext(Builder builder) { + this.build = builder.build; + this.launcher = builder.launcher; + this.workspace = builder.workspace; + this.listener = builder.listener; + this.logger = builder.logger; + this.envVars = builder.envVars; + this.imageHasBeenBuilt = false; + } + + public void execute(String command) throws InterruptedException, IOException { + String resolvedCommand = TemplateUtils.evaluate(command, envVars); + if (!Objects.equals(resolvedCommand, command)) { + log("before resolve: %s", command); + command = resolvedCommand; + } + log("%s", command); + BuildEnvContributor.saveEnvVarsToDisk(this); + Shell shell = new Shell(command); + shell.perform(getBuild(), getLauncher(), getListener()); + } + + public AbstractBuild getBuild() { + return build; + } + + public Launcher getLauncher() { + return launcher; + } + + public FilePath getWorkspace() { + return workspace; + } + + public BuildListener getListener() { + return listener; + } + + public Logger getLogger() { + return logger; + } + + public EnvVars getEnvVars() { + return envVars; + } + + public boolean isImageHasBeenBuilt() { + return imageHasBeenBuilt; + } + + public void setImageHasBeenBuilt() { + this.imageHasBeenBuilt = true; + } + + public void log(String template) { + log(template, (Object) null); + } + + public void log(String template, Object... arguments) { + logger.println(template, true, arguments); + } + + public static class Builder { + + private AbstractBuild build; + private Launcher launcher; + private FilePath workspace; + private BuildListener listener; + private Logger logger; + private EnvVars envVars; + + public Builder build(AbstractBuild build) { + this.build = build; + return this; + } + + public Builder launcher(Launcher launcher) { + this.launcher = launcher; + return this; + } + + public Builder workspace(FilePath workspace) { + this.workspace = workspace; + return this; + } + + public Builder listener(BuildListener listener) { + this.listener = listener; + return this; + } + + public Builder logger(Logger logger) { + this.logger = logger; + return this; + } + + public Builder envVars(EnvVars envVars) { + this.envVars = envVars; + return this; + } + + public BuilderContext build() { + return new BuilderContext(this); + } + + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/FileManager.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/FileManager.java new file mode 100644 index 0000000..fb62b65 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/FileManager.java @@ -0,0 +1,44 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder; + +import hudson.model.AbstractBuild; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.BuildConstants.*; + +/** + * @author liaozan + * @since 2022/2/8 + */ +@SuppressWarnings("ResultOfMethodCallIgnored") +public class FileManager { + + public static File getCurrentBuildDir(AbstractBuild build) { + return build.getRootDir(); + } + + public static File getWorkspaceBuildScriptDir(AbstractBuild build) { + return new File(Objects.requireNonNull(build.getWorkspace()).getRemote(), BUILD_SCRIPT_NAME); + } + + public static File getEnvVarsFile(AbstractBuild build) throws IOException { + File buildScriptDir = getBuildScriptDir(build); + File envVarsFile = new File(buildScriptDir, ENV_VARS); + if (!envVarsFile.exists()) { + envVarsFile.createNewFile(); + } + return envVarsFile; + } + + public static File getBuildScriptDir(AbstractBuild build) { + File buildDir = getCurrentBuildDir(build); + File buildScriptDir = new File(buildDir, BUILD_SCRIPT_NAME); + if (!buildScriptDir.exists()) { + buildScriptDir.mkdirs(); + } + return buildScriptDir; + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/IntegrationBuilder.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/IntegrationBuilder.java new file mode 100644 index 0000000..e4867b0 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/IntegrationBuilder.java @@ -0,0 +1,280 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder; + +import com.schbrain.ci.jenkins.plugins.integration.action.ViewBuildScriptAction; +import com.schbrain.ci.jenkins.plugins.integration.builder.config.*; +import com.schbrain.ci.jenkins.plugins.integration.builder.config.DockerConfig.PushConfig; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.Logger; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.*; +import hudson.model.*; +import hudson.tasks.Builder; +import net.sf.json.JSONObject; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; +import org.springframework.lang.Nullable; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.BuildConstants.*; +import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.*; +import static com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils.lookupFile; + +/** + * @author liaozan + * @since 2022/1/14 + */ +@SuppressWarnings("unused") +public class IntegrationBuilder extends Builder { + + private final MavenConfig mavenConfig; + private final DockerConfig dockerConfig; + private final DeployToK8sConfig deployToK8sConfig; + + @DataBoundConstructor + public IntegrationBuilder(@Nullable MavenConfig mavenConfig, + @Nullable DockerConfig dockerConfig, + @Nullable DeployToK8sConfig deployToK8sConfig) { + this.mavenConfig = mavenConfig; + this.dockerConfig = dockerConfig; + this.deployToK8sConfig = deployToK8sConfig; + } + + @Nullable + public MavenConfig getMavenConfig() { + return mavenConfig; + } + + @Nullable + public DockerConfig getDockerConfig() { + return dockerConfig; + } + + @Nullable + public DeployToK8sConfig getDeployToK8sConfig() { + return deployToK8sConfig; + } + + /** + * Builder start + */ + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { + build.addAction(new ViewBuildScriptAction(FileManager.getBuildScriptDir(build))); + BuilderContext builderContext = new BuilderContext.Builder() + .build(build) + .launcher(launcher) + .listener(listener) + .logger(Logger.of(listener.getLogger())) + .workspace(checkWorkspaceValid(build.getWorkspace())) + .envVars(createEnvVars(build)) + .build(); + return doPerformBuild(builderContext); + } + + @Override + public IntegrationDescriptor getDescriptor() { + return (IntegrationDescriptor) super.getDescriptor(); + } + + protected boolean doPerformBuild(BuilderContext context) throws IOException, InterruptedException { + try { + // download build scripts + downloadBuildScript(context); + // maven build + performMavenBuild(context); + // read maven build-info + readMavenBuildInfo(context); + // docker build + performDockerBuild(context); + // docker push + performDockerPush(context); + // deploy + deployToRemote(context); + } finally { + if (context.isImageHasBeenBuilt()) { + // delete the built image if possible + deleteImageAfterBuild(context); + } + // setup description + setBuildDescription(context); + // copy to buildDir + saveTheBuildScripts(context); + } + return true; + } + + private EnvVars createEnvVars(AbstractBuild build) { + EnvVars envVars = new EnvVars(); + ParametersAction parametersAction = build.getAction(ParametersAction.class); + if (parametersAction == null) { + return envVars; + } + List allParameters = parametersAction.getAllParameters(); + for (ParameterValue parameter : allParameters) { + if (parameter.getValue() == null) { + continue; + } + envVars.put(parameter.getName(), parameter.getValue().toString()); + } + return envVars; + } + + private void downloadBuildScript(BuilderContext context) throws IOException, InterruptedException { + String buildScriptUrl = DEFAULT_SCRIPT_GIT_REPO; + String buildScriptBranch = DEFAULT_SCRIPT_GIT_BRANCH; + // if buildScriptUrl and buildScriptBranch is set, will use specified value to download build-script, otherwise will use default value + if (getDockerConfig() != null) { + if (StringUtils.isNotBlank(getDockerConfig().getBuildScriptUrl())) { + buildScriptUrl = getDockerConfig().getBuildScriptUrl(); + } + if (StringUtils.isNotBlank(getDockerConfig().getBuildScriptBranch())) { + buildScriptBranch = getDockerConfig().getBuildScriptBranch(); + } + } + // git archive the build scripts + String archiveCommand = String.format("git archive -o %s --format=zip --remote=%s %s", SCRIPT_ZIP_NAME, buildScriptUrl, buildScriptBranch); + context.execute(archiveCommand); + // unzip to directory in workspace + String unzipCommand = String.format("unzip -o %s -d %s", SCRIPT_ZIP_NAME, BUILD_SCRIPT_NAME); + context.execute(unzipCommand); + } + + private void setBuildDescription(BuilderContext context) throws IOException, InterruptedException { + FilePath gitPropertiesFile = lookupFile(context, GitConstants.GIT_PROPERTIES_FILE); + if (gitPropertiesFile == null) { + return; + } + Map gitProperties = FileUtils.filePathToMap(gitPropertiesFile); + String author = gitProperties.get(GitConstants.GIT_COMMITTER); + String branch = gitProperties.get(GitConstants.GIT_BRANCH); + AbstractBuild build = context.getBuild(); + String description = String.format("author: %s, branch: %s", author, branch); + build.setDescription(description); + } + + /** + * Check workspace + */ + private FilePath checkWorkspaceValid(@CheckForNull FilePath workspace) throws IOException, InterruptedException { + if (workspace == null) { + throw new IllegalStateException("workspace is null"); + } + if (!workspace.exists()) { + throw new IllegalStateException("workspace is not exist"); + } + return workspace; + } + + private void performMavenBuild(BuilderContext context) throws IOException, InterruptedException { + MavenConfig mavenConfig = getMavenConfig(); + if (mavenConfig == null) { + context.log("maven build is not checked"); + return; + } + + mavenConfig.build(context); + } + + private void readMavenBuildInfo(BuilderContext context) throws IOException, InterruptedException { + EnvVars envVars = context.getEnvVars(); + FilePath dockerBuildInfo = lookupFile(context, DockerConstants.BUILD_INFO_FILE_NAME); + if (dockerBuildInfo == null) { + context.log("%s file not exist, skip docker build", DockerConstants.BUILD_INFO_FILE_NAME); + return; + } + // overwriting existing environment variables is not allowed + FileUtils.filePathToMap(dockerBuildInfo).forEach(envVars::putIfAbsent); + } + + private void performDockerBuild(BuilderContext context) throws IOException, InterruptedException { + DockerConfig dockerConfig = getDockerConfig(); + if (dockerConfig == null) { + context.log("docker build is not checked"); + return; + } + + dockerConfig.build(context); + } + + private void performDockerPush(BuilderContext context) throws IOException, InterruptedException { + DockerConfig dockerConfig = getDockerConfig(); + if (dockerConfig == null) { + context.log("docker build is not checked"); + return; + } + PushConfig pushConfig = dockerConfig.getPushConfig(); + if (pushConfig == null) { + context.log("docker push is not checked"); + return; + } + + pushConfig.build(context); + } + + /** + * Delete the image produced in the build + */ + private void deleteImageAfterBuild(BuilderContext context) throws InterruptedException, IOException { + DockerConfig dockerConfig = getDockerConfig(); + if (dockerConfig == null) { + context.log("docker build is not checked"); + return; + } + if (!dockerConfig.getDeleteImageAfterBuild()) { + context.log("delete built image is skip"); + return; + } + + String imageName = context.getEnvVars().get(DockerConstants.IMAGE); + if (imageName == null) { + return; + } + + String command = String.format("docker rmi -f %s", imageName); + context.execute(command); + } + + /** + * 部署镜像到远端 + */ + private void deployToRemote(BuilderContext context) throws IOException, InterruptedException { + DeployToK8sConfig k8sConfig = getDeployToK8sConfig(); + if (k8sConfig == null) { + context.log("k8s deploy is not checked"); + return; + } + + k8sConfig.build(context); + } + + private void saveTheBuildScripts(BuilderContext context) throws InterruptedException, IOException { + String copy = String.format("cp -r %s %s", BUILD_SCRIPT_NAME, FileManager.getCurrentBuildDir(context.getBuild())); + context.execute(copy); + } + + // can not move outside builder class + @Extension + public static class IntegrationDescriptor extends Descriptor { + + public IntegrationDescriptor() { + load(); + } + + @Override + public String getDisplayName() { + return "发布集成"; + } + + @Override + public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { + save(); + return super.configure(req, formData); + } + + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/BuildConfig.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/BuildConfig.java new file mode 100644 index 0000000..d7f040f --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/BuildConfig.java @@ -0,0 +1,37 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.config; + +import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.Logger; +import hudson.*; +import hudson.model.*; + +import java.io.IOException; + +/** + * @author liaozan + * @since 2022/1/17 + */ +public abstract class BuildConfig> extends AbstractDescribableImpl { + + protected AbstractBuild build; + protected Launcher launcher; + protected FilePath workspace; + protected BuildListener listener; + protected Logger logger; + protected EnvVars envVars; + protected BuilderContext context; + + public void build(BuilderContext context) throws IOException, InterruptedException { + this.context = context; + this.build = context.getBuild(); + this.launcher = context.getLauncher(); + this.workspace = context.getWorkspace(); + this.listener = context.getListener(); + this.logger = context.getLogger(); + this.envVars = context.getEnvVars(); + doBuild(); + } + + protected abstract void doBuild() throws InterruptedException, IOException; + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/DeployToK8sConfig.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/DeployToK8sConfig.java new file mode 100644 index 0000000..6873148 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/DeployToK8sConfig.java @@ -0,0 +1,101 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.config; + +import com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy.DeployStyleRadio; +import com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy.service.ServiceDeployConfig; +import com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DockerConstants; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils; +import hudson.*; +import hudson.model.Descriptor; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * @author liaozan + * @since 2022/1/16 + */ +@SuppressWarnings("unused") +public class DeployToK8sConfig extends BuildConfig { + + private final String configLocation; + + private final DeployStyleRadio deployStyle; + + private final ServiceDeployConfig serviceDeployConfig; + + @DataBoundConstructor + public DeployToK8sConfig(String configLocation, DeployStyleRadio deployStyle, ServiceDeployConfig serviceDeployConfig) { + this.configLocation = Util.fixNull(configLocation); + this.deployStyle = deployStyle; + this.serviceDeployConfig = serviceDeployConfig; + } + + public String getConfigLocation() { + return configLocation; + } + + public DeployStyleRadio getDeployStyle() { + return deployStyle; + } + + public ServiceDeployConfig getServiceDeployConfig() { + return serviceDeployConfig; + } + + public void doBuild() throws InterruptedException, IOException { + String imageName = envVars.get(DockerConstants.IMAGE); + if (StringUtils.isBlank(imageName)) { + context.log("image name is empty ,skip deploy"); + return; + } + // make sure to build Deployment first + buildDeployment(); + buildService(); + } + + private void buildDeployment() throws IOException, InterruptedException { + DeployStyleRadio deployStyle = getDeployStyle(); + if (null == deployStyle) { + return; + } + + String deployFileLocation = deployStyle.getDeployFileLocation(context); + executeK8sCommand(deployFileLocation); + } + + private void buildService() throws IOException, InterruptedException { + if (serviceDeployConfig == null) { + return; + } + String deployFileLocation = serviceDeployConfig.getServiceDeployFileLocation(context); + executeK8sCommand(deployFileLocation); + } + + private void executeK8sCommand(String deployFileLocation) throws InterruptedException, IOException { + String configLocation = getConfigLocation(); + if (null == configLocation) { + context.log("not specified configLocation of k8s config ,will use default config ."); + } + + String deployFileRelativePath = FileUtils.toRelativePath(workspace, new FilePath(new File(deployFileLocation))); + String command = String.format("kubectl apply -f %s", deployFileRelativePath); + if (StringUtils.isNotBlank(configLocation)) { + command = command + " --kubeconfig " + configLocation; + } + context.execute(command); + } + + @Extension + public static class DescriptorImpl extends Descriptor { + + public List> getDeployStyles() { + return Jenkins.get().getDescriptorList(DeployStyleRadio.class); + } + + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/DockerConfig.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/DockerConfig.java new file mode 100644 index 0000000..7c91fef --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/DockerConfig.java @@ -0,0 +1,182 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.config; + +import com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.TemplateUtils; +import hudson.*; +import hudson.model.Descriptor; +import org.apache.commons.lang3.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; +import org.springframework.lang.Nullable; + +import java.io.IOException; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.BuildConstants.BUILD_SCRIPT_NAME; +import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DockerConstants.*; +import static com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils.lookupFile; + +/** + * @author liaozan + * @since 2022/1/16 + */ +@SuppressWarnings("unused") +public class DockerConfig extends BuildConfig { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneId.systemDefault()); + + private final Boolean buildImage; + private final PushConfig pushConfig; + private final Boolean deleteImageAfterBuild; + private final String javaOpts; + private final String buildScriptUrl; + private final String buildScriptBranch; + private final Boolean disableSkywalking; + + @DataBoundConstructor + public DockerConfig(Boolean buildImage, PushConfig pushConfig, Boolean deleteImageAfterBuild, String javaOpts, + String buildScriptUrl, String buildScriptBranch, Boolean disableSkywalking) { + this.buildImage = Util.fixNull(buildImage, false); + this.pushConfig = pushConfig; + this.deleteImageAfterBuild = Util.fixNull(deleteImageAfterBuild, false); + this.javaOpts = Util.fixNull(javaOpts); + this.buildScriptUrl = buildScriptUrl; + this.buildScriptBranch = buildScriptBranch; + this.disableSkywalking = disableSkywalking; + } + + @Nullable + public PushConfig getPushConfig() { + return pushConfig; + } + + public Boolean getBuildImage() { + return buildImage; + } + + public Boolean getDeleteImageAfterBuild() { + return deleteImageAfterBuild; + } + + public String getJavaOpts() { + return javaOpts; + } + + public String getBuildScriptUrl() { + return buildScriptUrl; + } + + public String getBuildScriptBranch() { + return buildScriptBranch; + } + + public Boolean getDisableSkywalking() { + return disableSkywalking; + } + + @Override + public void doBuild() throws IOException, InterruptedException { + if (!getBuildImage()) { + context.log("docker build image is skipped"); + return; + } + + String javaOpts = this.javaOpts; + if (Boolean.TRUE.equals(getDisableSkywalking())) { + if (StringUtils.isBlank(javaOpts)) { + javaOpts = DISABLE_SKYWALKING_OPTIONS; + } else { + javaOpts = javaOpts + " " + DISABLE_SKYWALKING_OPTIONS; + } + } + envVars.put(JAVA_OPTS, javaOpts); + + String imageName = getFullImageName(); + if (imageName == null) { + return; + } + envVars.put(IMAGE, imageName); + + FilePath dockerfile = lookupFile(workspace.child(BUILD_SCRIPT_NAME), DOCKERFILE_NAME, context.getLogger()); + if (dockerfile == null) { + context.log("Dockerfile not exist, skip docker build"); + return; + } + + TemplateUtils.evaluate(Paths.get(dockerfile.getRemote()), context); + + String relativePath = FileUtils.toRelativePath(workspace, dockerfile); + String command = String.format("docker build --pull -t %s -f %s .", imageName, relativePath); + context.execute(command); + context.setImageHasBeenBuilt(); + } + + private String getFullImageName() { + String registry = null; + PushConfig pushConfig = getPushConfig(); + if (pushConfig != null) { + registry = pushConfig.getRegistry(); + } + if (StringUtils.isBlank(registry)) { + registry = envVars.get(REGISTRY); + } + if (StringUtils.isBlank(registry)) { + throw new IllegalArgumentException("REGISTRY is null or empty"); + } + + String appName = envVars.get(APP_NAME); + String version = envVars.get(VERSION); + String buildTime = DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(build.getStartTimeInMillis())); + return String.format("%s/%s:%s-%s", registry, appName, version, buildTime); + } + + @Extension + public static class DescriptorImpl extends Descriptor { + + } + + public static class PushConfig extends BuildConfig { + + private final Boolean pushImage; + private final String registry; + + @DataBoundConstructor + public PushConfig(Boolean pushImage, String registry) { + this.pushImage = pushImage; + this.registry = registry; + } + + public Boolean getPushImage() { + return pushImage; + } + + public String getRegistry() { + return registry; + } + + @Override + public void doBuild() throws IOException, InterruptedException { + if (!getPushImage()) { + logger.println("docker push image is skipped"); + return; + } + + String imageName = envVars.get(IMAGE); + if (imageName == null) { + return; + } + String command = String.format("docker push %s", imageName); + context.execute(command); + } + + @Extension + @SuppressWarnings("unused") + public static class DescriptorImpl extends Descriptor { + + } + + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/MavenConfig.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/MavenConfig.java new file mode 100644 index 0000000..8ea69fa --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/MavenConfig.java @@ -0,0 +1,57 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.config; + +import hudson.Extension; +import hudson.Util; +import hudson.model.Descriptor; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.IOException; + +/** + * @author liaozan + * @since 2022/1/16 + */ +@SuppressWarnings("unused") +public class MavenConfig extends BuildConfig { + + private final String mvnCommand; + + private final String javaHome; + + @DataBoundConstructor + public MavenConfig(String mvnCommand, String javaHome) { + this.mvnCommand = Util.fixNull(mvnCommand); + this.javaHome = Util.fixNull(javaHome); + } + + public String getMvnCommand() { + return mvnCommand; + } + + public String getJavaHome() { + return javaHome; + } + + @Override + public void doBuild() throws IOException, InterruptedException { + String mavenCommand = getMvnCommand(); + if (StringUtils.isBlank(mavenCommand)) { + logger.println("maven command is empty, skip maven build"); + return; + } + + String javaHome = getJavaHome(); + if (StringUtils.isNotBlank(javaHome)) { + envVars.put("JAVA_HOME", javaHome); + } + + context.execute(mavenCommand); + } + + @Extension + public static class DescriptorImpl extends Descriptor { + + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployStyleRadio.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployStyleRadio.java new file mode 100644 index 0000000..7fd4be9 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployStyleRadio.java @@ -0,0 +1,31 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy; + +import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext; +import hudson.model.Describable; +import hudson.model.Descriptor; +import jenkins.model.Jenkins; + +import java.io.IOException; + +/** + * @author zhangdd on 2022/1/20 + */ +public abstract class DeployStyleRadio implements Describable { + + //--------------------------------------------------------------------- + // Abstract methods to be implemented by subclasses + //--------------------------------------------------------------------- + + public abstract String getDeployFileLocation(BuilderContext builderContext) throws IOException, InterruptedException; + + @Override + @SuppressWarnings("unchecked") + public Descriptor getDescriptor() { + return Jenkins.get().getDescriptor(getClass()); + } + + public abstract static class InventoryDescriptor extends Descriptor { + + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployTemplateComponent.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployTemplateComponent.java new file mode 100644 index 0000000..55b66b5 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployTemplateComponent.java @@ -0,0 +1,99 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy; + +import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext; +import com.schbrain.ci.jenkins.plugins.integration.builder.FileManager; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.TemplateUtils; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.EnvVars; +import hudson.Extension; +import org.apache.commons.lang3.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DeployConstants.*; + +/** + * @author zhangdd on 2022/1/20 + */ +@SuppressWarnings("unused") +public class DeployTemplateComponent extends DeployStyleRadio { + + private final String namespace; + private final String replicas; + private final String memoryRequest; + private final String memoryLimit; + private final String nodeTag; + private final String port; + + @DataBoundConstructor + public DeployTemplateComponent(String namespace, String replicas, String memoryRequest, String memoryLimit, String nodeTag, String port) { + this.namespace = namespace; + this.replicas = replicas; + this.memoryRequest = memoryRequest; + this.memoryLimit = memoryLimit; + this.nodeTag = nodeTag; + this.port = port; + } + + public String getNamespace() { + return namespace; + } + + public String getReplicas() { + return replicas; + } + + public String getMemoryRequest() { + return memoryRequest; + } + + public String getMemoryLimit() { + return memoryLimit; + } + + public String getNodeTag() { + return nodeTag; + } + + public String getPort() { + return port; + } + + @Override + public String getDeployFileLocation(BuilderContext context) throws IOException, InterruptedException { + Path templateFile = getDeployTemplate(context); + contributeEnv(context.getEnvVars()); + TemplateUtils.evaluate(templateFile, context); + return templateFile.toString(); + } + + private void contributeEnv(EnvVars envVars) { + envVars.put(K8S_POD_NAMESPACE, getNamespace()); + envVars.put(K8S_POD_PORT, getPort()); + envVars.put(K8S_POD_REPLICAS, getReplicas()); + envVars.put(K8S_POD_MEMORY_REQUEST, getMemoryRequest()); + envVars.put(K8S_POD_MEMORY_LIMIT, getMemoryLimit()); + envVars.put(K8S_POD_NODE_TAG, StringUtils.isBlank(getNodeTag()) ? "app" : getNodeTag()); + } + + private Path getDeployTemplate(BuilderContext context) { + File buildScriptDir = FileManager.getWorkspaceBuildScriptDir(context.getBuild()); + return Paths.get(buildScriptDir.getPath(), DEPLOYMENT_TEMPLATE_FILE_NAME); + } + + @Extension + public static class DescriptorImpl extends InventoryDescriptor { + + @NonNull + @Override + public String getDisplayName() { + return "使用默认模版"; + } + + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/PointDeployFileComponent.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/PointDeployFileComponent.java new file mode 100644 index 0000000..9d5aef2 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/PointDeployFileComponent.java @@ -0,0 +1,41 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy; + +import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * @author zhangdd on 2022/1/20 + */ +@SuppressWarnings("unused") +public class PointDeployFileComponent extends DeployStyleRadio { + + private final String deployFileLocation; + + @DataBoundConstructor + public PointDeployFileComponent(String deployFileLocation) { + this.deployFileLocation = deployFileLocation; + } + + public String getDeployFileLocation() { + return deployFileLocation; + } + + @Override + public String getDeployFileLocation(BuilderContext builderContext) { + return getDeployFileLocation(); + } + + @Extension + public static class DescriptorImpl extends InventoryDescriptor { + + @NonNull + @Override + public String getDisplayName() { + return "指定部署文件位置"; + } + + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/service/ServiceDeployConfig.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/service/ServiceDeployConfig.java new file mode 100644 index 0000000..2f56c68 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/service/ServiceDeployConfig.java @@ -0,0 +1,87 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.config.deploy.service; + +import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext; +import com.schbrain.ci.jenkins.plugins.integration.builder.FileManager; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.TemplateUtils; +import hudson.EnvVars; +import org.kohsuke.stapler.DataBoundConstructor; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DeployConstants.*; +import static com.schbrain.ci.jenkins.plugins.integration.builder.constants.Constants.DockerConstants.APP_NAME; + +/** + * @author liaozan + * @since 2022/3/10 + */ +public class ServiceDeployConfig { + + private final String serviceMode; + private final String serviceNamespace; + private final String serviceName; + private final String servicePort; + + @DataBoundConstructor + public ServiceDeployConfig(String serviceMode, String serviceNamespace, String serviceName, String servicePort) { + this.serviceMode = serviceMode; + this.serviceNamespace = serviceNamespace; + this.serviceName = serviceName; + this.servicePort = servicePort; + } + + public String getServiceMode() { + return serviceMode; + } + + public String getServiceNamespace() { + return serviceNamespace; + } + + public String getServiceName() { + return serviceName; + } + + public String getServicePort() { + return servicePort; + } + + public String getServiceDeployFileLocation(BuilderContext context) throws IOException, InterruptedException { + Path templateFile = getServiceDeployTemplate(context); + contributeEnv(context.getEnvVars()); + TemplateUtils.evaluate(templateFile, context); + return templateFile.toString(); + } + + private void contributeEnv(EnvVars envVars) { + envVars.put(K8S_SERVICE_MODE, getServiceMode()); + + if (StringUtils.hasText(getServiceNamespace())) { + envVars.put(K8S_SERVICE_NAMESPACE, getServiceNamespace()); + } else { + envVars.put(K8S_SERVICE_NAMESPACE, envVars.get(K8S_POD_NAMESPACE)); + } + + if (StringUtils.hasText(getServiceName())) { + envVars.put(K8S_SERVICE_NAME, getServiceName()); + } else { + envVars.put(K8S_SERVICE_NAME, envVars.get(APP_NAME)); + } + + if (StringUtils.hasText(getServicePort())) { + envVars.put(K8S_SERVICE_PORT, getServicePort()); + } else { + envVars.put(K8S_SERVICE_PORT, envVars.get(K8S_POD_PORT)); + } + } + + private Path getServiceDeployTemplate(BuilderContext context) { + File buildScriptDir = FileManager.getWorkspaceBuildScriptDir(context.getBuild()); + return Paths.get(buildScriptDir.getPath(), SERVICE_TEMPLATE_FILE_NAME); + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/constants/Constants.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/constants/Constants.java new file mode 100644 index 0000000..73f23e6 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/constants/Constants.java @@ -0,0 +1,56 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.constants; + +/** + * @author zhangdd on 2022/1/20 + */ +public class Constants { + + public static class BuildConstants { + + public static final String DEFAULT_SCRIPT_GIT_REPO = "git@gitlab.schbrain.com:tools/build-script.git"; + public static final String DEFAULT_SCRIPT_GIT_BRANCH = "main"; + public static final String SCRIPT_ZIP_NAME = "build-script.zip"; + public static final String BUILD_SCRIPT_NAME = "build-script"; + public static final String ENV_VARS = "envVars"; + + } + + public static class DeployConstants { + + public static final String DEPLOYMENT_TEMPLATE_FILE_NAME = "k8s-deploy-template.yaml"; + public static final String SERVICE_TEMPLATE_FILE_NAME = "k8s-service-template.yaml"; + public static final String K8S_POD_NAMESPACE = "NAMESPACE"; + public static final String K8S_POD_PORT = "PORT"; + public static final String K8S_POD_REPLICAS = "REPLICAS"; + public static final String K8S_POD_MEMORY_LIMIT = "MEMORY_LIMIT"; + public static final String K8S_POD_MEMORY_REQUEST = "MEMORY_REQUEST"; + public static final String K8S_POD_NODE_TAG = "NODE_TAG"; + public static final String K8S_SERVICE_MODE = "SERVICE_MODE"; + public static final String K8S_SERVICE_NAMESPACE = "SERVICE_NAMESPACE"; + public static final String K8S_SERVICE_NAME = "SERVICE_NAME"; + public static final String K8S_SERVICE_PORT = "SERVICE_PORT"; + + } + + public static class DockerConstants { + + public static final String BUILD_INFO_FILE_NAME = "dockerBuildInfo"; + public static final String DOCKERFILE_NAME = "Dockerfile"; + public static final String IMAGE = "IMAGE"; + public static final String REGISTRY = "REGISTRY"; + public static final String APP_NAME = "APP_NAME"; + public static final String VERSION = "VERSION"; + public static final String JAVA_OPTS = "JAVA_OPTS"; + public static final String DISABLE_SKYWALKING_OPTIONS = "-Dskywalking.agent.enable=false"; + + } + + public static class GitConstants { + + public static final String GIT_PROPERTIES_FILE = "git.properties"; + public static final String GIT_BRANCH = "git.branch"; + public static final String GIT_COMMITTER = "git.commit.user.name"; + + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/env/BuildEnvContributor.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/env/BuildEnvContributor.java new file mode 100644 index 0000000..2962a05 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/env/BuildEnvContributor.java @@ -0,0 +1,49 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.env; + +import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext; +import com.schbrain.ci.jenkins.plugins.integration.builder.FileManager; +import com.schbrain.ci.jenkins.plugins.integration.builder.util.FileUtils; +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.AbstractBuild; +import hudson.model.BuildVariableContributor; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +/** + * @author liaozan + * @since 2022/1/17 + */ +@Extension +public class BuildEnvContributor extends BuildVariableContributor { + + private static final String DELIMITER = "="; + + public static void saveEnvVarsToDisk(BuilderContext context) throws IOException { + File envVarsFile = FileManager.getEnvVarsFile(context.getBuild()); + FileUtils.writeUtf8String("", envVarsFile); + FileUtils.writeUtf8Map(context.getEnvVars(), envVarsFile, DELIMITER); + } + + @Override + public void buildVariablesFor(AbstractBuild build, Map variables) { + File envFilePath; + try { + envFilePath = FileManager.getEnvVarsFile(build); + } catch (IOException e) { + return; + } + EnvVars envVars = new EnvVars(); + for (String line : FileUtils.readUtf8Lines(envFilePath)) { + String[] variablePair = line.split(DELIMITER); + // variables may not be split by = + if (variablePair.length != 1) { + envVars.putIfNotNull(variablePair[0], variablePair[1]); + } + } + variables.putAll(envVars); + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/FileUtils.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/FileUtils.java new file mode 100644 index 0000000..63a96a1 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/FileUtils.java @@ -0,0 +1,125 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.util; + +import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.FilePath; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.*; +import java.util.Map.Entry; + +/** + * @author liaozan + * @since 2022/1/17 + */ +public class FileUtils { + + /** + * lookup the special file + */ + @CheckForNull + public static FilePath lookupFile(BuilderContext context, String fileName) throws IOException, InterruptedException { + return lookupFile(context.getWorkspace(), fileName, context.getLogger()); + } + + /** + * lookup the special file + */ + @CheckForNull + public static FilePath lookupFile(FilePath searchLocation, String fileName, Logger logger) throws IOException, InterruptedException { + if (searchLocation == null || !searchLocation.exists()) { + logger.println("searchLocation not exist", true); + return null; + } + FilePath[] fileList = searchLocation.list("**/" + fileName); + if (fileList.length == 0) { + logger.println("could not found matched file: %s", fileName); + return null; + } + return getTheClosestFile(fileList); + } + + public static String toRelativePath(FilePath root, FilePath filePath) { + Path rootPath = Paths.get(root.getRemote()); + Path targetFilePath = Paths.get(filePath.getRemote()); + return rootPath.relativize(targetFilePath).toString(); + } + + public static FilePath getTheClosestFile(FilePath[] fileList) { + FilePath matched = fileList[0]; + if (fileList.length == 1) { + return matched; + } + + for (FilePath filePath : fileList) { + String filePathName = filePath.getRemote(); + if (filePathName.length() < matched.getRemote().length()) { + matched = filePath; + } + } + return matched; + } + + public static Map filePathToMap(FilePath lookupFile) throws IOException, InterruptedException { + Map result = new HashMap<>(); + Properties properties = new Properties(); + properties.load(new StringReader(lookupFile.readToString())); + for (String propertyName : properties.stringPropertyNames()) { + result.put(propertyName, properties.getProperty(propertyName)); + } + return result; + } + + public static void writeUtf8String(String content, File file) { + writeUtf8String(content, file.getPath()); + } + + public static void writeUtf8String(String content, String path) { + writeString(content, path, StandardCharsets.UTF_8); + } + + public static void writeString(String content, String path, Charset charset) { + try { + Path filePath = getFilePath(path); + Files.write(filePath, content.getBytes(charset)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public static void writeUtf8Map(Map variables, File file, String delimiter) { + try { + Path filePath = getFilePath(file.getPath()); + BufferedWriter writer = Files.newBufferedWriter(filePath); + for (Entry entry : variables.entrySet()) { + String content = String.format("%s%s%s", entry.getKey(), delimiter, entry.getValue()); + writer.write(content); + writer.newLine(); + writer.flush(); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public static List readUtf8Lines(File file) { + try { + Path filePath = getFilePath(file.getPath()); + return Files.readAllLines(filePath); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static Path getFilePath(String path) throws IOException { + Path filePath = Paths.get(path); + if (Files.notExists(filePath)) { + Files.createFile(filePath); + } + return filePath; + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/Logger.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/Logger.java new file mode 100644 index 0000000..97fb9bc --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/Logger.java @@ -0,0 +1,46 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.util; + +import java.io.PrintStream; + +/** + * @author liaozan + * @since 2022/1/20 + */ +public class Logger extends PrintStream { + + private final PrintStream delegate; + + private Logger(PrintStream delegate) { + super(delegate); + this.delegate = delegate; + } + + public static Logger of(PrintStream delegate) { + return new Logger(delegate); + } + + public void println(String content, Object... args) { + println(content, true, args); + } + + public void println(String content, boolean format, Object... args) { + content = String.format(content, args); + println(content, format); + } + + public void println(String content, boolean format) { + if (format) { + String wrappedContent = "|| " + content + " ||"; + StringBuilder wrapperLine = new StringBuilder(); + wrapperLine.append("=".repeat(wrappedContent.length())); + delegate.println(); + delegate.println(wrapperLine); + delegate.println(wrappedContent); + delegate.println(wrapperLine); + delegate.println(); + } else { + delegate.println(content); + } + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/TemplateUtils.java b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/TemplateUtils.java new file mode 100644 index 0000000..76f0674 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/java/com/schbrain/ci/jenkins/plugins/integration/builder/util/TemplateUtils.java @@ -0,0 +1,45 @@ +package com.schbrain.ci.jenkins.plugins.integration.builder.util; + +import com.schbrain.ci.jenkins.plugins.integration.builder.BuilderContext; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @author liaozan + * @since 2022/1/23 + */ +public class TemplateUtils { + + public static void evaluate(Path templateFile, BuilderContext context) throws IOException, InterruptedException { + if (templateFile == null) { + return; + } + + String resolved = evaluate(Files.readString(templateFile), context.getEnvVars()); + Files.write(templateFile, resolved.getBytes(StandardCharsets.UTF_8)); + } + + public static String evaluate(String template, Map variables) { + if (null == template) { + return null; + } + if (null == variables || variables.isEmpty()) { + return template; + } + + Map params = new LinkedHashMap<>(variables); + VelocityContext velocityContext = new VelocityContext(params); + StringWriter writer = new StringWriter(); + Velocity.evaluate(velocityContext, writer, "Template Evaluate", template); + return writer.getBuffer().toString(); + } + +} \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/action/ViewBuildScriptAction/index.jelly b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/action/ViewBuildScriptAction/index.jelly new file mode 100644 index 0000000..5a6876f --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/action/ViewBuildScriptAction/index.jelly @@ -0,0 +1,10 @@ + + + + +

构建脚本

+ + + + + \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/IntegrationBuilder/config.jelly b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/IntegrationBuilder/config.jelly new file mode 100644 index 0000000..d41ae2e --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/IntegrationBuilder/config.jelly @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/DeployToK8sConfig/config.jelly b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/DeployToK8sConfig/config.jelly new file mode 100644 index 0000000..768d761 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/DeployToK8sConfig/config.jelly @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/DockerConfig/config.jelly b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/DockerConfig/config.jelly new file mode 100644 index 0000000..a956479 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/DockerConfig/config.jelly @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/MavenConfig/config.jelly b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/MavenConfig/config.jelly new file mode 100644 index 0000000..fe4f143 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/MavenConfig/config.jelly @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployTemplateComponent/config.jelly b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployTemplateComponent/config.jelly new file mode 100644 index 0000000..8572ee5 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/DeployTemplateComponent/config.jelly @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/PointDeployFileComponent/config.jelly b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/PointDeployFileComponent/config.jelly new file mode 100644 index 0000000..087d414 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/resources/com/schbrain/ci/jenkins/plugins/integration/builder/config/deploy/PointDeployFileComponent/config.jelly @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/integration/integration-jenkins-plugin/src/main/resources/index.jelly b/integration/integration-jenkins-plugin/src/main/resources/index.jelly new file mode 100644 index 0000000..ce68056 --- /dev/null +++ b/integration/integration-jenkins-plugin/src/main/resources/index.jelly @@ -0,0 +1,7 @@ + + +
+ This plugin provider integration with Kubernetes. +
\ No newline at end of file diff --git a/integration/integration-maven-plugin/README.md b/integration/integration-maven-plugin/README.md new file mode 100644 index 0000000..51ac665 --- /dev/null +++ b/integration/integration-maven-plugin/README.md @@ -0,0 +1,59 @@ +### Summary + +> this is a maven plugin that simplify container deployment. it will generate `kubernetes-deploy.yaml`、`Dockerfile` according to your configuration + +### Requirement + +- your project should be a Spring Boot project +- your project will running in container + +### How to use + +**First** add `integration-maven-plugin` in the `pom.xml` , it must be added under the `spring-boot-maven-plugin` . like this + +```.xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + com.schbrain.maven.plugin + integration-maven-plugin + + + +``` + +**Second** specify build-script repository. + +```.xml + + git@gitlab.xxx.com:tools/build-script.git + main + +``` + +--- + +The completion config like this: + +```.xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + com.schbrain.maven.plugin + integration-maven-plugin + + git@gitlab.xxx.com:tools/build-script.git + main + + + + +``` diff --git a/integration/integration-maven-plugin/pom.xml b/integration/integration-maven-plugin/pom.xml new file mode 100644 index 0000000..0c42b97 --- /dev/null +++ b/integration/integration-maven-plugin/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + com.schbrain.framework + integration + ${revision} + + + com.schbrain.maven.plugin + integration-maven-plugin + maven-plugin + + + + org.apache.maven + maven-plugin-api + ${maven.core.version} + provided + + + org.apache.maven + maven-core + ${maven.core.version} + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${maven.plugin.version} + provided + + + cn.hutool + hutool-all + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + ${maven.plugin.version} + + + com.schbrain.maven.plugin + + + + + + + \ No newline at end of file diff --git a/integration/integration-maven-plugin/src/main/java/com/schbrain/maven/plugin/mojo/PrepareMojo.java b/integration/integration-maven-plugin/src/main/java/com/schbrain/maven/plugin/mojo/PrepareMojo.java new file mode 100644 index 0000000..7884b34 --- /dev/null +++ b/integration/integration-maven-plugin/src/main/java/com/schbrain/maven/plugin/mojo/PrepareMojo.java @@ -0,0 +1,183 @@ +package com.schbrain.maven.plugin.mojo; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.*; +import org.apache.maven.project.MavenProject; + +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.util.HashMap; +import java.util.Map; + +/** + * @author liaozan + * @since 2022/1/4 + */ +@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection"}) +@Mojo(name = "prepare", threadSafe = true, defaultPhase = LifecyclePhase.PACKAGE, + requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, + requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) +public class PrepareMojo extends AbstractMojo { + + private static final String DOCKER_BUILD_INFO = "dockerBuildInfo"; + + /** + * maven project + */ + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; + + /** + * maven session + */ + @Parameter(defaultValue = "${session}", readonly = true, required = true) + private MavenSession session; + + /** + * 构造最终的名字 + */ + @Parameter(defaultValue = "${project.build.finalName}", readonly = true) + private String finalName; + + /** + * 构建输出目录 + */ + @Parameter(defaultValue = "${project.build.directory}", readonly = true) + private File buildDirectory; + + /** + * docker 镜像推送地址 + */ + @Parameter(required = true, defaultValue = "${docker.registry}") + private String dockerRegistry; + + /** + * spring 启动的 profile + */ + @Parameter(required = true, defaultValue = "${spring.profile}") + private String springProfile; + + /** + * 是否包含系统属性 + */ + @Parameter(defaultValue = "false") + private boolean includeSystemProperties; + + /** + * 是否包含系统环境变量 + */ + @Parameter(defaultValue = "false") + private boolean includeSystemEnv; + + /** + * 打包的名字, 为空取 root project artifactId + */ + @Parameter + private String appName; + + /** + * 打包的版本, 为空取 root project version + */ + @Parameter + private String version; + + /** + * 额外的属性 + */ + @Parameter + private Map additionalProperties; + + public void execute() throws MojoExecutionException { + if (shouldSkip()) { + return; + } + + validateParam(); + + Path targetFile = getTargetFile(finalName, buildDirectory); + if (!Files.exists(targetFile)) { + throw new MojoExecutionException("target jar is not present, it's required for build"); + } + try { + Map variables = collectProjectVariables(targetFile); + storeVariablesToFile(variables); + getLog().info("generate build properties: \n" + JSONUtil.toJsonPrettyStr(variables)); + } catch (Exception e) { + throw new MojoExecutionException(e); + } + } + + private void validateParam() throws MojoExecutionException { + MavenProject topLevelProject = session.getTopLevelProject(); + if (StrUtil.isBlank(dockerRegistry)) { + throw new MojoExecutionException("docker.registry is required for build, but found empty value, please check the pom configuration"); + } + if (StrUtil.isBlank(springProfile)) { + throw new MojoExecutionException("spring.profile is required for build, but found empty value, please check the pom configuration"); + } + if (StrUtil.isBlank(appName)) { + appName = topLevelProject.getArtifactId(); + } + if (StrUtil.isBlank(version)) { + version = topLevelProject.getVersion(); + } + } + + private Map collectProjectVariables(Path targetFile) { + MavenProject topProject = session.getTopLevelProject(); + Map variables = new HashMap<>(); + // required for docker build + variables.put("APP_NAME", appName); + variables.put("JAR_FILE", getJarFileRelativePath(topProject.getBasedir(), targetFile)); + variables.put("VERSION", version); + variables.put("REGISTRY", dockerRegistry); + variables.put("PROFILE", springProfile); + // add additionalProperties + if (additionalProperties != null) { + variables.putAll(additionalProperties); + } + // add system properties + if (includeSystemProperties) { + variables.putAll(System.getProperties()); + } + // add system env + if (includeSystemEnv) { + variables.putAll(System.getenv()); + } + return variables; + } + + private void storeVariablesToFile(Map variables) throws IOException { + Path dockerBuildInfo = Paths.get(buildDirectory.getAbsolutePath(), DOCKER_BUILD_INFO); + FileUtil.del(dockerBuildInfo); + Files.createFile(dockerBuildInfo); + FileUtil.writeUtf8Map(variables, dockerBuildInfo.toFile(), "=", false); + } + + private String getJarFileRelativePath(File basedir, Path jarFile) { + return Paths.get(basedir.getPath()).relativize(jarFile).toString(); + } + + private boolean shouldSkip() { + if ("pom".equals(this.project.getPackaging())) { + getLog().warn("docker-build goal could not be applied to pom project"); + return true; + } + return false; + } + + private Path getTargetFile(String finalName, File targetDirectory) { + return Paths.get(targetDirectory.getPath(), withExtension(finalName)); + } + + private String withExtension(String fileName) { + return String.format("%s.%s", fileName, project.getArtifact().getArtifactHandler().getExtension()); + } + +} \ No newline at end of file diff --git a/integration/pom.xml b/integration/pom.xml new file mode 100644 index 0000000..c48c392 --- /dev/null +++ b/integration/pom.xml @@ -0,0 +1,22 @@ + + + + 4.0.0 + + + com.schbrain.framework + schbrain-parent + ${revision} + + + integration + pom + + + integration-maven-plugin + integration-jenkins-plugin + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index f30a17f..02025c2 100644 --- a/pom.xml +++ b/pom.xml @@ -112,11 +112,20 @@ 2.0.0 + 3.8.6 + 3.6.4 1.4.1 ${revision} https://maven.schbrain.com/repository + + commons + starters + support + integration + + diff --git a/starters/.gitignore b/starters/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/starters/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.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 \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/pom.xml b/starters/apollo-spring-boot-starter/pom.xml new file mode 100644 index 0000000..aaecd23 --- /dev/null +++ b/starters/apollo-spring-boot-starter/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + + com.schbrain.framework + starters + ${revision} + + + apollo-spring-boot-starter + + + + com.ctrip.framework.apollo + apollo-client + + + com.schbrain.framework + schbrain-spring-support + + + + \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainApolloAutoConfiguration.java b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainApolloAutoConfiguration.java new file mode 100644 index 0000000..40853e3 --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainApolloAutoConfiguration.java @@ -0,0 +1,24 @@ +package com.schbrain.framework.autoconfigure.apollo; + +import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; +import com.ctrip.framework.apollo.spring.boot.ApolloAutoConfiguration; +import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants; +import com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties; +import com.schbrain.framework.autoconfigure.apollo.properties.ConfigurationPropertiesRegistry; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Import; + +/** + * @author liaozan + * @since 2021/11/15 + */ +@EnableApolloConfig +@Import({SchbrainPropertySourcesProcessor.class, ConfigurationPropertiesRegistry.class}) +@EnableConfigurationProperties(ApolloProperties.class) +@AutoConfiguration(before = ApolloAutoConfiguration.class) +@ConditionalOnProperty(value = PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, matchIfMissing = true) +public class SchbrainApolloAutoConfiguration { + +} \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainApolloPropertiesEnvironmentPostProcessor.java b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainApolloPropertiesEnvironmentPostProcessor.java new file mode 100644 index 0000000..a10981d --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainApolloPropertiesEnvironmentPostProcessor.java @@ -0,0 +1,22 @@ +package com.schbrain.framework.autoconfigure.apollo; + +import com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer; +import com.schbrain.framework.autoconfigure.apollo.util.PropertySourceOrderUtils; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * @author liaozan + * @since 2022/4/19 + */ +@Order(ApolloApplicationContextInitializer.DEFAULT_ORDER + 1) +public class SchbrainApolloPropertiesEnvironmentPostProcessor implements EnvironmentPostProcessor { + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + PropertySourceOrderUtils.adjustPropertySourceOrder(environment); + } + +} \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainPropertySourcesProcessor.java b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainPropertySourcesProcessor.java new file mode 100644 index 0000000..c3b829c --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/SchbrainPropertySourcesProcessor.java @@ -0,0 +1,21 @@ +package com.schbrain.framework.autoconfigure.apollo; + +import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor; +import com.schbrain.framework.autoconfigure.apollo.util.PropertySourceOrderUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * @author liaozan + * @since 2021/12/6 + */ +public class SchbrainPropertySourcesProcessor extends PropertySourcesProcessor { + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + super.postProcessBeanFactory(beanFactory); + PropertySourceOrderUtils.adjustPropertySourceOrder(beanFactory.getBean(ConfigurableEnvironment.class)); + } + +} \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ApolloProperties.java b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ApolloProperties.java new file mode 100644 index 0000000..1d0536d --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ApolloProperties.java @@ -0,0 +1,28 @@ +package com.schbrain.framework.autoconfigure.apollo.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.env.Environment; + +import static com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties.PREFIX; + +/** + * this class MUST NOT load from remote + * + * @author liaozan + * @since 2021/12/6 + */ +@Data +@ConfigurationProperties(prefix = PREFIX) +public class ApolloProperties { + + public static final String PREFIX = "schbrain.apollo"; + + private boolean remoteFirst = false; + + public static ApolloProperties get(Environment environment) { + return Binder.get(environment).bindOrCreate(PREFIX, ApolloProperties.class); + } + +} diff --git a/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ApolloPropertiesPreparer.java b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ApolloPropertiesPreparer.java new file mode 100644 index 0000000..0dfb773 --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ApolloPropertiesPreparer.java @@ -0,0 +1,136 @@ +package com.schbrain.framework.autoconfigure.apollo.properties; + +import com.ctrip.framework.foundation.Foundation; +import com.schbrain.common.util.ApplicationName; +import com.schbrain.common.util.EnvUtils; +import com.schbrain.framework.support.spring.EnvironmentPostProcessorAdapter; +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.StringUtils; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.ctrip.framework.apollo.core.ApolloClientSystemConsts.*; +import static com.ctrip.framework.apollo.core.ConfigConsts.APOLLO_META_KEY; +import static com.ctrip.framework.apollo.spring.config.PropertySourcesConstants.*; + +/** + * @author liaozan + * @since 2021/11/6 + */ +public class ApolloPropertiesPreparer extends EnvironmentPostProcessorAdapter implements Ordered { + + // get properties after configData loaded + public static final Integer ORDER = ConfigDataEnvironmentPostProcessor.ORDER + 1; + public static final String ENV_KEY = "env"; + private static final Map INIT_PROPERTIES = new LinkedHashMap<>(); + + public ApolloPropertiesPreparer(DeferredLogFactory deferredLogFactory, ConfigurableBootstrapContext bootstrapContext) { + super(deferredLogFactory, bootstrapContext); + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (disabled(environment)) { + print("apollo is disabled"); + return; + } + setRequiredProperty(environment); + } + + @Override + public int getOrder() { + return ORDER; + } + + @Override + protected void onBootstrapContextClosed(ConfigurableApplicationContext context) { + context.getBeanFactory().addBeanPostProcessor(new ConfigurablePropertiesBeanPostProcessor(context)); + } + + private boolean disabled(ConfigurableEnvironment environment) { + Boolean enabled = environment.getProperty(APOLLO_BOOTSTRAP_ENABLED, Boolean.class, true); + return Boolean.FALSE.equals(enabled); + } + + private void setRequiredProperty(ConfigurableEnvironment environment) { + String appId = getAppId(environment); + saveProperty(APP_ID, appId); + + String env = getEnv(environment); + saveProperty(ENV_KEY, env); + + String apolloUrl = getApolloUrl(environment, env); + saveProperty(APOLLO_META, apolloUrl); + + saveProperty(APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, true); + saveProperty(APOLLO_BOOTSTRAP_ENABLED, true); + saveProperty(APOLLO_PROPERTY_ORDER_ENABLE, true); + saveProperty(APOLLO_PROPERTY_NAMES_CACHE_ENABLE, true); + + printProperties(); + } + + private void saveProperty(String key, Object value) { + INIT_PROPERTIES.put(key, value); + System.setProperty(key, value.toString()); + } + + private void printProperties() { + INIT_PROPERTIES.forEach((k, v) -> print(k + " : " + v)); + } + + private void print(String message) { + getLog().debug(message); + System.out.println(message); + } + + private String getApolloUrl(ConfigurableEnvironment environment, String env) { + String fallbackKey = env + ".meta"; + + // {env}.meta + String searchKey = fallbackKey; + String apolloUrl = environment.getProperty(searchKey); + if (StringUtils.hasText(apolloUrl)) { + return apolloUrl; + } + + // apollo.meta.{env} + searchKey = APOLLO_META_KEY + "." + env; + apolloUrl = environment.getProperty(searchKey); + if (StringUtils.hasText(apolloUrl)) { + return apolloUrl; + } + + // apollo.meta + searchKey = APOLLO_META_KEY; + apolloUrl = environment.getProperty(searchKey); + if (StringUtils.hasText(apolloUrl)) { + return apolloUrl; + } + + // fallback + return Foundation.getProperty(fallbackKey, null); + } + + private String getEnv(ConfigurableEnvironment environment) { + return EnvUtils.getProfile(environment); + } + + private String getAppId(ConfigurableEnvironment environment) { + String appId; + if (environment.containsProperty(APP_ID)) { + appId = environment.getRequiredProperty(APP_ID); + } else { + appId = ApplicationName.get(environment); + } + return appId; + } + +} \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ConfigurablePropertiesBeanPostProcessor.java b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ConfigurablePropertiesBeanPostProcessor.java new file mode 100644 index 0000000..e51ec63 --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ConfigurablePropertiesBeanPostProcessor.java @@ -0,0 +1,23 @@ +package com.schbrain.framework.autoconfigure.apollo.properties; + +import com.schbrain.common.util.support.ConfigurableProperties; +import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils; +import com.schbrain.framework.support.spring.BeanPostProcessorAdapter; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * @author liaozan + * @since 2022/9/16 + */ +public class ConfigurablePropertiesBeanPostProcessor extends BeanPostProcessorAdapter { + + public ConfigurablePropertiesBeanPostProcessor(ConfigurableApplicationContext applicationContext) { + this.setApplicationContext(applicationContext); + } + + @Override + protected ConfigurableProperties doPostProcessBeforeInstantiation(Class beanClass) { + return ConfigUtils.loadConfig(environment, beanClass); + } + +} \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ConfigurationPropertiesRegistry.java b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ConfigurationPropertiesRegistry.java new file mode 100644 index 0000000..002b1b0 --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/properties/ConfigurationPropertiesRegistry.java @@ -0,0 +1,69 @@ +package com.schbrain.framework.autoconfigure.apollo.properties; + +import cn.hutool.core.text.StrPool; +import com.ctrip.framework.apollo.spring.property.SpringValue; +import com.ctrip.framework.apollo.spring.property.SpringValueRegistry; +import com.ctrip.framework.apollo.spring.util.SpringInjector; +import com.google.common.base.CaseFormat; +import org.apache.commons.collections4.MapUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.boot.context.properties.ConfigurationPropertiesBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Map; + +import static org.springframework.beans.factory.config.PlaceholderConfigurerSupport.*; + +/** + * @author liaozan + * @since 2022/9/19 + */ +public class ConfigurationPropertiesRegistry implements SmartInitializingSingleton, ApplicationContextAware { + + private ApplicationContext applicationContext; + + private AutowireCapableBeanFactory beanFactory; + + private SpringValueRegistry springValueRegistry; + + @Override + public void afterSingletonsInstantiated() { + Map beanMap = ConfigurationPropertiesBean.getAll(applicationContext); + if (MapUtils.isEmpty(beanMap)) { + return; + } + beanMap.forEach((beanName, propertiesBean) -> { + String prefix = propertiesBean.getAnnotation().prefix(); + Object instance = propertiesBean.getInstance(); + ReflectionUtils.doWithFields(instance.getClass(), + field -> register(beanName, prefix, instance, field), + field -> !Modifier.isFinal(field.getModifiers())); + }); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + this.beanFactory = applicationContext.getAutowireCapableBeanFactory(); + this.springValueRegistry = SpringInjector.getInstance(SpringValueRegistry.class); + } + + private void register(String beanName, String prefix, Object instance, Field field) { + String propertyName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, field.getName()); + String key = prefix + StrPool.DOT + propertyName; + String placeholder = toPlaceHolder(key); + SpringValue springValue = new SpringValue(key, placeholder, instance, beanName, field, false); + springValueRegistry.register(beanFactory, key, springValue); + } + + private String toPlaceHolder(String key) { + return DEFAULT_PLACEHOLDER_PREFIX + key + DEFAULT_VALUE_SEPARATOR + DEFAULT_PLACEHOLDER_SUFFIX; + } + +} \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/util/ConfigUtils.java b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/util/ConfigUtils.java new file mode 100644 index 0000000..c546f8d --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/util/ConfigUtils.java @@ -0,0 +1,133 @@ +package com.schbrain.framework.autoconfigure.apollo.util; + +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigService; +import com.ctrip.framework.apollo.enums.ConfigSourceType; +import com.schbrain.common.util.ConfigurationPropertiesUtils; +import com.schbrain.common.util.properties.SchbrainMapPropertySource; +import com.schbrain.common.util.support.ConfigurableProperties; +import com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.boot.DefaultPropertiesPropertySource; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.core.env.*; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import static com.ctrip.framework.apollo.core.ApolloClientSystemConsts.APP_ID; + +/** + * @author liaozan + * @since 2021/12/6 + */ +public class ConfigUtils { + + private static final Log LOGGER = LogFactory.getLog(ConfigUtils.class); + + private static final boolean APOLLO_DISABLED = System.getProperty(APP_ID) == null; + + public static boolean isApolloDisabled() { + return APOLLO_DISABLED; + } + + public static T loadConfig(ConfigurableEnvironment environment, Class propertyClass) { + // load default and local properties + T target = BeanUtils.instantiateClass(propertyClass).bindOrCreate(environment, false); + Map defaultProperties = ConfigurationPropertiesUtils.toMap(target); + // load remote config + Map loadedProperties = loadConfig(target.getNamespace(), target.getPrefix()); + // merge + ApolloProperties apolloProperties = ApolloProperties.get(environment); + Map mergedProperties = mergeProperties(loadedProperties, defaultProperties, apolloProperties.isRemoteFirst()); + // add to environment + addToEnvironment(environment, target.getName(), mergedProperties); + // rebind after addToEnvironment + return target.bindOrCreate(environment, true); + } + + public static Map loadConfig(String namespace) { + return loadConfig(namespace, null); + } + + public static Map loadConfig(String namespace, String prefix) { + if (isApolloDisabled()) { + return new LinkedHashMap<>(); + } + Config config = ConfigService.getConfig(namespace); + if (config.getSourceType() == ConfigSourceType.LOCAL) { + LOGGER.warn(String.format("Failed to get config from Apollo namespace: %s, will use the local cache value", namespace)); + } + + Map configs = new LinkedHashMap<>(); + for (String propertyName : config.getPropertyNames()) { + if (prefix != null) { + if (!propertyName.startsWith(prefix)) { + continue; + } + } + String propertyValue = config.getProperty(propertyName, null); + configs.put(propertyName, propertyValue); + } + return configs; + } + + public static void addToEnvironment(ConfigurableEnvironment environment, T properties) { + addToEnvironment(environment, new SchbrainMapPropertySource(properties.getName(), properties)); + } + + public static void addToEnvironment(ConfigurableEnvironment environment, String name, Map properties) { + addToEnvironment(environment, new SchbrainMapPropertySource(name, properties)); + } + + public static void addToEnvironment(ConfigurableEnvironment environment, MapPropertySource propertySource) { + MutablePropertySources propertySources = environment.getPropertySources(); + String propertySourceName = propertySource.getName(); + + boolean alreadyExist = propertySources.contains(propertySourceName); + if (alreadyExist) { + PropertySource existing = propertySources.get(propertySourceName); + if (existing instanceof MapPropertySource) { + Map existingSource = ((MapPropertySource) existing).getSource(); + existingSource.putAll(propertySource.getSource()); + } else { + LOGGER.warn("Existing propertySource is not an instance of MapPropertySource, overwrite existing..."); + propertySources.replace(propertySourceName, propertySource); + } + } else { + propertySources.addLast(propertySource); + } + resolvePlaceHolders(environment, propertySource); + ConfigurationPropertySources.attach(environment); + DefaultPropertiesPropertySource.moveToEnd(environment); + } + + public static void resolvePlaceHolders(ConfigurableEnvironment environment, MapPropertySource propertySource) { + Map source = propertySource.getSource(); + for (Entry entry : source.entrySet()) { + Object value = entry.getValue(); + if (value instanceof String) { + String resolvedValue = environment.resolvePlaceholders((String) value); + source.put(entry.getKey(), resolvedValue); + } + } + } + + private static Map mergeProperties(Map loadedProperties, + Map defaultProperties, + boolean remoteFirst) { + Map mergedProperties; + if (remoteFirst) { + defaultProperties.putAll(loadedProperties); + mergedProperties = defaultProperties; + } else { + loadedProperties.putAll(defaultProperties); + mergedProperties = loadedProperties; + } + return mergedProperties; + } + +} \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/util/PropertySourceOrderUtils.java b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/util/PropertySourceOrderUtils.java new file mode 100644 index 0000000..a15af5c --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/apollo/util/PropertySourceOrderUtils.java @@ -0,0 +1,53 @@ +package com.schbrain.framework.autoconfigure.apollo.util; + +import com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties; +import org.springframework.boot.DefaultPropertiesPropertySource; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.core.env.*; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.ctrip.framework.apollo.spring.config.PropertySourcesConstants.*; + +/** + * @author liaozan + * @since 2022/4/19 + */ +public class PropertySourceOrderUtils { + + private static final Set APOLLO_PROPERTIES = Set.of(APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, APOLLO_PROPERTY_SOURCE_NAME); + + /** + * Adjust {@link MutablePropertySources} according to the + * {@link ApolloProperties#isRemoteFirst()} to ensure the configuration properties work as expected + */ + public static void adjustPropertySourceOrder(ConfigurableEnvironment environment) { + MutablePropertySources mutablePropertySources = environment.getPropertySources(); + + List> remotePropertySources = mutablePropertySources.stream() + .filter(propertySource -> APOLLO_PROPERTIES.contains(propertySource.getName())) + .collect(Collectors.toList()); + + if (CollectionUtils.isEmpty(remotePropertySources)) { + return; + } + + ApolloProperties apolloProperties = ApolloProperties.get(environment); + for (PropertySource remotePropertySource : remotePropertySources) { + mutablePropertySources.remove(remotePropertySource.getName()); + if (apolloProperties.isRemoteFirst()) { + mutablePropertySources.addFirst(remotePropertySource); + } else { + mutablePropertySources.addLast(remotePropertySource); + } + } + + // Make sure the default configurations always in the end + DefaultPropertiesPropertySource.moveToEnd(environment); + ConfigurationPropertySources.attach(environment); + } + +} \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/resources/META-INF/app.properties b/starters/apollo-spring-boot-starter/src/main/resources/META-INF/app.properties new file mode 100644 index 0000000..776a52a --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/resources/META-INF/app.properties @@ -0,0 +1,5 @@ +default.meta=https://kp-config-dev.schbrain.com +dev.meta=https://kp-config-dev.schbrain.com +test.meta=https://kp-config-test.schbrain.com +pre.meta=http://service-apollo-meta-server-pre.apollo:8080 +prod.meta=http://service-apollo-meta-server-prod.apollo:8080 \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json b/starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..63b9e07 --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,18 @@ +{ + "groups": [ + { + "name": "schbrain.apollo", + "type": "com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties", + "sourceType": "com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties" + } + ], + "properties": [ + { + "name": "schbrain.apollo.remote-first", + "type": "java.lang.Boolean", + "sourceType": "com.schbrain.framework.autoconfigure.apollo.properties.ApolloProperties", + "defaultValue": false + } + ], + "hints": [] +} \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring.factories b/starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..4d43360 --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ + com.schbrain.framework.autoconfigure.apollo.properties.ApolloPropertiesPreparer,\ + com.schbrain.framework.autoconfigure.apollo.SchbrainApolloPropertiesEnvironmentPostProcessor \ No newline at end of file diff --git a/starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..a7f3cb2 --- /dev/null +++ b/starters/apollo-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.framework.autoconfigure.apollo.SchbrainApolloAutoConfiguration \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/pom.xml b/starters/cache-spring-boot-starter/pom.xml new file mode 100644 index 0000000..067dfa1 --- /dev/null +++ b/starters/cache-spring-boot-starter/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + + com.schbrain.framework + starters + ${revision} + + + cache-spring-boot-starter + + + + com.schbrain.framework + apollo-spring-boot-starter + + + org.springframework.boot + spring-boot-starter-data-redis + + + + \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/CacheUtils.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/CacheUtils.java new file mode 100644 index 0000000..28b879d --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/CacheUtils.java @@ -0,0 +1,164 @@ +package com.schbrain.framework.autoconfigure.cache; + +import com.schbrain.framework.autoconfigure.cache.exception.CacheException; +import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; + +import java.time.Duration; +import java.util.*; +import java.util.function.Supplier; + +@Slf4j +public class CacheUtils { + + private static CacheProvider cacheProvider; + + public static CacheProvider getCacheProvider() { + if (cacheProvider == null) { + throw new CacheException("CacheProvider is null, Please ensure the cache config is correct"); + } + return cacheProvider; + } + + public static void setCacheProvider(CacheProvider cacheProvider) { + CacheUtils.cacheProvider = cacheProvider; + } + + /** + * 缓存是否过期 + */ + public static boolean isExpired(String cacheKey) { + return getCacheProvider().isExpired(cacheKey); + } + + /** + * 设置过期时间 + */ + public static void expire(String cacheKey, Duration expiration) { + getCacheProvider().expire(cacheKey, expiration); + } + + /** + * 获取过期时间 + */ + public static Duration getExpire(String cacheKey) { + return getCacheProvider().getExpire(cacheKey); + } + + /** + * 获取缓存数据 + */ + public static T getValue(String cacheKey, Class valueType) { + return getCacheProvider().get(cacheKey, valueType); + } + + /** + * 设置缓存数据 + */ + public static void putValue(String cacheKey, T value, Duration expiration) { + getCacheProvider().set(cacheKey, value, expiration); + } + + /** + * 获取缓存数据列表 + */ + public static List getList(String cacheKey, Class valueType) { + return getCacheProvider().getList(cacheKey, valueType); + } + + /** + * 设置缓存数据列表 + */ + public static void putList(String cacheKey, List value, Duration expiration) { + getCacheProvider().set(cacheKey, value, expiration); + } + + /** + * 设置新的缓存,返回旧的缓存 + */ + public static T getAndSet(String cacheKey, T value, Class valueType, Duration expiration) { + T cachedData = getValue(cacheKey, valueType); + putValue(cacheKey, value, expiration); + return cachedData; + } + + /** + * 设置新的缓存,返回旧的缓存 + */ + public static List getAndSet(String cacheKey, List value, Class valueType, Duration expiration) { + List cachedData = getList(cacheKey, valueType); + putList(cacheKey, value, expiration); + return cachedData; + } + + /** + * 缓存List + */ + public static List getListIfPresent(String cacheKey, Duration expiration, Class valueType, Supplier> dataLoader) { + List cachedData = getList(cacheKey, valueType); + if (CollectionUtils.isNotEmpty(cachedData)) { + return cachedData; + } + List dataList = dataLoader.get(); + if (CollectionUtils.isEmpty(dataList)) { + log.warn("The cacheKey {} did not get value from the cache, and the dataLoader returned empty list", cacheKey); + } else { + putList(cacheKey, dataList, expiration); + } + return dataList; + } + + /** + * 缓存数据 + */ + public static T getValueIfPresent(String cacheKey, Duration expiration, Class valueType, Supplier dataLoader) { + T cachedData = getValue(cacheKey, valueType); + if (cachedData != null) { + return cachedData; + } + cachedData = dataLoader.get(); + if (cachedData == null) { + log.warn("The cacheKey {} did not get value from the cache, and the dataLoader returned empty value", cacheKey); + } else { + putValue(cacheKey, cachedData, expiration); + } + return cachedData; + } + + /** + * 批量获取 + */ + public static Map multiGet(Collection cacheKeys, Class valueType) { + return multiGet(cacheKeys, valueType, true); + } + + /** + * 批量获取 + */ + public static Map multiGet(Collection cacheKeys, Class valueType, boolean discardIfValueIsNull) { + return getCacheProvider().multiGet(cacheKeys, valueType, discardIfValueIsNull); + } + + /** + * 批量设置 + */ + public static void multiSet(Map data, Duration expiration) { + getCacheProvider().multiSet(data, expiration); + } + + /** + * 删除缓存 + */ + public static void del(String... cacheKeys) { + del(Arrays.asList(cacheKeys)); + } + + /** + * 删除缓存 + */ + public static void del(List cacheKeys) { + getCacheProvider().del(cacheKeys); + } + +} \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/SchbrainCacheAutoConfiguration.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/SchbrainCacheAutoConfiguration.java new file mode 100644 index 0000000..cf6800f --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/SchbrainCacheAutoConfiguration.java @@ -0,0 +1,35 @@ +package com.schbrain.framework.autoconfigure.cache; + +import com.schbrain.framework.autoconfigure.cache.properties.CacheProperties; +import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider; +import com.schbrain.framework.autoconfigure.cache.provider.CacheProviderDelegate; +import com.schbrain.framework.autoconfigure.cache.provider.redis.SchbrainRedisCacheConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; + +/** + * AutoConfiguration for schbrain cache + * + * @author zhuyf + * @since 2022/7/25 + */ +@AutoConfiguration(after = RedisAutoConfiguration.class) +@EnableConfigurationProperties(CacheProperties.class) +@Import(SchbrainRedisCacheConfiguration.class) +public class SchbrainCacheAutoConfiguration { + + @Bean + @ConditionalOnBean(CacheProvider.class) + public CacheProviderDelegate cacheServiceDelegate(CacheProvider cacheProvider, CacheProperties cacheProperties, + Environment environment) { + CacheProviderDelegate delegate = new CacheProviderDelegate(cacheProperties, cacheProvider, environment); + CacheUtils.setCacheProvider(delegate); + return delegate; + } + +} \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/exception/CacheException.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/exception/CacheException.java new file mode 100644 index 0000000..a88fe34 --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/exception/CacheException.java @@ -0,0 +1,15 @@ +package com.schbrain.framework.autoconfigure.cache.exception; + +/** + * @author zhuyf + * @since 2022/7/25 + */ +public class CacheException extends RuntimeException { + + private static final long serialVersionUID = -1948413195425101948L; + + public CacheException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/properties/CacheProperties.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/properties/CacheProperties.java new file mode 100644 index 0000000..7eca43c --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/properties/CacheProperties.java @@ -0,0 +1,27 @@ +package com.schbrain.framework.autoconfigure.cache.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author zhuyf + * @since 2022/7/26 + */ +@Data +@ConfigurationProperties(prefix = "schbrain.cache") +public class CacheProperties { + + /** + * cache prefix + */ + private String prefix; + /** + * cache prefix delimiter + */ + private String delimiter = ":"; + /** + * whatever to enable prefix append + */ + private boolean appendPrefix = true; + +} \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/CacheProvider.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/CacheProvider.java new file mode 100644 index 0000000..17b4135 --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/CacheProvider.java @@ -0,0 +1,62 @@ +package com.schbrain.framework.autoconfigure.cache.provider; + +import java.time.Duration; +import java.util.*; + +/** + * @author zhuyf + * @since 2020/9/24 + **/ +public interface CacheProvider { + + /** + * 指定缓存失效时间 + */ + void expire(String cacheKey, Duration expiration); + + /** + * 根据cacheKey 获取过期时间 + */ + Duration getExpire(String cacheKey); + + /** + * 判断cacheKey是否存在 + */ + boolean hasKey(String cacheKey); + + /** + * 删除缓存 + */ + void del(List cacheKeys); + + /** + * 缓存获取 + */ + T get(String cacheKey, Class valueType); + + /** + * 缓存获取 + */ + Map multiGet(Collection cacheKeys, Class valueType, boolean discardIfValueIsNull); + + /** + * list 缓存获取 + */ + List getList(String cacheKey, Class valueType); + + /** + * 缓存放入并设置时间 + */ + void set(String cacheKey, T value, Duration expiration); + + /** + * 缓存放入并设置时间 + */ + void multiSet(Map data, Duration expiration); + + /** + * 查询key是否过期 + */ + boolean isExpired(String cacheKey); + +} \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/CacheProviderDelegate.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/CacheProviderDelegate.java new file mode 100644 index 0000000..ade1051 --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/CacheProviderDelegate.java @@ -0,0 +1,156 @@ +package com.schbrain.framework.autoconfigure.cache.provider; + +import com.google.common.collect.Maps; +import com.schbrain.common.util.ApplicationName; +import com.schbrain.common.util.StreamUtils; +import com.schbrain.framework.autoconfigure.cache.exception.CacheException; +import com.schbrain.framework.autoconfigure.cache.properties.CacheProperties; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.env.Environment; + +import java.time.Duration; +import java.util.*; +import java.util.Map.Entry; + +/** + * @author liaozan + * @since 2022/8/1 + */ +public class CacheProviderDelegate implements CacheProvider { + + private final String prefixWithDelimiter; + + private final CacheProvider cacheProvider; + + public CacheProviderDelegate(CacheProperties properties, CacheProvider cacheProvider, Environment environment) { + this.cacheProvider = cacheProvider; + if (properties.isAppendPrefix()) { + String prefix = properties.getPrefix(); + if (StringUtils.isBlank(prefix)) { + prefix = ApplicationName.get(environment); + } + this.prefixWithDelimiter = prefix + properties.getDelimiter(); + } else { + this.prefixWithDelimiter = null; + } + } + + @Override + public void expire(String cacheKey, Duration expiration) { + checkDuration(expiration); + getCacheProvider().expire(withKeyPrefix(cacheKey), expiration); + } + + @Override + public Duration getExpire(String cacheKey) { + return getCacheProvider().getExpire(withKeyPrefix(cacheKey)); + } + + @Override + public boolean hasKey(String cacheKey) { + return getCacheProvider().hasKey(withKeyPrefix(cacheKey)); + } + + @Override + public void del(List cacheKeys) { + if (CollectionUtils.isEmpty(cacheKeys)) { + return; + } + List keysWithPrefix = StreamUtils.toList(cacheKeys, this::withKeyPrefix); + getCacheProvider().del(keysWithPrefix); + } + + @Override + public T get(String cacheKey, Class valueType) { + return getCacheProvider().get(withKeyPrefix(cacheKey), valueType); + } + + @Override + public Map multiGet(Collection cacheKeys, Class valueType, boolean discardIfValueIsNull) { + if (CollectionUtils.isEmpty(cacheKeys)) { + return Collections.emptyMap(); + } + List keysWithPrefix = StreamUtils.toList(cacheKeys, this::withKeyPrefix); + Map cachedDate = getCacheProvider().multiGet(keysWithPrefix, valueType, discardIfValueIsNull); + Map result = Maps.newHashMapWithExpectedSize(keysWithPrefix.size()); + // 这里不能用 Stream toMap, toMap 不允许 value 是 null + if (MapUtils.isEmpty(cachedDate)) { + if (discardIfValueIsNull) { + result = Collections.emptyMap(); + } else { + for (String cacheKey : keysWithPrefix) { + result.put(removeKeyPrefix(cacheKey), null); + } + } + return result; + } else { + if (discardIfValueIsNull) { + // 值为 null 的key 在实现类获取时已经被丢弃,直接遍历 put 即可 + for (Entry cacheEntry : cachedDate.entrySet()) { + result.put(removeKeyPrefix(cacheEntry.getKey()), cacheEntry.getValue()); + } + } else { + for (String cacheKey : keysWithPrefix) { + result.put(removeKeyPrefix(cacheKey), cachedDate.get(cacheKey)); + } + } + } + return result; + } + + @Override + public List getList(String cacheKey, Class valueType) { + return getCacheProvider().getList(withKeyPrefix(cacheKey), valueType); + } + + @Override + public void set(String cacheKey, T value, Duration expiration) { + checkDuration(expiration); + getCacheProvider().set(withKeyPrefix(cacheKey), value, expiration); + } + + @Override + public void multiSet(Map data, Duration expiration) { + if (MapUtils.isEmpty(data)) { + return; + } + checkDuration(expiration); + Map dataWithPrefixedKey = Maps.newHashMapWithExpectedSize(data.size()); + for (Entry entry : data.entrySet()) { + dataWithPrefixedKey.put(withKeyPrefix(entry.getKey()), entry.getValue()); + } + getCacheProvider().multiSet(dataWithPrefixedKey, expiration); + } + + @Override + public boolean isExpired(String cacheKey) { + return getCacheProvider().isExpired(withKeyPrefix(cacheKey)); + } + + public CacheProvider getCacheProvider() { + return cacheProvider; + } + + protected String withKeyPrefix(String cacheKey) { + if (StringUtils.isBlank(prefixWithDelimiter)) { + return cacheKey; + } + return prefixWithDelimiter + cacheKey; + } + + protected String removeKeyPrefix(String cacheKey) { + if (StringUtils.isBlank(cacheKey)) { + return cacheKey; + } + return StringUtils.removeStart(cacheKey, prefixWithDelimiter); + } + + protected void checkDuration(Duration expiration) { + if (expiration.isZero() || expiration.isNegative()) { + throw new CacheException("expiration must be a positive number"); + } + } + +} \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/redis/RedisCacheProvider.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/redis/RedisCacheProvider.java new file mode 100644 index 0000000..b249805 --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/redis/RedisCacheProvider.java @@ -0,0 +1,149 @@ +package com.schbrain.framework.autoconfigure.cache.provider.redis; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.schbrain.common.util.JacksonUtils; +import com.schbrain.framework.autoconfigure.cache.exception.CacheException; +import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.util.CollectionUtils; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author zhuyf + * @since 2022/7/25 + **/ +@Slf4j +public class RedisCacheProvider implements CacheProvider { + + private static final int DEFAULT_BATCH_SIZE = 100; + + private final StringRedisTemplate redisTemplate; + + public RedisCacheProvider(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + /** + * 指定缓存失效时间 + */ + @Override + public void expire(String key, Duration expiration) { + redisTemplate.expire(key, expiration.toMillis(), TimeUnit.MILLISECONDS); + } + + /** + * 根据key 获取过期时间 + */ + @Override + public Duration getExpire(String key) { + Long expiration = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + if (expiration == null) { + throw new CacheException("should not be null"); + } + return Duration.ofMillis(expiration); + } + + /** + * 判断key是否存在 + */ + @Override + public boolean hasKey(String cacheKey) { + return Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey)); + } + + /** + * 删除缓存 + */ + @Override + public void del(List cacheKeys) { + if (CollectionUtils.isEmpty(cacheKeys)) { + return; + } + redisTemplate.delete(cacheKeys); + } + + /** + * 缓存获取 + */ + @Override + public T get(String cacheKey, Class type) { + return JacksonUtils.getObjectFromJson(getValueFromRedis(cacheKey), type); + } + + @Override + public Map multiGet(Collection cacheKeys, Class type, boolean discardIfValueIsNull) { + Map result = Maps.newHashMapWithExpectedSize(cacheKeys.size()); + Iterables.partition(cacheKeys, DEFAULT_BATCH_SIZE).forEach(subKeys -> { + List valueList = Objects.requireNonNull(redisTemplate.opsForValue().multiGet(subKeys)); + for (int i = 0; i < subKeys.size(); i++) { + T rawValue = JacksonUtils.getObjectFromJson(valueList.get(i), type); + if (discardIfValueIsNull && rawValue == null) { + continue; + } + result.put(subKeys.get(i), rawValue); + } + }); + return result; + } + + /** + * list 缓存获取 + */ + @Override + public List getList(String cacheKey, Class type) { + return JacksonUtils.getListFromJson(getValueFromRedis(cacheKey), type); + } + + /** + * 普通缓存放入并设置时间 + */ + @Override + public void set(String cacheKey, T value, Duration expiration) { + setValueToRedis(cacheKey, value, expiration); + } + + @Override + public void multiSet(Map data, Duration expiration) { + Iterables.partition(data.keySet(), DEFAULT_BATCH_SIZE).forEach(keys -> + redisTemplate.executePipelined((RedisCallback) connection -> { + Map byteMap = Maps.newHashMapWithExpectedSize(keys.size()); + for (String key : keys) { + byteMap.put(key.getBytes(StandardCharsets.UTF_8), JacksonUtils.writeObjectAsBytes(data.get(key))); + } + connection.mSet(byteMap); + for (byte[] rawKey : byteMap.keySet()) { + connection.pExpire(rawKey, expiration.toMillis()); + } + return null; + })); + } + + /** + * 查询key是否过期 + */ + @Override + public boolean isExpired(String cacheKey) { + return !hasKey(cacheKey); + } + + private String getValueFromRedis(String cacheKey) { + if (StringUtils.isBlank(cacheKey)) { + return null; + } + return redisTemplate.opsForValue().get(cacheKey); + } + + private void setValueToRedis(String cacheKey, T value, Duration expiration) { + String cacheDate = JacksonUtils.toJsonString(value); + redisTemplate.opsForValue().set(cacheKey, cacheDate, expiration.toMillis(), TimeUnit.MILLISECONDS); + } + +} \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/redis/SchbrainRedisCacheConfiguration.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/redis/SchbrainRedisCacheConfiguration.java new file mode 100644 index 0000000..e3bebe1 --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/provider/redis/SchbrainRedisCacheConfiguration.java @@ -0,0 +1,26 @@ +package com.schbrain.framework.autoconfigure.cache.provider.redis; + +import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider; +import org.springframework.boot.autoconfigure.condition.*; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * Redis cache configuration + * + * @author liaozan + * @since 2022/8/7 + */ +@ConditionalOnClass(RedisConnectionFactory.class) +public class SchbrainRedisCacheConfiguration { + + @Bean + @ConditionalOnBean(RedisConnectionFactory.class) + @ConditionalOnMissingBean(RedisCacheProvider.class) + public CacheProvider schbrainRedisCacheProvider(RedisConnectionFactory redisConnectionFactory) { + StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(redisConnectionFactory); + return new RedisCacheProvider(stringRedisTemplate); + } + +} \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json b/starters/cache-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..8365f8e --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,32 @@ +{ + "groups": [ + { + "name": "schbrain.cache", + "type": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties", + "sourceType": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties" + } + ], + "properties": [ + { + "name": "schbrain.cache.append-prefix", + "type": "java.lang.Boolean", + "description": "whatever to enable prefix append", + "sourceType": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties", + "defaultValue": true + }, + { + "name": "schbrain.cache.delimiter", + "type": "java.lang.String", + "description": "cache prefix delimiter", + "sourceType": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties", + "defaultValue": ":" + }, + { + "name": "schbrain.cache.prefix", + "type": "java.lang.String", + "description": "cache prefix", + "sourceType": "com.schbrain.framework.autoconfigure.cache.properties.CacheProperties" + } + ], + "hints": [] +} \ No newline at end of file diff --git a/starters/cache-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/starters/cache-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..49d4301 --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.framework.autoconfigure.cache.SchbrainCacheAutoConfiguration \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/pom.xml b/starters/dubbo-spring-boot-starter/pom.xml new file mode 100644 index 0000000..6e3aefe --- /dev/null +++ b/starters/dubbo-spring-boot-starter/pom.xml @@ -0,0 +1,83 @@ + + + + 4.0.0 + + + com.schbrain.framework + starters + ${revision} + + + dubbo-spring-boot-starter + + + + com.schbrain.framework + apollo-spring-boot-starter + + + org.springframework.boot + spring-boot-starter-validation + + + org.apache.zookeeper + zookeeper + + + org.apache.curator + curator-recipes + + + org.apache.curator + curator-x-discovery + + + org.apache.curator + curator-framework + + + org.apache.dubbo + dubbo-common + + + org.apache.dubbo + dubbo-config-spring + + + org.apache.dubbo + dubbo-remoting-netty4 + + + org.apache.dubbo + dubbo-remoting-zookeeper-curator5 + + + org.apache.dubbo + dubbo-rpc-dubbo + + + org.apache.dubbo + dubbo-rpc-triple + + + org.apache.dubbo + dubbo-registry-zookeeper + + + org.apache.dubbo + dubbo-serialization-hessian2 + + + org.apache.dubbo + dubbo-serialization-fastjson2 + + + org.apache.dubbo + dubbo-metadata-report-zookeeper + + + + \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/DubboAutoConfiguration.java b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/DubboAutoConfiguration.java new file mode 100644 index 0000000..a525c49 --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/DubboAutoConfiguration.java @@ -0,0 +1,19 @@ +package com.schbrain.framework.autoconfigure.dubbo; + +import com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties; +import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +import static org.apache.dubbo.config.ConfigKeys.DUBBO_SCAN_BASE_PACKAGES; + +/** + * @author liaozan + * @since 2021/11/5 + */ +@AutoConfiguration +@EnableConfigurationProperties(DubboProperties.class) +@EnableDubbo(scanBasePackages = "${" + DUBBO_SCAN_BASE_PACKAGES + "}") +public class DubboAutoConfiguration { + +} \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/DubboExceptionFilter.java b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/DubboExceptionFilter.java new file mode 100644 index 0000000..8620b5e --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/DubboExceptionFilter.java @@ -0,0 +1,49 @@ +package com.schbrain.framework.autoconfigure.dubbo.filter; + +import cn.hutool.core.exceptions.ExceptionUtil; +import com.schbrain.common.exception.BaseException; +import lombok.extern.slf4j.Slf4j; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.*; +import org.apache.dubbo.rpc.filter.ExceptionFilter; +import org.apache.dubbo.rpc.service.GenericService; + +import java.util.Arrays; + +/** + * @author liaozan + * @since 2022/1/19 + */ +@Slf4j +@Activate(group = CommonConstants.PROVIDER, order = 1) +public class DubboExceptionFilter extends ExceptionFilter { + + @Override + public void onResponse(Result appResponse, Invoker invoker, Invocation invocation) { + if (!appResponse.hasException() || GenericService.class == invoker.getInterface()) { + return; + } + + Throwable cause = ExceptionUtil.getRootCause(appResponse.getException()); + appResponse.setException(cause); + logErrorDetail(invoker, cause); + + if (cause instanceof BaseException) { + return; + } + + super.onResponse(appResponse, invoker, invocation); + } + + private void logErrorDetail(Invoker invoker, Throwable exception) { + RpcServiceContext context = RpcContext.getServiceContext(); + String arguments = Arrays.toString(context.getArguments()); + String serviceName = invoker.getInterface().getSimpleName(); + String methodName = context.getMethodName(); + String remoteHost = context.getRemoteHost(); + String remoteApplication = context.getRemoteApplicationName(); + log.error("Catch rpc exception, client: {}@{}, target: {}#{}, args: {}", remoteApplication, remoteHost, serviceName, methodName, arguments, exception); + } + +} \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/TraceConsumerRpcFilter.java b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/TraceConsumerRpcFilter.java new file mode 100644 index 0000000..b7a2404 --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/TraceConsumerRpcFilter.java @@ -0,0 +1,22 @@ +package com.schbrain.framework.autoconfigure.dubbo.filter; + +import com.schbrain.common.util.TraceIdUtils; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.*; + +/** + * @author liaozan + * @since 2021/10/10 + */ +@Activate(group = CommonConstants.CONSUMER) +public class TraceConsumerRpcFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + String traceId = TraceIdUtils.get(); + RpcContext.getClientAttachment().setAttachment(TraceIdUtils.TRACE_ID, traceId); + return invoker.invoke(invocation); + } + +} \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/TraceProviderRpcFilter.java b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/TraceProviderRpcFilter.java new file mode 100644 index 0000000..fc3b6f5 --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/filter/TraceProviderRpcFilter.java @@ -0,0 +1,22 @@ +package com.schbrain.framework.autoconfigure.dubbo.filter; + +import com.schbrain.common.util.TraceIdUtils; +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.*; + +/** + * @author liaozan + * @since 2021/10/10 + */ +@Activate(group = CommonConstants.PROVIDER) +public class TraceProviderRpcFilter implements Filter { + + @Override + public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { + String traceId = RpcContext.getServerAttachment().getAttachment(TraceIdUtils.TRACE_ID); + TraceIdUtils.set(traceId); + return invoker.invoke(invocation); + } + +} \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/properties/DubboProperties.java b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/properties/DubboProperties.java new file mode 100644 index 0000000..9353d17 --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/properties/DubboProperties.java @@ -0,0 +1,47 @@ +package com.schbrain.framework.autoconfigure.dubbo.properties; + +import com.schbrain.common.util.support.ConfigurableProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.dubbo.config.spring.util.EnvironmentUtils; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * used to fetch dubbo.* config from remote + * + * @author liaozan + * @since 2021/12/6 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ConfigurationProperties(prefix = "dubbo") +public class DubboProperties extends ConfigurableProperties { + + private Map externalConfigurations; + + @Override + public String getDefaultNamespace() { + return "dubbo-common"; + } + + @Override + @SuppressWarnings("unchecked") + public DubboProperties bindOrCreate(ConfigurableEnvironment environment, boolean afterMerge) { + DubboProperties dubboProperties = super.bindOrCreate(environment, afterMerge); + if (afterMerge) { + Map externalConfigurations = new LinkedHashMap<>(EnvironmentUtils.filterDubboProperties(environment)); + Map configuredProperties = dubboProperties.getExternalConfigurations(); + if (configuredProperties == null) { + configuredProperties = new LinkedHashMap<>(); + } + externalConfigurations.putAll(configuredProperties); + dubboProperties.setExternalConfigurations(externalConfigurations); + } + return dubboProperties; + } + +} \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/properties/DubboPropertiesPreparer.java b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/properties/DubboPropertiesPreparer.java new file mode 100644 index 0000000..52beb3d --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/dubbo/properties/DubboPropertiesPreparer.java @@ -0,0 +1,74 @@ +package com.schbrain.framework.autoconfigure.dubbo.properties; + +import com.alibaba.fastjson2.JSONFactory; +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.ApplicationName; +import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils; +import org.apache.dubbo.config.bootstrap.DubboBootstrap; +import org.apache.dubbo.config.spring.ConfigCenterBean; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.apache.dubbo.config.ConfigKeys.DUBBO_SCAN_BASE_PACKAGES; + +/** + * @author liaozan + * @since 2021/10/10 + */ +public class DubboPropertiesPreparer implements EnvironmentPostProcessor, Ordered { + + public static final String DUBBO_APPLICATION_NAME = "dubbo.application.name"; + + public static final Integer DEFAULT_ORDER = Ordered.LOWEST_PRECEDENCE; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + DubboProperties dubboProperties = ConfigUtils.loadConfig(environment, DubboProperties.class); + addAdditionalProperties(environment, application, dubboProperties); + setUpConfigCenter(dubboProperties); + JSONFactory.setUseJacksonAnnotation(false); + } + + @Override + public int getOrder() { + return DEFAULT_ORDER; + } + + private void setUpConfigCenter(DubboProperties dubboProperties) { + ConfigCenterBean configCenterConfig = buildConfigCenter(dubboProperties); + DubboBootstrap.getInstance().configCenter(configCenterConfig); + } + + private ConfigCenterBean buildConfigCenter(DubboProperties dubboProperties) { + ConfigCenterBean configCenter = new ConfigCenterBean(); + configCenter.setExternalConfig(dubboProperties.getExternalConfigurations()); + return configCenter; + } + + private void addAdditionalProperties(ConfigurableEnvironment environment, SpringApplication application, DubboProperties dubboProperties) { + Map configurations = dubboProperties.getExternalConfigurations(); + configurations.put(DUBBO_SCAN_BASE_PACKAGES, getBasePackage(application)); + if (!configurations.containsKey(DUBBO_APPLICATION_NAME)) { + configurations.put(DUBBO_APPLICATION_NAME, ApplicationName.get(environment)); + } + Map properties = new LinkedHashMap<>(dubboProperties.getExternalConfigurations()); + ConfigUtils.addToEnvironment(environment, dubboProperties.getName(), properties); + } + + private String getBasePackage(SpringApplication application) { + return application.getAllSources() + .stream() + .filter(Class.class::isInstance) + .map(source -> (Class) source) + .map(Class::getPackage) + .map(Package::getName) + .findFirst() + .orElseThrow(() -> new BaseException("should never go here")); + } + +} \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter b/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter new file mode 100644 index 0000000..a9e7287 --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter @@ -0,0 +1,3 @@ +trace-consumer=com.schbrain.framework.autoconfigure.dubbo.filter.TraceConsumerRpcFilter +trace-provider=com.schbrain.framework.autoconfigure.dubbo.filter.TraceProviderRpcFilter +dubboException=com.schbrain.framework.autoconfigure.dubbo.filter.DubboExceptionFilter diff --git a/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json b/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..178c182 --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,32 @@ +{ + "groups": [ + { + "name": "dubbo", + "type": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties", + "sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties" + } + ], + "properties": [ + { + "name": "dubbo.external-configurations", + "type": "java.util.Map", + "sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties" + }, + { + "name": "dubbo.name", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties" + }, + { + "name": "dubbo.namespace", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties" + }, + { + "name": "dubbo.prefix", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.dubbo.properties.DubboProperties" + } + ], + "hints": [] +} \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring.factories b/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..8ec6885 --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.env.EnvironmentPostProcessor=com.schbrain.framework.autoconfigure.dubbo.properties.DubboPropertiesPreparer \ No newline at end of file diff --git a/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..12f9c99 --- /dev/null +++ b/starters/dubbo-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.framework.autoconfigure.dubbo.DubboAutoConfiguration \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/pom.xml b/starters/logger-spring-boot-starter/pom.xml new file mode 100644 index 0000000..86afe3d --- /dev/null +++ b/starters/logger-spring-boot-starter/pom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + + + com.schbrain.framework + starters + ${revision} + + + logger-spring-boot-starter + + + + com.schbrain.framework + apollo-spring-boot-starter + + + org.apache.skywalking + apm-toolkit-trace + + + org.apache.skywalking + apm-toolkit-logback-1.x + + + net.logstash.logback + logstash-logback-encoder + + + + \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/JsonLoggerInitializer.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/JsonLoggerInitializer.java new file mode 100644 index 0000000..22226e9 --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/JsonLoggerInitializer.java @@ -0,0 +1,155 @@ +package com.schbrain.framework.autoconfigure.logger; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.*; +import ch.qos.logback.core.rolling.RollingFileAppender; +import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; +import cn.hutool.json.JSONObject; +import com.schbrain.common.util.*; +import com.schbrain.common.util.InetUtils.HostInfo; +import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils; +import com.schbrain.framework.autoconfigure.logger.logstash.SchbrainLogstashEncoder; +import com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import net.logstash.logback.appender.LogstashTcpSocketAppender; +import net.logstash.logback.encoder.LogstashEncoder; +import net.logstash.logback.fieldnames.ShortenedFieldNames; +import org.apache.commons.collections4.IteratorUtils; +import org.slf4j.LoggerFactory; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.nio.file.Paths; +import java.util.List; + +/** + * Enable the json logging, will be auto active when the application is running in cloudPlatform + * + * @author liaozan + * @see CloudPlatform + * @since 2021/12/11 + */ +@Slf4j +@Setter +public class JsonLoggerInitializer implements ApplicationContextInitializer { + + /** + * 暴露 set 方法以便单独启动日志服务时进行配置 + */ + private ConfigurableEnvironment environment; + private LoggingFileProperties loggingFileProperties; + private String applicationName; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + this.environment = applicationContext.getEnvironment(); + this.loggingFileProperties = ConfigUtils.loadConfig(environment, LoggingFileProperties.class); + this.applicationName = ApplicationName.get(environment); + this.init(); + } + + public void init() { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + for (Logger logger : context.getLoggerList()) { + registerAppender(logger, context); + } + } + + private void registerAppender(Logger logger, LoggerContext context) { + List> appenderList = getAppenderList(logger); + if (CollectionUtils.isEmpty(appenderList)) { + return; + } + + if (loggingFileProperties.isEnableJsonFileOutput()) { + Appender appender = buildFileAppender(context); + logger.addAppender(appender); + } + + if (loggingFileProperties.isEnableJsonConsoleOutput()) { + Appender appender = buildConsoleAppender(context); + logger.addAppender(appender); + } + + if (loggingFileProperties.isEnableJsonLogWriteToLogstash() || EnvUtils.runningOnCloudPlatform(environment)) { + if (!StringUtils.hasText(loggingFileProperties.getLogstashAddress())) { + log.warn("logstash address is unset, will NOT write log to logstash"); + return; + } + Appender logstashAppender = buildLogstashAppender(context); + logger.addAppender(logstashAppender); + } + } + + private Appender buildLogstashAppender(LoggerContext context) { + LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender(); + appender.setContext(context); + appender.addDestination(loggingFileProperties.getLogstashAddress()); + appender.setEncoder(createJsonEncoder(context)); + appender.start(); + return appender; + } + + private List> getAppenderList(Logger logger) { + return IteratorUtils.toList(logger.iteratorForAppenders()); + } + + private Appender buildConsoleAppender(LoggerContext loggerContext) { + ConsoleAppender appender = new ConsoleAppender<>(); + appender.setContext(loggerContext); + appender.setEncoder(createJsonEncoder(loggerContext)); + appender.start(); + return appender; + } + + private LogstashEncoder createJsonEncoder(LoggerContext loggerContext) { + LogstashEncoder logstashEncoder = new SchbrainLogstashEncoder(); + logstashEncoder.setContext(loggerContext); + logstashEncoder.setFieldNames(new ShortenedFieldNames()); + logstashEncoder.setShortenedLoggerNameLength(40); + logstashEncoder.setCustomFields(getCustomFields()); + logstashEncoder.start(); + return logstashEncoder; + } + + private String getCustomFields() { + HostInfo hostInfo = InetUtils.findFirstNonLoopBackHostInfo(); + JSONObject customFields = new JSONObject(); + customFields.set("appName", applicationName); + customFields.set("hostName", hostInfo.getHostname()); + customFields.set("podIp", hostInfo.getIpAddress()); + return customFields.toString(); + } + + private Appender buildFileAppender(LoggerContext loggerContext) { + RollingFileAppender appender = new RollingFileAppender<>(); + appender.setContext(loggerContext); + appender.setFile(getPathLocation("json/json.log")); + appender.setRollingPolicy(createRollingPolicy(loggerContext, appender)); + appender.setEncoder(createJsonEncoder(loggerContext)); + appender.start(); + return appender; + } + + private String getPathLocation(String path) { + return Paths.get(loggingFileProperties.getLogPath(), path).toString(); + } + + private TimeBasedRollingPolicy createRollingPolicy(Context context, FileAppender appender) { + TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy<>(); + rollingPolicy.setMaxHistory(loggingFileProperties.getMaxHistory()); + rollingPolicy.setFileNamePattern(getPathLocation("json/json-%d{yyyy-MM-dd}.log")); + rollingPolicy.setContext(context); + rollingPolicy.setParent(appender); + rollingPolicy.start(); + return rollingPolicy; + } + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/LoggerAutoConfiguration.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/LoggerAutoConfiguration.java new file mode 100644 index 0000000..ed9c4bd --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/LoggerAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.schbrain.framework.autoconfigure.logger; + +import com.schbrain.framework.autoconfigure.logger.apollo.DynamicLoggerConfiguration; +import com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; + +/** + * @author liaozan + * @since 2021/11/19 + */ +@AutoConfiguration +@Import(DynamicLoggerConfiguration.class) +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +@EnableConfigurationProperties(LoggingFileProperties.class) +public class LoggerAutoConfiguration { + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/DynamicLoggerConfiguration.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/DynamicLoggerConfiguration.java new file mode 100644 index 0000000..eb6bd00 --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/DynamicLoggerConfiguration.java @@ -0,0 +1,77 @@ +package com.schbrain.framework.autoconfigure.logger.apollo; + +import com.ctrip.framework.apollo.*; +import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; +import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils; +import com.schbrain.framework.autoconfigure.logger.apollo.listener.LoggingConfigFileChangeListener; +import com.schbrain.framework.autoconfigure.logger.apollo.listener.LoggingLevelChangeListener; +import com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.StringUtils; + +/** + * 动态日志配置 + * + * @author liaozan + * @since 2021/11/19 + **/ +@Slf4j +@EnableConfigurationProperties(LoggingNamespaceProperties.class) +public class DynamicLoggerConfiguration { + + private final ConfigurableEnvironment environment; + private final LoggingSystem loggingSystem; + private final LoggingNamespaceProperties loggerProperties; + + public DynamicLoggerConfiguration(ConfigurableEnvironment environment, LoggingSystem loggingSystem, + LoggingNamespaceProperties loggerProperties) { + this.environment = environment; + this.loggingSystem = loggingSystem; + this.loggerProperties = loggerProperties; + this.init(); + } + + private void init() { + if (ConfigUtils.isApolloDisabled()) { + return; + } + listenToLoggingLevelChange(); + listenToLoggingConfigFileChange(); + } + + private void listenToLoggingConfigFileChange() { + String loggerConfigFileNamespace = loggerProperties.getLoggerConfigFile(); + if (!StringUtils.hasText(loggerConfigFileNamespace)) { + log.debug("logger config file reload is disabled"); + return; + } + + log.debug("init logger config file listener, config file namespace: {}", loggerConfigFileNamespace); + + ConfigFile loggingConfiguration = ConfigService.getConfigFile(loggerConfigFileNamespace, ConfigFileFormat.XML); + if (!loggingConfiguration.hasContent()) { + return; + } + loggingConfiguration.addChangeListener(new LoggingConfigFileChangeListener(loggingSystem, environment, loggerConfigFileNamespace)); + } + + private void listenToLoggingLevelChange() { + String loggerNamespace = loggerProperties.getLogger(); + if (!StringUtils.hasText(loggerNamespace)) { + log.debug("logger level reload is disabled"); + return; + } + + log.debug("init logger level listener, logger namespace: {}", loggerNamespace); + + Config config = ConfigService.getConfig(loggerNamespace); + if (config == null) { + return; + } + config.addChangeListener(new LoggingLevelChangeListener(loggingSystem)); + } + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/listener/LoggingConfigFileChangeListener.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/listener/LoggingConfigFileChangeListener.java new file mode 100644 index 0000000..fcc563b --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/listener/LoggingConfigFileChangeListener.java @@ -0,0 +1,57 @@ +package com.schbrain.framework.autoconfigure.logger.apollo.listener; + +import cn.hutool.extra.spring.SpringUtil; +import com.ctrip.framework.apollo.ConfigFileChangeListener; +import com.ctrip.framework.apollo.model.ConfigFileChangeEvent; +import com.schbrain.framework.autoconfigure.logger.JsonLoggerInitializer; +import com.schbrain.framework.autoconfigure.logger.util.LoggerUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.logging.*; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * @author liaozan + * @since 2021/11/8 + */ +@Slf4j +public class LoggingConfigFileChangeListener implements ConfigFileChangeListener { + + private final LoggingSystem loggingSystem; + private final ConfigurableEnvironment environment; + private final String loggerFileName; + + public LoggingConfigFileChangeListener(LoggingSystem loggingSystem, ConfigurableEnvironment environment, String loggerFileName) { + this.loggingSystem = loggingSystem; + this.environment = environment; + this.loggerFileName = loggerFileName; + } + + @Override + public void onChange(ConfigFileChangeEvent changeEvent) { + String content = changeEvent.getNewValue(); + if (!StringUtils.hasText(content)) { + log.warn("Empty logging configuration, reInitialize loggingSystem is disabled"); + return; + } + String configurationLocation = LoggerUtils.storeConfiguration(loggerFileName, content); + if (configurationLocation == null) { + return; + } + reinitialize(configurationLocation); + log.debug("ReInitialize loggingSystem, configFile location: {}", configurationLocation); + } + + private void reinitialize(String configLocation) { + List configurations = loggingSystem.getLoggerConfigurations(); + loggingSystem.cleanUp(); + loggingSystem.initialize(new LoggingInitializationContext(environment), configLocation, null); + configurations.forEach(configuration -> loggingSystem.setLogLevel(configuration.getName(), configuration.getConfiguredLevel())); + // reInitialize json logger + new JsonLoggerInitializer().initialize(SpringUtil.getBean(ConfigurableApplicationContext.class)); + } + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/listener/LoggingLevelChangeListener.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/listener/LoggingLevelChangeListener.java new file mode 100644 index 0000000..0ea011d --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/apollo/listener/LoggingLevelChangeListener.java @@ -0,0 +1,75 @@ +package com.schbrain.framework.autoconfigure.logger.apollo.listener; + +import com.ctrip.framework.apollo.ConfigChangeListener; +import com.ctrip.framework.apollo.enums.PropertyChangeType; +import com.ctrip.framework.apollo.model.ConfigChange; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.logging.*; + +import java.util.Set; + +import static com.ctrip.framework.apollo.enums.PropertyChangeType.*; +import static org.springframework.boot.logging.LoggingSystem.ROOT_LOGGER_NAME; + +/** + * @author liaozan + * @since 2021/11/8 + */ +@Slf4j +public class LoggingLevelChangeListener implements ConfigChangeListener { + + private static final String LOGGER_TAG = "logging.level."; + + private final LoggingSystem loggingSystem; + + public LoggingLevelChangeListener(LoggingSystem loggingSystem) { + this.loggingSystem = loggingSystem; + } + + @Override + public void onChange(ConfigChangeEvent changeEvent) { + Set changedKeys = changeEvent.changedKeys(); + for (String key : changedKeys) { + if (key.startsWith(LOGGER_TAG)) { + String loggerName = key.substring(LOGGER_TAG.length()); + LogLevel newLogLevel = getNewLogLevel(changeEvent, key); + configureLoggerLevel(loggerName, newLogLevel); + } + } + } + + private LogLevel getNewLogLevel(ConfigChangeEvent changeEvent, String key) { + // default is INFO + LogLevel newLogLevel = LogLevel.INFO; + ConfigChange configChange = changeEvent.getChange(key); + PropertyChangeType changeType = configChange.getChangeType(); + if (changeType == ADDED || changeType == MODIFIED) { + String newValue = configChange.getNewValue(); + newLogLevel = LogLevel.valueOf(newValue.toUpperCase()); + } + return newLogLevel; + } + + private void configureLoggerLevel(String loggerName, LogLevel logLevel) { + LoggerConfiguration configuration = getLoggerConfiguration(loggerName); + LogLevel configuredLevel = configuration.getConfiguredLevel(); + if (configuredLevel == logLevel) { + return; + } + loggingSystem.setLogLevel(loggerName, logLevel); + log.info("change [{}] logger level from {} to {}", loggerName, configuredLevel, logLevel); + } + + private LoggerConfiguration getLoggerConfiguration(String loggerName) { + LoggerConfiguration configuration = loggingSystem.getLoggerConfiguration(loggerName); + if (configuration == null) { + configuration = loggingSystem.getLoggerConfiguration(loggerName.toUpperCase()); + } + if (configuration == null) { + configuration = loggingSystem.getLoggerConfiguration(ROOT_LOGGER_NAME); + } + return configuration; + } + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logback/LogbackTraceIdConfiguration.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logback/LogbackTraceIdConfiguration.java new file mode 100644 index 0000000..3338fb2 --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logback/LogbackTraceIdConfiguration.java @@ -0,0 +1,20 @@ +package com.schbrain.framework.autoconfigure.logger.logback; + +import ch.qos.logback.classic.LoggerContext; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * @author liaozan + * @since 2023-04-08 + */ +public class LogbackTraceIdConfiguration implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.addTurboFilter(new TraceIdInitializeTurboFilter()); + } + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logback/TraceIdInitializeTurboFilter.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logback/TraceIdInitializeTurboFilter.java new file mode 100644 index 0000000..d5ea0fc --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logback/TraceIdInitializeTurboFilter.java @@ -0,0 +1,23 @@ +package com.schbrain.framework.autoconfigure.logger.logback; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.turbo.TurboFilter; +import ch.qos.logback.core.spi.FilterReply; +import com.schbrain.common.util.TraceIdUtils; +import org.slf4j.Marker; + +/** + * @author liaozan + * @since 2023-04-08 + */ +public class TraceIdInitializeTurboFilter extends TurboFilter { + + @Override + public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable throwable) { + // Make sure the traceId is initialized + TraceIdUtils.get(); + return FilterReply.NEUTRAL; + } + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/EventDateStringValueJsonProvider.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/EventDateStringValueJsonProvider.java new file mode 100644 index 0000000..ffed3d7 --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/EventDateStringValueJsonProvider.java @@ -0,0 +1,36 @@ +package com.schbrain.framework.autoconfigure.logger.logstash; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import cn.hutool.core.date.LocalDateTimeUtil; +import com.fasterxml.jackson.core.JsonGenerator; +import net.logstash.logback.composite.AbstractFieldJsonProvider; +import net.logstash.logback.composite.JsonWritingUtils; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @author liaozan + * @since 2022/1/11 + */ +public class EventDateStringValueJsonProvider extends AbstractFieldJsonProvider { + + public static final String FIELD_EVENT_DATE = "eventDate"; + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + + public EventDateStringValueJsonProvider() { + setFieldName(FIELD_EVENT_DATE); + } + + @Override + public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException { + JsonWritingUtils.writeStringField(generator, FIELD_EVENT_DATE, getEventDate(event)); + } + + private String getEventDate(ILoggingEvent event) { + LocalDateTime eventTime = LocalDateTimeUtil.of(event.getTimeStamp()); + return DATE_TIME_FORMATTER.format(eventTime); + } + +} diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/SchbrainLogstashEncoder.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/SchbrainLogstashEncoder.java new file mode 100644 index 0000000..f8b16d3 --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/SchbrainLogstashEncoder.java @@ -0,0 +1,18 @@ +package com.schbrain.framework.autoconfigure.logger.logstash; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import net.logstash.logback.composite.AbstractCompositeJsonFormatter; +import net.logstash.logback.encoder.LogstashEncoder; + +/** + * @author liaozan + * @since 2022/1/4 + */ +public class SchbrainLogstashEncoder extends LogstashEncoder { + + @Override + protected AbstractCompositeJsonFormatter createFormatter() { + return new SchbrainLogstashFormatter(this); + } + +} diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/SchbrainLogstashFormatter.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/SchbrainLogstashFormatter.java new file mode 100644 index 0000000..bd1de64 --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/logstash/SchbrainLogstashFormatter.java @@ -0,0 +1,66 @@ +package com.schbrain.framework.autoconfigure.logger.logstash; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.spi.ContextAware; +import net.logstash.logback.LogstashFormatter; +import net.logstash.logback.composite.JsonProvider; +import net.logstash.logback.composite.LogstashVersionJsonProvider; +import net.logstash.logback.composite.loggingevent.LogLevelValueJsonProvider; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author liaozan + * @since 2022/1/4 + */ +public class SchbrainLogstashFormatter extends LogstashFormatter { + + public SchbrainLogstashFormatter(ContextAware declaredOrigin) { + super(declaredOrigin); + configureProviders(); + } + + private void configureProviders() { + removeUnnecessaryProviders(); + addAdditionalProviders(); + } + + private void addAdditionalProviders() { + getProviders().addProvider(new EventDateStringValueJsonProvider()); + } + + /** + * be careful!!! getProviders().getProviders() is a unmodifiable List + */ + private void removeUnnecessaryProviders() { + List> providers = getProviders().getProviders(); + if (CollectionUtils.isEmpty(providers)) { + return; + } + + List> excludeProviders = getExcludeProviders(providers); + if (CollectionUtils.isEmpty(excludeProviders)) { + return; + } + + for (JsonProvider excludeProvider : excludeProviders) { + getProviders().removeProvider(excludeProvider); + } + } + + private List> getExcludeProviders(List> providers) { + List> excludeProviders = new ArrayList<>(); + for (JsonProvider provider : providers) { + if (provider instanceof LogLevelValueJsonProvider) { + excludeProviders.add(provider); + } + if (provider instanceof LogstashVersionJsonProvider) { + excludeProviders.add(provider); + } + } + return excludeProviders; + } + +} diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggerPropertiesPreparer.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggerPropertiesPreparer.java new file mode 100644 index 0000000..fc03f6e --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggerPropertiesPreparer.java @@ -0,0 +1,96 @@ +package com.schbrain.framework.autoconfigure.logger.properties; + +import com.google.common.collect.Maps; +import com.schbrain.common.util.InetUtils; +import com.schbrain.common.util.InetUtils.HostInfo; +import com.schbrain.framework.autoconfigure.apollo.properties.ApolloPropertiesPreparer; +import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils; +import com.schbrain.framework.autoconfigure.logger.util.LoggerUtils; +import com.schbrain.framework.support.spring.EnvironmentPostProcessorAdapter; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.logging.LoggingApplicationListener; +import org.springframework.boot.logging.*; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.util.Map; + +import static com.schbrain.framework.autoconfigure.logger.util.LoggerUtils.getLoggerConfigurationLocation; +import static org.springframework.boot.context.logging.LoggingApplicationListener.CONFIG_PROPERTY; + +/** + * @author liaozan + * @since 2021/11/19 + */ +public class LoggerPropertiesPreparer extends EnvironmentPostProcessorAdapter implements Ordered { + + public LoggerPropertiesPreparer(DeferredLogFactory factory, ConfigurableBootstrapContext bootstrapContext) { + super(factory, bootstrapContext); + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + LoggerUtils.setLogFactory(getDeferredLogFactory()); + bindLoggingProperties(environment); + bindHostInfoProperty(environment); + earlyLoadLoggingConfig(environment); + } + + @Override + public int getOrder() { + // Configure after the apollo property is initialize + return ApolloPropertiesPreparer.ORDER + 1; + } + + @Override + public void onBootstrapContextClosed(ConfigurableApplicationContext applicationContext) { + ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); + if (!beanFactory.containsSingleton(HostInfo.NAME)) { + HostInfo hostInfo = InetUtils.findFirstNonLoopBackHostInfo(); + beanFactory.registerSingleton(HostInfo.NAME, hostInfo); + } + } + + private void bindHostInfoProperty(ConfigurableEnvironment environment) { + HostInfo hostInfo = InetUtils.findFirstNonLoopBackHostInfo(); + Map source = Maps.newHashMapWithExpectedSize(2); + source.put("application.hostname", hostInfo.getHostname()); + source.put("application.ipAddress", hostInfo.getIpAddress()); + ConfigUtils.addToEnvironment(environment, HostInfo.NAME, source); + } + + /** + * @see LoggingApplicationListener#setLogLevels(LoggingSystem, ConfigurableEnvironment) + */ + @SuppressWarnings("JavadocReference") + private void earlyLoadLoggingConfig(ConfigurableEnvironment environment) { + LoggingNamespaceProperties properties = ConfigUtils.loadConfig(environment, LoggingNamespaceProperties.class); + // load logging level properties + Map loggingLevelProperties = ConfigUtils.loadConfig(properties.getLogger()); + ConfigUtils.addToEnvironment(environment, "loggingLevelProperties", loggingLevelProperties); + // load logging file properties + ConfigUtils.loadConfig(environment, LoggingFileProperties.class); + } + + /** + * Add {@link LoggingApplicationListener#CONFIG_PROPERTY} property to SystemProperty + * + * @see LoggingApplicationListener#initializeSystem(ConfigurableEnvironment, LoggingSystem, LogFile) + */ + @SuppressWarnings("JavadocReference") + private void bindLoggingProperties(ConfigurableEnvironment environment) { + if (environment.containsProperty(CONFIG_PROPERTY)) { + return; + } + String loggerConfigurationLocation = getLoggerConfigurationLocation(environment); + if (loggerConfigurationLocation == null) { + return; + } + System.setProperty(CONFIG_PROPERTY, loggerConfigurationLocation); + getLog().debug(String.format("%s is set to %s", CONFIG_PROPERTY, loggerConfigurationLocation)); + } + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggingFileProperties.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggingFileProperties.java new file mode 100644 index 0000000..0de111e --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggingFileProperties.java @@ -0,0 +1,33 @@ +package com.schbrain.framework.autoconfigure.logger.properties; + +import com.schbrain.common.util.support.ConfigurableProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +/** + * @author liaozan + * @since 2021/12/11 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ConfigurationProperties(prefix = "schbrain.logging.file") +public class LoggingFileProperties extends ConfigurableProperties { + + public static final String DEFAULT_LOG_PATH = "/data/logs"; + + private boolean enableJsonConsoleOutput = false; + + private boolean enableJsonFileOutput = false; + + private boolean enableJsonLogWriteToLogstash = false; + + private String logstashAddress; + + private String logPath = DEFAULT_LOG_PATH; + + private int maxHistory = (int) Duration.ofDays(30).toDays(); + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggingNamespaceProperties.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggingNamespaceProperties.java new file mode 100644 index 0000000..72b9399 --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/properties/LoggingNamespaceProperties.java @@ -0,0 +1,21 @@ +package com.schbrain.framework.autoconfigure.logger.properties; + +import com.schbrain.common.util.support.ConfigurableProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author liaozan + * @since 2021/11/24 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ConfigurationProperties(prefix = "schbrain.logging.namespace") +public class LoggingNamespaceProperties extends ConfigurableProperties { + + private String logger = "logger-common"; + + private String loggerConfigFile = "logback-spring"; + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/util/LoggerUtils.java b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/util/LoggerUtils.java new file mode 100644 index 0000000..af92f92 --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/logger/util/LoggerUtils.java @@ -0,0 +1,59 @@ +package com.schbrain.framework.autoconfigure.logger.util; + +import cn.hutool.system.SystemUtil; +import com.ctrip.framework.apollo.ConfigFile; +import com.ctrip.framework.apollo.ConfigService; +import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; +import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils; +import com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties; +import org.apache.commons.logging.Log; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.StringUtils; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; + +/** + * @author liaozan + * @since 2021/11/15 + */ +public class LoggerUtils { + + private static Log LOGGER; + + public static void setLogFactory(DeferredLogFactory deferredLog) { + LOGGER = deferredLog.getLog(LoggerUtils.class); + } + + @Nullable + public static String getLoggerConfigurationLocation(ConfigurableEnvironment environment) { + if (ConfigUtils.isApolloDisabled()) { + return null; + } + LoggingNamespaceProperties properties = ConfigUtils.loadConfig(environment, LoggingNamespaceProperties.class); + String namespace = properties.getLoggerConfigFile(); + ConfigFile loggingConfiguration = ConfigService.getConfigFile(namespace, ConfigFileFormat.XML); + String content = loggingConfiguration.getContent(); + if (!StringUtils.hasText(content)) { + LOGGER.warn("empty logging configuration, reinitialize loggingSystem is disabled"); + return null; + } + return storeConfiguration(namespace, content); + } + + @Nullable + public static String storeConfiguration(String fileName, String content) { + String tempDir = SystemUtil.getUserInfo().getTempDir(); + Path storeLocation = Paths.get(tempDir, fileName + ".xml"); + try { + return Files.write(storeLocation, content.getBytes(StandardCharsets.UTF_8)).toString(); + } catch (IOException e) { + LOGGER.warn("failed to write logging file, will not behave as expected", e); + return null; + } + } + +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json b/starters/logger-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..42dc0a3 --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,94 @@ +{ + "groups": [ + { + "name": "schbrain.logging.file", + "type": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties" + }, + { + "name": "schbrain.logging.namespace", + "type": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties" + } + ], + "properties": [ + { + "name": "schbrain.logging.file.enable-json-console-output", + "type": "java.lang.Boolean", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties", + "defaultValue": false + }, + { + "name": "schbrain.logging.file.enable-json-file-output", + "type": "java.lang.Boolean", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties", + "defaultValue": false + }, + { + "name": "schbrain.logging.file.enable-json-log-write-to-logstash", + "type": "java.lang.Boolean", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties", + "defaultValue": false + }, + { + "name": "schbrain.logging.file.log-path", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties", + "defaultValue": "\/data\/logs" + }, + { + "name": "schbrain.logging.file.logstash-address", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties" + }, + { + "name": "schbrain.logging.file.max-history", + "type": "java.lang.Integer", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties", + "defaultValue": 0 + }, + { + "name": "schbrain.logging.file.name", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties" + }, + { + "name": "schbrain.logging.file.namespace", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties" + }, + { + "name": "schbrain.logging.file.prefix", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties" + }, + { + "name": "schbrain.logging.namespace.logger", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties", + "defaultValue": "logger-common" + }, + { + "name": "schbrain.logging.namespace.logger-config-file", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties", + "defaultValue": "logback-spring" + }, + { + "name": "schbrain.logging.namespace.name", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties" + }, + { + "name": "schbrain.logging.namespace.namespace", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties" + }, + { + "name": "schbrain.logging.namespace.prefix", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.logger.properties.LoggingNamespaceProperties" + } + ], + "hints": [] +} \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/resources/META-INF/spring.factories b/starters/logger-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..989351d --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.context.ApplicationContextInitializer=\ + com.schbrain.framework.autoconfigure.logger.JsonLoggerInitializer,\ + com.schbrain.framework.autoconfigure.logger.logback.LogbackTraceIdConfiguration +org.springframework.boot.env.EnvironmentPostProcessor=com.schbrain.framework.autoconfigure.logger.properties.LoggerPropertiesPreparer \ No newline at end of file diff --git a/starters/logger-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/starters/logger-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..fe3480c --- /dev/null +++ b/starters/logger-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.framework.autoconfigure.logger.LoggerAutoConfiguration \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/pom.xml b/starters/mybatis-spring-boot-starter/pom.xml new file mode 100644 index 0000000..58cf0bb --- /dev/null +++ b/starters/mybatis-spring-boot-starter/pom.xml @@ -0,0 +1,47 @@ + + + + 4.0.0 + + + com.schbrain.framework + starters + ${revision} + + + mybatis-spring-boot-starter + + + + com.schbrain.framework + apollo-spring-boot-starter + + + com.mysql + mysql-connector-j + + + com.baomidou + mybatis-plus-boot-starter + + + com.zaxxer + HikariCP + + + com.alibaba + druid-spring-boot-starter + true + + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus.version} + test + + + + \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/MybatisAutoConfiguration.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/MybatisAutoConfiguration.java new file mode 100644 index 0000000..936f8c9 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/MybatisAutoConfiguration.java @@ -0,0 +1,99 @@ +package com.schbrain.framework.autoconfigure.mybatis; + +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer; +import com.baomidou.mybatisplus.core.injector.ISqlInjector; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.schbrain.framework.autoconfigure.mybatis.biz.BizIdInjectInterceptor; +import com.schbrain.framework.autoconfigure.mybatis.configuration.MybatisConfigurationCustomizer; +import com.schbrain.framework.autoconfigure.mybatis.configuration.MybatisPlusGlobalConfigCustomizer; +import com.schbrain.framework.autoconfigure.mybatis.datasource.DataSourceConnectionPostProcessor; +import com.schbrain.framework.autoconfigure.mybatis.datasource.customizer.DataSourceCustomizer; +import com.schbrain.framework.autoconfigure.mybatis.datasource.customizer.DefaultDataSourceCustomizer; +import com.schbrain.framework.autoconfigure.mybatis.datasource.extractor.*; +import com.schbrain.framework.autoconfigure.mybatis.listener.TableConstraintCheckerBean; +import com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties; +import com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties; +import com.schbrain.framework.autoconfigure.mybatis.sql.injector.DefaultMethodSqlInjector; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.*; + +import javax.sql.DataSource; +import java.util.List; + +/** + * @author liaozan + * @since 2021/10/14 + */ +@AutoConfiguration(before = MybatisPlusAutoConfiguration.class) +@EnableConfigurationProperties({MybatisProperties.class, DataSourceConnectionProperties.class}) +@Import({HikariDataSourcePropertiesExtractor.class, DruidDataSourcePropertiesExtractor.class}) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class MybatisAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public MybatisPlusInterceptor mybatisPlusInterceptor(MybatisProperties mybatisProperties, + ObjectProvider attackInterceptor, + ObjectProvider paginationInterceptor, + ObjectProvider bizIdInjectInterceptor) { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + if (mybatisProperties.isAddBlockAttackInterceptor()) { + interceptor.addInnerInterceptor(attackInterceptor.getIfUnique(BlockAttackInnerInterceptor::new)); + } + if (mybatisProperties.isAddPageInterceptor()) { + interceptor.addInnerInterceptor(paginationInterceptor.getIfUnique(PaginationInnerInterceptor::new)); + } + bizIdInjectInterceptor.ifUnique(interceptor::addInnerInterceptor); + return interceptor; + } + + @Bean + @ConditionalOnMissingBean + public ISqlInjector defaultSqlInjector() { + return new DefaultMethodSqlInjector(); + } + + @Bean + @ConditionalOnMissingBean + public BizIdInjectInterceptor bizIdInjectInterceptor() { + return new BizIdInjectInterceptor(); + } + + @Bean + @ConditionalOnMissingBean + public DataSourceCustomizer dataSourceCustomizer(List extractors) { + return new DefaultDataSourceCustomizer(extractors); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnSingleCandidate(DataSource.class) + public TableConstraintCheckerBean constraintCheckInitializer(DataSource dataSource, MybatisProperties mybatisProperties) { + return new TableConstraintCheckerBean(dataSource, mybatisProperties); + } + + @Bean + public MybatisPlusPropertiesCustomizer globalConfigCustomizer() { + return new MybatisPlusGlobalConfigCustomizer(); + } + + @Bean + public MybatisConfigurationCustomizer configurationCustomizer(MybatisProperties mybatisProperties) { + return new MybatisConfigurationCustomizer(mybatisProperties); + } + + @Bean + public DataSourceConnectionPostProcessor dataSourceConnectionPostProcessor(ObjectProvider dataSourceCustomizers, + ObjectProvider connectionProperties) { + return new DataSourceConnectionPostProcessor(dataSourceCustomizers, connectionProperties); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/annotation/BizId.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/annotation/BizId.java new file mode 100644 index 0000000..5a509ef --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/annotation/BizId.java @@ -0,0 +1,28 @@ +package com.schbrain.framework.autoconfigure.mybatis.annotation; + +import com.schbrain.framework.autoconfigure.mybatis.biz.BizIdType; + +import java.lang.annotation.*; + +/** + * @author liaozan + * @since 2023-03-22 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface BizId { + + /** + * 逻辑主键列名,为空时取字段名 + * + * @see com.schbrain.framework.autoconfigure.mybatis.core.BizIdColumnField + */ + String value() default ""; + + /** + * 逻辑主键类型 + */ + BizIdType type() default BizIdType.ID_WORKER; + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseEntity.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseEntity.java new file mode 100644 index 0000000..345d2c8 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseEntity.java @@ -0,0 +1,34 @@ +package com.schbrain.framework.autoconfigure.mybatis.base; + +import com.baomidou.mybatisplus.annotation.*; +import com.schbrain.framework.autoconfigure.mybatis.constant.MybatisConstants; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @author liaozan + * @since 2021/10/14 + */ +@Data +public class BaseEntity { + + /** + * 主键 id + */ + @TableId(value = MybatisConstants.ID, type = IdType.AUTO) + protected Long id; + + /** + * 创建时间 + */ + @TableField(value = MybatisConstants.CREATE_TIME) + protected LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(value = MybatisConstants.MODIFY_TIME) + protected LocalDateTime modifyTime; + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseEntityWithLogicDelete.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseEntityWithLogicDelete.java new file mode 100644 index 0000000..b190d37 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseEntityWithLogicDelete.java @@ -0,0 +1,32 @@ +package com.schbrain.framework.autoconfigure.mybatis.base; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.schbrain.framework.autoconfigure.mybatis.constant.MybatisConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @author liaozan + * @since 2021/11/25 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class BaseEntityWithLogicDelete extends BaseEntity { + + /** + * 逻辑删除 + * 注意:只有写 sql 明确指定查询此字段的时候才有值, update 时,无法修改此字段 + */ + @TableLogic + @TableField(value = MybatisConstants.DELETED, select = false) + protected boolean deleted; + + /** + * 逻辑删除版本 + * 注意:只有写 sql 明确指定查询此字段的时候才有值 + */ + @TableField(value = MybatisConstants.DELETE_VERSION, select = false) + protected Long deleteVersion; + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseMapper.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseMapper.java new file mode 100644 index 0000000..4fe75d3 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseMapper.java @@ -0,0 +1,34 @@ +package com.schbrain.framework.autoconfigure.mybatis.base; + +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; +import org.apache.ibatis.annotations.Param; + +/** + * @author liaozan + * @since 2021/10/14 + */ +public interface BaseMapper extends com.baomidou.mybatisplus.core.mapper.BaseMapper { + + /** + * 根据 id 更新,null 会被更新为 null + */ + int alwaysUpdateSomeColumnById(@Param(Constants.ENTITY) T entity); + + /** + * 便捷方法查询数据 + */ + default LambdaQueryChainWrapper lambdaQuery() { + return ChainWrappers.lambdaQueryChain(this); + } + + /** + * 便捷方法更新数据 + */ + default LambdaUpdateChainWrapper lambdaUpdate() { + return ChainWrappers.lambdaUpdateChain(this); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseService.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseService.java new file mode 100644 index 0000000..b5a42d4 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseService.java @@ -0,0 +1,73 @@ +package com.schbrain.framework.autoconfigure.mybatis.base; + +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.*; +import java.util.function.Supplier; + +public interface BaseService extends IService { + + /** + * 根据 id 获取记录 + * + * @param throwIfNotFound 未获取到记录时是否抛异常 + */ + T getById(Long id, boolean throwIfNotFound); + + /** + * 根据 id 获取记录 + * + * @param notFoundSupplier 未获取到记录时的异常处理 + */ + T getById(Long id, Supplier notFoundSupplier); + + /** + * 根据 id 获取 + */ + Map getMapByIds(Collection ids); + + /** + * 根据业务主键获取记录 + */ + T getByBizId(String bizId); + + /** + * 根据业务主键获取记录 + * + * @param throwIfNotFound 未获取到记录时是否抛异常 + */ + T getByBizId(String bizId, boolean throwIfNotFound); + + /** + * 根据业务主键获取记录 + * + * @param notFoundSupplier 未获取到记录时是否抛异常 + */ + T getByBizId(String bizId, Supplier notFoundSupplier); + + /** + * 根据业务主键获取 + */ + List listByBizIds(Collection bizIds); + + /** + * 根据业务主键获取 + */ + Map getMapByBizIds(Collection bizIds); + + /** + * 根据 id 更新,null 会被更新为 null + */ + boolean updateByIdWithNull(T entity); + + /** + * 根据 id 批量更新,null 会被更新为 null, 默认批量大小 1000 + */ + boolean updateBatchByIdsWithNull(Collection entityList); + + /** + * 根据 id 批量更新,null 会被更新为 null + */ + boolean updateBatchByIdsWithNull(Collection entityList, int batchSize); + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseServiceImpl.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseServiceImpl.java new file mode 100644 index 0000000..276018a --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/base/BaseServiceImpl.java @@ -0,0 +1,147 @@ +package com.schbrain.framework.autoconfigure.mybatis.base; + +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.StreamUtils; +import com.schbrain.common.util.support.ValidateSupport; +import com.schbrain.framework.autoconfigure.mybatis.annotation.BizId; +import com.schbrain.framework.autoconfigure.mybatis.biz.BizIdHelper; +import com.schbrain.framework.autoconfigure.mybatis.core.BizIdColumnField; +import com.schbrain.framework.autoconfigure.mybatis.exception.NoSuchRecordException; +import org.apache.ibatis.binding.MapperMethod.ParamMap; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ReflectionUtils; + +import java.io.Serializable; +import java.util.*; +import java.util.function.Supplier; + +/** + * @author liaozan + * @since 2021/10/14 + */ +public class BaseServiceImpl, T extends BaseEntity> extends ServiceImpl implements BaseService, ValidateSupport, InitializingBean { + + private BizIdColumnField bizIdColumnField; + + @Override + public T getById(Serializable id) { + return getById((Long) id, false); + } + + @Override + public T getById(Long id, boolean throwIfNotFound) { + Supplier notFoundSupplier = null; + if (throwIfNotFound) { + notFoundSupplier = () -> new NoSuchRecordException(entityClass, id); + } + return getById(id, notFoundSupplier); + } + + @Override + public T getById(Long id, Supplier notFoundSupplier) { + T entity = super.getById(id); + if (entity == null && notFoundSupplier != null) { + throw notFoundSupplier.get(); + } + return entity; + } + + @Override + public Map getMapByIds(Collection ids) { + if (isEmpty(ids)) { + return Collections.emptyMap(); + } + return StreamUtils.toMap(super.listByIds(ids), T::getId); + } + + @Override + public T getByBizId(String bizId) { + return getByBizId(bizId, false); + } + + @Override + public T getByBizId(String bizId, boolean throwIfNotFound) { + Supplier notFoundSupplier = null; + if (throwIfNotFound) { + notFoundSupplier = () -> new NoSuchRecordException(entityClass, bizId); + } + return getByBizId(bizId, notFoundSupplier); + } + + @Override + public T getByBizId(String bizId, Supplier notFoundSupplier) { + assertBidColumnFieldExist(); + T entity = query().eq(bizIdColumnField.getColumnName(), bizId).one(); + if (entity == null && notFoundSupplier != null) { + throw notFoundSupplier.get(); + } + return entity; + } + + @Override + public List listByBizIds(Collection bizIds) { + assertBidColumnFieldExist(); + if (isEmpty(bizIds)) { + return Collections.emptyList(); + } + return query().in(bizIdColumnField.getColumnName(), bizIds).list(); + } + + @Override + public Map getMapByBizIds(Collection bizIds) { + assertBidColumnFieldExist(); + if (isEmpty(bizIds)) { + return Collections.emptyMap(); + } + return StreamUtils.toMap(listByBizIds(bizIds), entity -> bizIdColumnField.getValue(entity)); + } + + @Override + public boolean updateByIdWithNull(T entity) { + return SqlHelper.retBool(getBaseMapper().alwaysUpdateSomeColumnById(entity)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateBatchByIdsWithNull(Collection entityList) { + return updateBatchByIdsWithNull(entityList, DEFAULT_BATCH_SIZE); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateBatchByIdsWithNull(Collection entityList, int batchSize) { + String sqlStatement = getUpdateByIdWithNullStatementId(mapperClass); + return executeBatch(entityList, batchSize, (sqlSession, entity) -> { + ParamMap param = new ParamMap<>(); + param.put(Constants.ENTITY, entity); + sqlSession.update(sqlStatement, param); + }); + } + + @Override + public void afterPropertiesSet() { + ReflectionUtils.doWithFields(entityClass, bizId -> { + if (this.bizIdColumnField != null) { + throw new BaseException(String.format("@BizId can't more than one in Class: \"%s\"", entityClass.getName())); + } + this.bizIdColumnField = new BizIdColumnField(entityClass, bizId); + BizIdHelper.putBizColumnField(entityClass, bizIdColumnField); + }, field -> field.isAnnotationPresent(BizId.class)); + } + + private void assertBidColumnFieldExist() { + if (bizIdColumnField == null) { + throw new BaseException(String.format("@BizId not exist in Class: \"%s\"", entityClass.getName())); + } + } + + private String getUpdateByIdWithNullStatementId(Class mapperClass) { + return mapperClass.getName() + StringPool.DOT + "alwaysUpdateSomeColumnById"; + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdGenerator.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdGenerator.java new file mode 100644 index 0000000..991c5ea --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdGenerator.java @@ -0,0 +1,15 @@ +package com.schbrain.framework.autoconfigure.mybatis.biz; + +/** + * @author liaozan + * @since 2023-04-17 + */ +@FunctionalInterface +public interface BizIdGenerator { + + /** + * 生成 BizId 的值 + */ + String generate(Object entity); + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdHelper.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdHelper.java new file mode 100644 index 0000000..f3cf6ab --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdHelper.java @@ -0,0 +1,48 @@ +package com.schbrain.framework.autoconfigure.mybatis.biz; + +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.schbrain.framework.autoconfigure.mybatis.annotation.BizId; +import com.schbrain.framework.autoconfigure.mybatis.core.BizIdColumnField; +import org.apache.ibatis.session.Configuration; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author liaozan + * @since 2023-04-17 + */ +public class BizIdHelper { + + private static final Map, BizIdColumnField> BIZ_ID_COLUMN_CACHE = new ConcurrentHashMap<>(); + + public static BizIdColumnField getBizColumnField(Class entityClass) { + if (entityClass == null) { + return null; + } + return BIZ_ID_COLUMN_CACHE.get(entityClass); + } + + public static void putBizColumnField(Class entityClass, BizIdColumnField bizIdColumnField) { + if (entityClass == null) { + return; + } + BIZ_ID_COLUMN_CACHE.put(entityClass, bizIdColumnField); + } + + public static String getColumnName(Class entityClass, Field bizIdField, BizId annotation) { + if (StringUtils.isNotBlank(annotation.value())) { + return annotation.value(); + } + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + Configuration configuration = tableInfo.getConfiguration(); + if (configuration.isMapUnderscoreToCamelCase()) { + return StringUtils.camelToUnderline(bizIdField.getName()); + } + return bizIdField.getName(); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdInjectInterceptor.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdInjectInterceptor.java new file mode 100644 index 0000000..08d80b1 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdInjectInterceptor.java @@ -0,0 +1,45 @@ +package com.schbrain.framework.autoconfigure.mybatis.biz; + +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import com.schbrain.framework.autoconfigure.mybatis.core.BizIdColumnField; +import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; + +import java.sql.SQLException; + +/** + * @author liaozan + * @since 2023-04-17 + */ +public class BizIdInjectInterceptor implements InnerInterceptor { + + @Override + public void beforeUpdate(Executor executor, MappedStatement ms, Object entity) throws SQLException { + SqlCommandType sqlCommandType = ms.getSqlCommandType(); + if (sqlCommandType != SqlCommandType.INSERT) { + return; + } + Class entityClass = entity.getClass(); + BizIdColumnField bizColumnField = BizIdHelper.getBizColumnField(entityClass); + if (bizColumnField == null) { + return; + } + doBizIdFill(entity, bizColumnField); + } + + protected void doBizIdFill(Object entity, BizIdColumnField bizColumnField) { + BizIdType bizIdType = bizColumnField.getAnnotation().type(); + if (bizIdType == BizIdType.INPUT) { + return; + } + if (bizIdType == BizIdType.ID_WORKER) { + String bizIdValue = bizColumnField.getValue(entity); + if (StringUtils.isBlank(bizIdValue)) { + bizColumnField.setValue(entity, bizIdType.generateBizId(entity)); + } + } + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdType.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdType.java new file mode 100644 index 0000000..12d0990 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/biz/BizIdType.java @@ -0,0 +1,35 @@ +package com.schbrain.framework.autoconfigure.mybatis.biz; + +import com.schbrain.common.util.IdWorker; +import lombok.AllArgsConstructor; + +/** + * @author liaozan + * @since 2023-04-17 + */ +@AllArgsConstructor +public enum BizIdType { + + /** + * 用户输入 + */ + INPUT(entity -> null), + + /** + * idWorker + */ + ID_WORKER(entity -> IdWorker.getIdStr()); + + /** + * bizId 生成 + */ + private final BizIdGenerator generator; + + /** + * 生成 bizId 的值 + */ + public String generateBizId(Object entity) { + return generator.generate(entity); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/configuration/MybatisConfigurationCustomizer.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/configuration/MybatisConfigurationCustomizer.java new file mode 100644 index 0000000..c81db45 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/configuration/MybatisConfigurationCustomizer.java @@ -0,0 +1,37 @@ +package com.schbrain.framework.autoconfigure.mybatis.configuration; + +import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.schbrain.framework.autoconfigure.mybatis.core.MybatisXmlLanguageDriver; +import com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties; +import com.schbrain.framework.autoconfigure.mybatis.type.InstantToLongTypeHandler; +import org.apache.ibatis.type.EnumOrdinalTypeHandler; + +import java.time.Instant; + +/** + * @author liaozan + * @since 2021/11/8 + */ +public class MybatisConfigurationCustomizer implements ConfigurationCustomizer { + + private final MybatisProperties mybatisProperties; + + public MybatisConfigurationCustomizer(MybatisProperties mybatisProperties) { + this.mybatisProperties = mybatisProperties; + } + + @Override + public void customize(MybatisConfiguration configuration) { + configuration.setCacheEnabled(false); + configuration.setDefaultEnumTypeHandler(EnumOrdinalTypeHandler.class); + configuration.setUseActualParamName(true); + configuration.setMapUnderscoreToCamelCase(true); + // set MybatisXmlLanguageDriver default to support deleteVersion field fill + configuration.setDefaultScriptingLanguage(MybatisXmlLanguageDriver.class); + if (mybatisProperties.isConvertInstantToLong()) { + configuration.getTypeHandlerRegistry().register(Instant.class, new InstantToLongTypeHandler()); + } + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/configuration/MybatisPlusGlobalConfigCustomizer.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/configuration/MybatisPlusGlobalConfigCustomizer.java new file mode 100644 index 0000000..1a36bea --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/configuration/MybatisPlusGlobalConfigCustomizer.java @@ -0,0 +1,43 @@ +package com.schbrain.framework.autoconfigure.mybatis.configuration; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig; + +/** + * 当前类在 mybatis-plus 自动配置之前初始化,所以这里只能设置 {@link GlobalConfig} 一些基本属性,类里的其他对象还没进行初始化 + *

+ * 如果设置了其他属性,也会在 {@link com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration} 中被覆盖 + *

+ * 例如 + *

    + *
  • {@link com.baomidou.mybatisplus.core.handlers.MetaObjectHandler}
  • + *
  • {@link com.baomidou.mybatisplus.core.incrementer.IKeyGenerator}
  • + *
  • {@link com.baomidou.mybatisplus.core.injector.ISqlInjector}
  • + *
  • {@link com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator}
  • + *
+ *

+ * 如果需要设置其他属性,可以往容器里注入{@link com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer} + * + * @author liaozan + * @see MybatisConfigurationCustomizer + * @since 2021/11/3 + */ +public class MybatisPlusGlobalConfigCustomizer implements MybatisPlusPropertiesCustomizer { + + @Override + public void customize(MybatisPlusProperties properties) { + GlobalConfig globalConfig = properties.getGlobalConfig(); + globalConfig.setBanner(false); + + DbConfig dbConfig = globalConfig.getDbConfig(); + dbConfig.setIdType(IdType.AUTO); + dbConfig.setInsertStrategy(FieldStrategy.NOT_NULL); + dbConfig.setUpdateStrategy(FieldStrategy.NOT_NULL); + dbConfig.setWhereStrategy(FieldStrategy.NOT_NULL); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constant/MybatisConstants.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constant/MybatisConstants.java new file mode 100644 index 0000000..9da9a92 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constant/MybatisConstants.java @@ -0,0 +1,42 @@ +package com.schbrain.framework.autoconfigure.mybatis.constant; + +/** + * @author liaozan + * @since 2022/8/30 + */ +public class MybatisConstants { + + /** + * 主键 + */ + public static final String ID = "id"; + /** + * 创建时间 + */ + public static final String CREATE_TIME = "create_time"; + /** + * 修改时间 + */ + public static final String MODIFY_TIME = "modify_time"; + /** + * 是否删除 + */ + public static final String DELETED = "deleted"; + /** + * 删除版本 + */ + public static final String DELETE_VERSION = "delete_version"; + /** + * 当前时间戳 + */ + public static final String CURRENT_TIMESTAMP = "current_timestamp"; + /** + * 自增 + */ + public static final String AUTO_INCREMENT = "auto_increment"; + /** + * 更新为当前时间戳 + */ + public static final String UPDATE_WITH_CURRENT_TIMESTAMP = "on update current_timestamp"; + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/ColumnMeta.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/ColumnMeta.java new file mode 100644 index 0000000..469474c --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/ColumnMeta.java @@ -0,0 +1,37 @@ +package com.schbrain.framework.autoconfigure.mybatis.constraint; + +import lombok.Data; + +/** + * @author liaozan + * @since 2022/8/30 + */ +@Data +public class ColumnMeta { + + /** + * 表名 + */ + private String tableName; + /** + * 列名 + */ + private String columnName; + /** + * 数据类型 + */ + private String dataType; + /** + * 是否允许为空 + */ + private boolean nullable; + /** + * 列默认值 + */ + private String columnDefault; + /** + * 扩展信息 + */ + private String extra; + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/ColumnMetaRowMapper.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/ColumnMetaRowMapper.java new file mode 100644 index 0000000..bf1e045 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/ColumnMetaRowMapper.java @@ -0,0 +1,30 @@ +package com.schbrain.framework.autoconfigure.mybatis.constraint; + +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * @author liaozan + * @since 2022/8/30 + */ +public class ColumnMetaRowMapper implements RowMapper { + + @Override + public ColumnMeta mapRow(ResultSet resultSet, int rowNum) throws SQLException { + ColumnMeta columnMeta = new ColumnMeta(); + columnMeta.setTableName(resultSet.getString("TABLE_NAME")); + columnMeta.setColumnName(resultSet.getString("COLUMN_NAME")); + columnMeta.setDataType(resultSet.getString("DATA_TYPE")); + columnMeta.setNullable(isNullable(resultSet.getString("IS_NULLABLE"))); + columnMeta.setColumnDefault(resultSet.getString("COLUMN_DEFAULT")); + columnMeta.setExtra(resultSet.getString("EXTRA")); + return columnMeta; + } + + private boolean isNullable(String value) { + return "YES".equalsIgnoreCase(value); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/DefaultTableConstraintChecker.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/DefaultTableConstraintChecker.java new file mode 100644 index 0000000..17ba121 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/DefaultTableConstraintChecker.java @@ -0,0 +1,126 @@ +package com.schbrain.framework.autoconfigure.mybatis.constraint; + +import com.mysql.cj.MysqlType; +import com.schbrain.framework.autoconfigure.mybatis.exception.TableConstraintException; + +import static com.schbrain.framework.autoconfigure.mybatis.constant.MybatisConstants.*; + +/** + * @author liaozan + * @since 2022/8/30 + */ +@SuppressWarnings("DuplicatedCode") +public class DefaultTableConstraintChecker implements TableConstraintChecker { + + @Override + public void checkBasicField(Table table) { + checkIdField(table); + checkCreateTimeField(table); + checkModifyTimeField(table); + } + + @Override + public void checkLogicDeleteField(Table table) { + checkDeletedField(table); + checkDeleteVersionField(table); + } + + protected void checkIdField(Table table) { + ColumnMeta idColumnMeta = table.getColumnMeta(ID); + if (idColumnMeta == null) { + addMissingFieldError(table, ID); + return; + } + + checkNullable(table, idColumnMeta); + if (!MysqlType.BIGINT.getName().equalsIgnoreCase(idColumnMeta.getDataType())) { + addError(table, idColumnMeta, "should be type of 'bigint'"); + } + if (!AUTO_INCREMENT.equalsIgnoreCase(idColumnMeta.getExtra())) { + addError(table, idColumnMeta, "should be 'auto_increment'"); + } + } + + protected void checkCreateTimeField(Table table) { + ColumnMeta createTimeColumnMeta = table.getColumnMeta(CREATE_TIME); + if (createTimeColumnMeta == null) { + addMissingFieldError(table, CREATE_TIME); + return; + } + + checkNullable(table, createTimeColumnMeta); + if (!MysqlType.DATETIME.getName().equalsIgnoreCase(createTimeColumnMeta.getDataType())) { + addError(table, createTimeColumnMeta, "should be type of 'datetime'"); + } + if (!CURRENT_TIMESTAMP.equalsIgnoreCase(createTimeColumnMeta.getColumnDefault())) { + addError(table, createTimeColumnMeta, "default value should be 'current_timestamp'"); + } + } + + protected void checkModifyTimeField(Table table) { + ColumnMeta modifyTimeColumnMeta = table.getColumnMeta(MODIFY_TIME); + if (modifyTimeColumnMeta == null) { + addMissingFieldError(table, MODIFY_TIME); + return; + } + + checkNullable(table, modifyTimeColumnMeta); + if (!MysqlType.DATETIME.getName().equalsIgnoreCase(modifyTimeColumnMeta.getDataType())) { + addError(table, modifyTimeColumnMeta, "should be type of 'datetime'"); + } + if (!CURRENT_TIMESTAMP.equalsIgnoreCase(modifyTimeColumnMeta.getColumnDefault())) { + addError(table, modifyTimeColumnMeta, "default value should be 'current_timestamp'"); + } + if (!modifyTimeColumnMeta.getExtra().toLowerCase().contains(UPDATE_WITH_CURRENT_TIMESTAMP)) { + addError(table, modifyTimeColumnMeta, "need set to 'on update current_timestamp'"); + } + } + + protected void checkDeletedField(Table table) { + ColumnMeta deletedColumnMeta = table.getColumnMeta(DELETED); + if (deletedColumnMeta == null) { + addMissingFieldError(table, DELETED); + return; + } + + checkNullable(table, deletedColumnMeta); + if (!MysqlType.TINYINT.getName().equalsIgnoreCase(deletedColumnMeta.getDataType())) { + addError(table, deletedColumnMeta, "should be type of 'tinyint'"); + } + } + + protected void checkDeleteVersionField(Table table) { + ColumnMeta deleteVersionColumnMeta = table.getColumnMeta(DELETE_VERSION); + if (deleteVersionColumnMeta == null) { + addMissingFieldError(table, DELETE_VERSION); + return; + } + + checkNullable(table, deleteVersionColumnMeta); + if (!MysqlType.BIGINT.getName().equalsIgnoreCase(deleteVersionColumnMeta.getDataType())) { + addError(table, deleteVersionColumnMeta, "should be type of 'bigint'"); + } + if (!"0".equalsIgnoreCase(deleteVersionColumnMeta.getColumnDefault())) { + addError(table, deleteVersionColumnMeta, "default value should be '0'"); + } + } + + protected void checkNullable(Table table, ColumnMeta columnMeta) { + if (columnMeta.isNullable()) { + addError(table, columnMeta, "should be 'not null'"); + } + } + + private void addError(Table table, ColumnMeta columnMeta, String errorMsg) { + addError(table, columnMeta.getColumnName(), errorMsg); + } + + private void addMissingFieldError(Table table, String columnName) { + addError(table, columnName, "not exist"); + } + + private void addError(Table table, String columnName, String errorMsg) { + table.addError(new TableConstraintException(table.getTableName(), columnName, errorMsg)); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/DefaultTableMetaDataLoader.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/DefaultTableMetaDataLoader.java new file mode 100644 index 0000000..25c019b --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/DefaultTableMetaDataLoader.java @@ -0,0 +1,36 @@ +package com.schbrain.framework.autoconfigure.mybatis.constraint; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; + +import java.util.*; + +/** + * @author liaozan + * @since 2022/8/30 + */ +public class DefaultTableMetaDataLoader implements TableMetaDataLoader { + + public static final String METADATA_QUERY = "SELECT `TABLE_NAME`,`COLUMN_NAME`,`COLUMN_DEFAULT`,`DATA_TYPE`,`EXTRA`,`IS_NULLABLE` FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ?"; + + private final RowMapper rowMapper = new ColumnMetaRowMapper(); + + private final JdbcTemplate jdbcTemplate; + + public DefaultTableMetaDataLoader(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Map> loadTableMeta(String database) { + List columnMetas = jdbcTemplate.query(METADATA_QUERY, rowMapper, database); + Map> metaMap = new HashMap<>(); + for (ColumnMeta columnMeta : columnMetas) { + String tableName = columnMeta.getTableName(); + List metaList = metaMap.computeIfAbsent(tableName, name -> new ArrayList<>()); + metaList.add(columnMeta); + } + return metaMap; + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/IgnoreConstraintCheck.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/IgnoreConstraintCheck.java new file mode 100644 index 0000000..ee11635 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/IgnoreConstraintCheck.java @@ -0,0 +1,14 @@ +package com.schbrain.framework.autoconfigure.mybatis.constraint; + +import java.lang.annotation.*; + +/** + * @author liaozan + * @since 2023-03-18 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface IgnoreConstraintCheck { + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/Table.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/Table.java new file mode 100644 index 0000000..2e25cf5 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/Table.java @@ -0,0 +1,71 @@ +package com.schbrain.framework.autoconfigure.mybatis.constraint; + +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.schbrain.common.util.StreamUtils; +import com.schbrain.framework.autoconfigure.mybatis.exception.TableConstraintException; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nullable; +import java.util.*; + +/** + * @author liaozan + * @since 2022/9/2 + */ +public class Table { + + private final TableInfo tableInfo; + + private final List fieldInfoList; + + private final Map columnMetaMap; + + private final List errors = new ArrayList<>(); + + public Table(TableInfo tableInfo, Map columnMetaMap) { + this.tableInfo = tableInfo; + this.fieldInfoList = StreamUtils.toList(tableInfo.getFieldList(), FieldInfo::new); + this.columnMetaMap = columnMetaMap; + } + + public String getTableName() { + return tableInfo.getTableName(); + } + + @Nullable + public ColumnMeta getColumnMeta(String column) { + return columnMetaMap.get(column); + } + + public boolean containsColumn(String column) { + return columnMetaMap.containsKey(column); + } + + public List getFieldInfoList() { + return fieldInfoList; + } + + public void addError(TableConstraintException error) { + errors.add(error); + } + + public List getErrors() { + return errors; + } + + @Data + public static class FieldInfo { + + private String field; + private String column; + + public FieldInfo(TableFieldInfo tableFieldInfo) { + this.field = tableFieldInfo.getField().getName(); + this.column = StringUtils.unwrap(tableFieldInfo.getColumn(), "`"); + } + + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableConstraintCheckFailureAnalyzer.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableConstraintCheckFailureAnalyzer.java new file mode 100644 index 0000000..4713744 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableConstraintCheckFailureAnalyzer.java @@ -0,0 +1,20 @@ +package com.schbrain.framework.autoconfigure.mybatis.constraint; + +import com.schbrain.framework.autoconfigure.mybatis.exception.TableConstraintException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * @author liaozan + * @since 2022/8/31 + */ +@Slf4j +public class TableConstraintCheckFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, TableConstraintException cause) { + return new FailureAnalysis(cause.getMessage(), null, cause); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableConstraintChecker.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableConstraintChecker.java new file mode 100644 index 0000000..5211157 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableConstraintChecker.java @@ -0,0 +1,19 @@ +package com.schbrain.framework.autoconfigure.mybatis.constraint; + +/** + * @author liaozan + * @since 2022/8/30 + */ +public interface TableConstraintChecker { + + /** + * 检查基础字段 + */ + void checkBasicField(Table table); + + /** + * 检查逻辑删除字段 + */ + void checkLogicDeleteField(Table table); + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableMetaDataLoader.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableMetaDataLoader.java new file mode 100644 index 0000000..03b0a4f --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/constraint/TableMetaDataLoader.java @@ -0,0 +1,17 @@ +package com.schbrain.framework.autoconfigure.mybatis.constraint; + +import java.util.List; +import java.util.Map; + +/** + * @author liaozan + * @since 2022/8/30 + */ +public interface TableMetaDataLoader { + + /** + * 加载指定数据库的所有表元信息 + */ + Map> loadTableMeta(String database); + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/BizIdColumnField.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/BizIdColumnField.java new file mode 100644 index 0000000..c458fbf --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/BizIdColumnField.java @@ -0,0 +1,55 @@ +package com.schbrain.framework.autoconfigure.mybatis.core; + +import com.schbrain.common.exception.BaseException; +import com.schbrain.framework.autoconfigure.mybatis.annotation.BizId; +import com.schbrain.framework.autoconfigure.mybatis.biz.BizIdHelper; +import lombok.Data; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Field; + +import static java.lang.invoke.MethodHandles.*; + +/** + * @author liaozan + * @since 2023-03-23 + */ +@Data +public class BizIdColumnField { + + private final BizId annotation; + + private final String columnName; + + private final MethodHandle bizIdFieldGetterMethodHandle; + + private final MethodHandle bizIdFieldSetterMethodHandle; + + public BizIdColumnField(Class entityClass, Field bizIdField) { + this.annotation = bizIdField.getAnnotation(BizId.class); + this.columnName = BizIdHelper.getColumnName(entityClass, bizIdField, this.annotation); + try { + this.bizIdFieldGetterMethodHandle = privateLookupIn(entityClass, lookup()).findGetter(entityClass, bizIdField.getName(), String.class); + this.bizIdFieldSetterMethodHandle = privateLookupIn(entityClass, lookup()).findSetter(entityClass, bizIdField.getName(), String.class); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new BaseException(e.getMessage(), e); + } + } + + public String getValue(T entity) { + try { + return (String) bizIdFieldGetterMethodHandle.invoke(entity); + } catch (Throwable e) { + throw new BaseException(e.getMessage(), e); + } + } + + public void setValue(T entity, String value) { + try { + bizIdFieldSetterMethodHandle.invoke(entity, value); + } catch (Throwable e) { + throw new BaseException(e.getMessage(), e); + } + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/LogicDeleteSupportSqlSource.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/LogicDeleteSupportSqlSource.java new file mode 100644 index 0000000..3a02ea8 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/LogicDeleteSupportSqlSource.java @@ -0,0 +1,36 @@ +package com.schbrain.framework.autoconfigure.mybatis.core; + +import org.apache.ibatis.mapping.*; + +import java.util.List; + +/** + * @author liaozan + * @since 2021/11/27 + */ +public class LogicDeleteSupportSqlSource implements SqlSource { + + public static final String DELETE_VERSION = "deleteVersion"; + + private final SqlSource sqlSource; + + public LogicDeleteSupportSqlSource(SqlSource sqlSource) { + this.sqlSource = sqlSource; + } + + @Override + public BoundSql getBoundSql(Object parameterObject) { + BoundSql boundSql = sqlSource.getBoundSql(parameterObject); + if (hasDeleteVersionProperty(boundSql.getParameterMappings())) { + boundSql.setAdditionalParameter(DELETE_VERSION, System.currentTimeMillis()); + } + return boundSql; + } + + private boolean hasDeleteVersionProperty(List mappings) { + return mappings.stream() + .map(ParameterMapping::getProperty) + .anyMatch(property -> property.equals(DELETE_VERSION)); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/MybatisXmlLanguageDriver.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/MybatisXmlLanguageDriver.java new file mode 100644 index 0000000..910abd2 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/core/MybatisXmlLanguageDriver.java @@ -0,0 +1,19 @@ +package com.schbrain.framework.autoconfigure.mybatis.core; + +import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver; +import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.session.Configuration; + +/** + * @author liaozan + * @since 2021/11/27 + */ +public class MybatisXmlLanguageDriver extends MybatisXMLLanguageDriver { + + @Override + public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType) { + SqlSource sqlSource = super.createSqlSource(configuration, script, parameterType); + return new LogicDeleteSupportSqlSource(sqlSource); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/DataSourceConnectionPostProcessor.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/DataSourceConnectionPostProcessor.java new file mode 100644 index 0000000..a18a2f9 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/DataSourceConnectionPostProcessor.java @@ -0,0 +1,42 @@ +package com.schbrain.framework.autoconfigure.mybatis.datasource; + +import com.schbrain.framework.autoconfigure.mybatis.datasource.customizer.DataSourceCustomizer; +import com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties; +import com.schbrain.framework.support.spring.BeanPostProcessorAdapter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; + +import javax.sql.DataSource; +import java.sql.SQLException; + +/** + * @author liaozan + * @since 2021/11/23 + */ +@Slf4j +public class DataSourceConnectionPostProcessor extends BeanPostProcessorAdapter { + + // use ObjectProvider to avoid early initialization beans + private final ObjectProvider customizers; + private final ObjectProvider connectionProperties; + + public DataSourceConnectionPostProcessor(ObjectProvider customizers, + ObjectProvider connectionProperties) { + this.customizers = customizers; + this.connectionProperties = connectionProperties; + } + + @Override + protected void processBeforeInitialization(DataSource dataSource, String beanName) throws BeansException { + DataSourceConnectionProperties connectionProperties = this.connectionProperties.getObject(); + customizers.orderedStream().forEach(customizer -> { + try { + customizer.customize(dataSource, connectionProperties); + } catch (SQLException e) { + log.warn("failed to customize dataSource connectionProperties", e); + } + }); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/customizer/DataSourceCustomizer.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/customizer/DataSourceCustomizer.java new file mode 100644 index 0000000..f303649 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/customizer/DataSourceCustomizer.java @@ -0,0 +1,16 @@ +package com.schbrain.framework.autoconfigure.mybatis.datasource.customizer; + +import com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties; + +import javax.sql.DataSource; +import java.sql.SQLException; + +/** + * @author liaozan + * @since 2021/11/28 + */ +public interface DataSourceCustomizer { + + void customize(DataSource dataSource, DataSourceConnectionProperties properties) throws SQLException; + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/customizer/DefaultDataSourceCustomizer.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/customizer/DefaultDataSourceCustomizer.java new file mode 100644 index 0000000..b08e634 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/customizer/DefaultDataSourceCustomizer.java @@ -0,0 +1,37 @@ +package com.schbrain.framework.autoconfigure.mybatis.datasource.customizer; + +import com.schbrain.framework.autoconfigure.mybatis.datasource.extractor.DataSourcePropertiesExtractor; +import com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.*; + +/** + * @author liaozan + * @since 2021/11/28 + */ +public class DefaultDataSourceCustomizer implements DataSourceCustomizer { + + private final List extractors; + + public DefaultDataSourceCustomizer(List extractors) { + AnnotationAwareOrderComparator.sort(extractors); + this.extractors = extractors; + } + + @Override + public void customize(DataSource dataSource, DataSourceConnectionProperties properties) throws SQLException { + Map connectionProps = properties.toConfigurationMap(); + for (DataSourcePropertiesExtractor extractor : extractors) { + if (extractor.support(dataSource)) { + Properties originProps = extractor.extract(dataSource, connectionProps); + if (originProps != null) { + originProps.putAll(connectionProps); + } + } + } + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DataSourcePropertiesExtractor.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DataSourcePropertiesExtractor.java new file mode 100644 index 0000000..c01a3b9 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DataSourcePropertiesExtractor.java @@ -0,0 +1,18 @@ +package com.schbrain.framework.autoconfigure.mybatis.datasource.extractor; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.Map; +import java.util.Properties; + +/** + * @author liaozan + * @since 2021/11/23 + */ +public interface DataSourcePropertiesExtractor { + + boolean support(DataSource dataSource) throws SQLException; + + Properties extract(DataSource dataSource, Map properties) throws SQLException; + +} diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DataSourcePropertiesExtractorSupport.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DataSourcePropertiesExtractorSupport.java new file mode 100644 index 0000000..dc8c1a3 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DataSourcePropertiesExtractorSupport.java @@ -0,0 +1,31 @@ +package com.schbrain.framework.autoconfigure.mybatis.datasource.extractor; + +import org.springframework.util.TypeUtils; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.Map; +import java.util.Properties; + +/** + * @author liaozan + * @since 2021/11/23 + */ +public abstract class DataSourcePropertiesExtractorSupport implements DataSourcePropertiesExtractor { + + @Override + public boolean support(DataSource dataSource) { + Class supportedType = getSupportedType(); + return TypeUtils.isAssignable(supportedType, dataSource.getClass()); + } + + public abstract Class getSupportedType(); + + @Override + public Properties extract(DataSource dataSource, Map properties) throws SQLException { + return extract(dataSource); + } + + protected abstract Properties extract(DataSource dataSource) throws SQLException; + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DruidDataSourcePropertiesExtractor.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DruidDataSourcePropertiesExtractor.java new file mode 100644 index 0000000..783dd2d --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/DruidDataSourcePropertiesExtractor.java @@ -0,0 +1,29 @@ +package com.schbrain.framework.autoconfigure.mybatis.datasource.extractor; + +import com.alibaba.druid.pool.DruidDataSource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.Properties; + +/** + * @author liaozan + * @since 2021/11/23 + */ +@Slf4j +@ConditionalOnClass(DruidDataSource.class) +public class DruidDataSourcePropertiesExtractor extends DataSourcePropertiesExtractorSupport { + + @Override + public Class getSupportedType() { + return DruidDataSource.class; + } + + @Override + protected Properties extract(DataSource dataSource) throws SQLException { + return dataSource.unwrap(DruidDataSource.class).getConnectProperties(); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/HikariDataSourcePropertiesExtractor.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/HikariDataSourcePropertiesExtractor.java new file mode 100644 index 0000000..1f6fc08 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/datasource/extractor/HikariDataSourcePropertiesExtractor.java @@ -0,0 +1,27 @@ +package com.schbrain.framework.autoconfigure.mybatis.datasource.extractor; + +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.Properties; + +/** + * @author liaozan + * @since 2021/11/23 + */ +@ConditionalOnClass(HikariDataSource.class) +public class HikariDataSourcePropertiesExtractor extends DataSourcePropertiesExtractorSupport { + + @Override + public Class getSupportedType() { + return HikariDataSource.class; + } + + @Override + protected Properties extract(DataSource dataSource) throws SQLException { + return dataSource.unwrap(HikariDataSource.class).getDataSourceProperties(); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/exception/NoSuchRecordException.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/exception/NoSuchRecordException.java new file mode 100644 index 0000000..2831b3a --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/exception/NoSuchRecordException.java @@ -0,0 +1,19 @@ +package com.schbrain.framework.autoconfigure.mybatis.exception; + +import com.schbrain.common.exception.BaseException; + +import java.io.Serializable; + +/** + * @author liaozan + * @since 2022/04/15 + */ +public class NoSuchRecordException extends BaseException { + + private static final long serialVersionUID = -2197824144318250175L; + + public NoSuchRecordException(Class entityClass, Serializable id) { + super(String.format("No %s entity with id %s exists!", entityClass.getSimpleName(), id)); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/exception/TableConstraintException.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/exception/TableConstraintException.java new file mode 100644 index 0000000..7f0fdb0 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/exception/TableConstraintException.java @@ -0,0 +1,31 @@ +package com.schbrain.framework.autoconfigure.mybatis.exception; + +import cn.hutool.core.text.StrFormatter; +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.StreamUtils; +import lombok.Getter; + +import java.util.List; + +/** + * @author liaozan + * @since 2022/8/30 + */ +@Getter +public class TableConstraintException extends BaseException { + + private static final long serialVersionUID = -3139175416089223586L; + + public TableConstraintException(String message, Object... args) { + super(StrFormatter.format(message, args)); + } + + public TableConstraintException(String tableName, String column, String message) { + super("Table: '" + tableName + "' , Column: '" + column + "' " + message); + } + + public TableConstraintException(List errors) { + super(StreamUtils.join(StreamUtils.toList(errors, Throwable::getMessage), System.lineSeparator())); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/listener/TableConstraintCheckerBean.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/listener/TableConstraintCheckerBean.java new file mode 100644 index 0000000..78ce54b --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/listener/TableConstraintCheckerBean.java @@ -0,0 +1,173 @@ +package com.schbrain.framework.autoconfigure.mybatis.listener; + +import com.baomidou.mybatisplus.core.mapper.Mapper; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.ReflectionKit; +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.StreamUtils; +import com.schbrain.framework.autoconfigure.mybatis.base.*; +import com.schbrain.framework.autoconfigure.mybatis.constraint.*; +import com.schbrain.framework.autoconfigure.mybatis.constraint.Table.FieldInfo; +import com.schbrain.framework.autoconfigure.mybatis.exception.TableConstraintException; +import com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.*; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.*; + +import static java.util.stream.Collectors.toList; + +/** + * @author liaozan + * @since 2022/8/28 + */ +@Slf4j +public class TableConstraintCheckerBean implements SmartInitializingSingleton, BeanFactoryAware { + + private final DataSource dataSource; + + private final MybatisProperties mybatisProperties; + + private ConfigurableListableBeanFactory beanFactory; + + public TableConstraintCheckerBean(DataSource dataSource, MybatisProperties mybatisProperties) { + this.dataSource = dataSource; + this.mybatisProperties = mybatisProperties; + } + + @Override + public void afterSingletonsInstantiated() { + if (!mybatisProperties.isEnableTableConstraintCheck()) { + log.warn("Table constraint check is disabled"); + return; + } + + log.info("Table constraint check started"); + + List metaDataLoaders = beanFactory.getBeanProvider(TableMetaDataLoader.class).orderedStream().collect(toList()); + if (CollectionUtils.isEmpty(metaDataLoaders)) { + JdbcTemplate jdbcTemplate = beanFactory.getBean(JdbcTemplate.class); + // Avoid add to a immutable collection + metaDataLoaders = List.of(new DefaultTableMetaDataLoader(jdbcTemplate)); + } + + Map> tableMetadata = loadTableMetadata(metaDataLoaders); + if (MapUtils.isEmpty(tableMetadata)) { + log.warn("Table metadata is empty, ignore table constraint check"); + return; + } + + List checkers = beanFactory.getBeanProvider(TableConstraintChecker.class).orderedStream().collect(toList()); + if (CollectionUtils.isEmpty(checkers)) { + // Avoid add to a immutable collection + checkers = List.of(new DefaultTableConstraintChecker()); + } + + List errors = new ArrayList<>(); + for (Class mapper : getAllMappers()) { + doConstraintCheck(mapper, tableMetadata, checkers, errors); + } + + if (CollectionUtils.isNotEmpty(errors)) { + throw new TableConstraintException(errors); + } + + log.info("Table constraint check completed"); + beanFactory.destroyBean(this); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory); + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + private void doConstraintCheck(Class mapperClass, Map> tableMetadata, + List checkers, List errors) { + if (!ClassUtils.isAssignable(BaseMapper.class, mapperClass)) { + return; + } + + Class entityClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0); + if (!ClassUtils.isAssignable(BaseEntity.class, entityClass)) { + return; + } + + if (entityClass.isAssignableFrom(IgnoreConstraintCheck.class)) { + return; + } + + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + if (tableInfo == null) { + errors.add(new TableConstraintException("Could not get tableInfo for {}", entityClass.getName())); + return; + } + + Map columnMetaMap = StreamUtils.toMap(tableMetadata.get(tableInfo.getTableName()), ColumnMeta::getColumnName); + if (MapUtils.isEmpty(columnMetaMap)) { + errors.add(new TableConstraintException("Table: '{}' not exist ", tableInfo.getTableName())); + return; + } + + Table table = new Table(tableInfo, columnMetaMap); + for (TableConstraintChecker checker : checkers) { + checkAllFieldExist(table); + checker.checkBasicField(table); + if (ClassUtils.isAssignable(BaseEntityWithLogicDelete.class, entityClass)) { + checker.checkLogicDeleteField(table); + } + } + errors.addAll(table.getErrors()); + } + + private String getDatabaseName() { + try { + return dataSource.getConnection().getCatalog(); + } catch (SQLException e) { + throw new BaseException("Can not get connection from DataSource", e); + } + } + + private Map> loadTableMetadata(List loaders) { + String database = getDatabaseName(); + + Map> tableMetadata = null; + for (TableMetaDataLoader metaDataLoader : loaders) { + tableMetadata = metaDataLoader.loadTableMeta(database); + if (tableMetadata != null) { + break; + } + } + return tableMetadata; + } + + private List> getAllMappers() { + Map sqlSessionFactoryMap = beanFactory.getBeansOfType(SqlSessionFactory.class); + return sqlSessionFactoryMap.values().stream() + .flatMap(sqlSessionFactory -> sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers().stream()) + .collect(toList()); + } + + /** + * @see TableInfoHelper#getAllFields(Class) + */ + private void checkAllFieldExist(Table table) { + for (FieldInfo fieldInfo : table.getFieldInfoList()) { + if (!table.containsColumn(fieldInfo.getColumn())) { + table.addError(new TableConstraintException(table.getTableName(), fieldInfo.getColumn(), "not exist")); + } + } + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/properties/DataSourceConnectionProperties.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/properties/DataSourceConnectionProperties.java new file mode 100644 index 0000000..b654bc8 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/properties/DataSourceConnectionProperties.java @@ -0,0 +1,67 @@ +package com.schbrain.framework.autoconfigure.mybatis.properties; + +import com.mysql.cj.conf.PropertyDefinitions.ZeroDatetimeBehavior; +import com.mysql.cj.conf.PropertyKey; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.nio.charset.StandardCharsets; +import java.time.ZoneId; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @author liaozan + * @since 2021/11/23 + */ +@Data +@ConfigurationProperties(prefix = "schbrain.datasource.connection") +public class DataSourceConnectionProperties { + + /** + * 使用 ssl 连接 + */ + private Boolean useSsl = false; + /** + * tinyint(1) 视为 boolean + */ + private Boolean tinyInt1isBit = true; + /** + * 重写批处理sql + */ + private Boolean rewriteBatchedStatements = true; + /** + * 是否允许一个 statement 用分号分割执行多个查询语句 + */ + private Boolean allowMultiQueries = true; + /** + * 允许从服务端获取公钥进行连接 + */ + private Boolean allowPublicKeyRetrieval = true; + /** + * 连接数据库使用的时区 + */ + private ZoneId serverTimeZone = ZoneId.systemDefault(); + /** + * 时间格式字段值为 0 的时候的处理方式 + */ + private ZeroDatetimeBehavior zeroDatetimeBehavior = ZeroDatetimeBehavior.CONVERT_TO_NULL; + /** + * 数据库连接字符编码 + */ + private String characterEncoding = StandardCharsets.UTF_8.name(); + + public Map toConfigurationMap() { + Map properties = new LinkedHashMap<>(); + properties.put(PropertyKey.useSSL.getKeyName(), this.useSsl.toString()); + properties.put(PropertyKey.tinyInt1isBit.getKeyName(), this.tinyInt1isBit.toString()); + properties.put(PropertyKey.rewriteBatchedStatements.getKeyName(), this.rewriteBatchedStatements.toString()); + properties.put(PropertyKey.allowMultiQueries.getKeyName(), this.allowMultiQueries.toString()); + properties.put(PropertyKey.connectionTimeZone.getKeyName(), this.serverTimeZone.getId()); + properties.put(PropertyKey.allowPublicKeyRetrieval.getKeyName(), this.allowPublicKeyRetrieval.toString()); + properties.put(PropertyKey.zeroDateTimeBehavior.getKeyName(), this.zeroDatetimeBehavior.name()); + properties.put(PropertyKey.characterEncoding.getKeyName(), this.characterEncoding); + return properties; + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/properties/MybatisProperties.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/properties/MybatisProperties.java new file mode 100644 index 0000000..1a5aa58 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/properties/MybatisProperties.java @@ -0,0 +1,39 @@ +package com.schbrain.framework.autoconfigure.mybatis.properties; + +import com.schbrain.common.util.support.ConfigurableProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author liaozan + * @since 2021/11/23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ConfigurationProperties(prefix = "schbrain.mybatis") +public class MybatisProperties extends ConfigurableProperties { + + /** + * 分页拦截器 + */ + private boolean addPageInterceptor = true; + /** + * 阻断全表更新操作,禁止不带 where 更新,删除 + */ + private boolean addBlockAttackInterceptor = true; + /** + * 是否开启表约束检查 + */ + private boolean enableTableConstraintCheck = true; + /** + * Instant 转为 long + */ + private boolean convertInstantToLong = true; + + @Override + public String getDefaultNamespace() { + return "mybatis-common"; + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/injector/DefaultMethodSqlInjector.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/injector/DefaultMethodSqlInjector.java new file mode 100644 index 0000000..76cc603 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/injector/DefaultMethodSqlInjector.java @@ -0,0 +1,59 @@ +package com.schbrain.framework.autoconfigure.mybatis.sql.injector; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.AbstractSqlInjector; +import com.baomidou.mybatisplus.core.injector.methods.*; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.extension.injector.methods.AlwaysUpdateSomeColumnById; +import com.google.common.collect.Lists; +import com.schbrain.framework.autoconfigure.mybatis.constant.MybatisConstants; +import com.schbrain.framework.autoconfigure.mybatis.sql.method.Delete; +import com.schbrain.framework.autoconfigure.mybatis.sql.method.DeleteBatchByIds; +import com.schbrain.framework.autoconfigure.mybatis.sql.method.DeleteByMap; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * @author liaozan + * @since 2021/11/26 + */ +@Slf4j +public class DefaultMethodSqlInjector extends AbstractSqlInjector { + + /** + *

    + *
  • replace {@link com.baomidou.mybatisplus.core.injector.methods.Delete} to {@link Delete}
  • + *
  • replace {@link com.baomidou.mybatisplus.core.injector.methods.DeleteById} to {@link com.schbrain.framework.autoconfigure.mybatis.sql.method.DeleteById}
  • + *
  • replace {@link com.baomidou.mybatisplus.core.injector.methods.DeleteByMap} to {@link DeleteByMap}
  • + *
  • replace {@link com.baomidou.mybatisplus.core.injector.methods.DeleteBatchByIds} to {@link DeleteBatchByIds}
  • + *
+ */ + @Override + public List getMethodList(Class mapperClass, TableInfo tableInfo) { + List methodList = Lists.newArrayListWithExpectedSize(20); + methodList.add(new Insert()); + methodList.add(new Delete()); + methodList.add(new DeleteByMap()); + methodList.add(new Update()); + methodList.add(new SelectByMap()); + methodList.add(new SelectCount()); + methodList.add(new SelectMaps()); + methodList.add(new SelectMapsPage()); + methodList.add(new SelectObjs()); + methodList.add(new SelectList()); + methodList.add(new SelectPage()); + if (tableInfo.havePK()) { + methodList.add(new com.schbrain.framework.autoconfigure.mybatis.sql.method.DeleteById()); + methodList.add(new DeleteBatchByIds()); + methodList.add(new UpdateById()); + methodList.add(new AlwaysUpdateSomeColumnById(field -> !field.getColumn().equals(MybatisConstants.DELETE_VERSION))); + methodList.add(new SelectById()); + methodList.add(new SelectBatchByIds()); + } else { + log.warn("{} ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.", tableInfo.getEntityType()); + } + return methodList; + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/Delete.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/Delete.java new file mode 100644 index 0000000..3baa19d --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/Delete.java @@ -0,0 +1,20 @@ +package com.schbrain.framework.autoconfigure.mybatis.sql.method; + +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.schbrain.framework.autoconfigure.mybatis.util.SqlUtils; + +/** + * @author liaozan + * @since 2021/11/26 + */ +public class Delete extends com.baomidou.mybatisplus.core.injector.methods.Delete { + + private static final long serialVersionUID = -4047186946220703735L; + + @Override + protected String sqlLogicSet(TableInfo table) { + String logicSet = super.sqlLogicSet(table); + return SqlUtils.withLogicDeleteVersionIfNecessary(table, logicSet); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteBatchByIds.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteBatchByIds.java new file mode 100644 index 0000000..f043788 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteBatchByIds.java @@ -0,0 +1,20 @@ +package com.schbrain.framework.autoconfigure.mybatis.sql.method; + +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.schbrain.framework.autoconfigure.mybatis.util.SqlUtils; + +/** + * @author liaozan + * @since 2021/11/26 + */ +public class DeleteBatchByIds extends com.baomidou.mybatisplus.core.injector.methods.DeleteBatchByIds { + + private static final long serialVersionUID = -6821464569587694540L; + + @Override + protected String sqlLogicSet(TableInfo table) { + String logicSet = super.sqlLogicSet(table); + return SqlUtils.withLogicDeleteVersionIfNecessary(table, logicSet); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteById.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteById.java new file mode 100644 index 0000000..b469762 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteById.java @@ -0,0 +1,57 @@ +package com.schbrain.framework.autoconfigure.mybatis.sql.method; + +import com.baomidou.mybatisplus.core.enums.SqlMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.schbrain.framework.autoconfigure.mybatis.util.SqlUtils; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; + +/** + * Mybatis-Plus 对于 DeleteById 的逻辑删除和其他 Delete 行为不一致, 所以这里去掉了不一致的行为 + * 详情见 Github Issue + * + * @author liaozan + * @since 2021/11/26 + */ +public class DeleteById extends com.baomidou.mybatisplus.core.injector.methods.DeleteById { + + private static final long serialVersionUID = 998500455669716402L; + + @Override + public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + if (tableInfo.isWithLogicDelete()) { + return addLogicDeleteMappedStatement(mapperClass, modelClass, tableInfo); + } else { + return addDeleteMappedStatement(mapperClass, tableInfo); + } + } + + @Override + protected String sqlLogicSet(TableInfo table) { + String logicSet = super.sqlLogicSet(table); + return SqlUtils.withLogicDeleteVersionIfNecessary(table, logicSet); + } + + private MappedStatement addDeleteMappedStatement(Class mapperClass, TableInfo tableInfo) { + SqlMethod sqlMethod = SqlMethod.DELETE_BY_ID; + String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty()); + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class); + return this.addDeleteMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource); + } + + private MappedStatement addLogicDeleteMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BY_ID; + String sql = getLogicDeleteSql(tableInfo, sqlMethod); + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class); + return addUpdateMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource); + } + + private String getLogicDeleteSql(TableInfo tableInfo, SqlMethod sqlMethod) { + String tableName = tableInfo.getTableName(); + String keyColumn = tableInfo.getKeyColumn(); + String keyProperty = tableInfo.getKeyProperty(); + String logicDeleteSql = tableInfo.getLogicDeleteSql(true, true); + return String.format(sqlMethod.getSql(), tableName, sqlLogicSet(tableInfo), keyColumn, keyProperty, logicDeleteSql); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteByMap.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteByMap.java new file mode 100644 index 0000000..0f859c0 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/sql/method/DeleteByMap.java @@ -0,0 +1,20 @@ +package com.schbrain.framework.autoconfigure.mybatis.sql.method; + +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.schbrain.framework.autoconfigure.mybatis.util.SqlUtils; + +/** + * @author liaozan + * @since 2021/11/26 + */ +public class DeleteByMap extends com.baomidou.mybatisplus.core.injector.methods.DeleteByMap { + + private static final long serialVersionUID = 1937536236892111475L; + + @Override + protected String sqlLogicSet(TableInfo table) { + String logicSet = super.sqlLogicSet(table); + return SqlUtils.withLogicDeleteVersionIfNecessary(table, logicSet); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/type/InstantToLongTypeHandler.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/type/InstantToLongTypeHandler.java new file mode 100644 index 0000000..1c2fbf8 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/type/InstantToLongTypeHandler.java @@ -0,0 +1,38 @@ +package com.schbrain.framework.autoconfigure.mybatis.type; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.*; +import java.time.Instant; + +/** + * @author liaozan + * @since 2022/8/31 + */ +public class InstantToLongTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int index, Instant parameter, JdbcType jdbcType) throws SQLException { + ps.setLong(index, parameter.toEpochMilli()); + } + + @Override + public Instant getNullableResult(ResultSet rs, String columnName) throws SQLException { + long result = rs.getLong(columnName); + return result == 0 && rs.wasNull() ? null : Instant.ofEpochMilli(result); + } + + @Override + public Instant getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + long result = rs.getLong(columnIndex); + return result == 0 && rs.wasNull() ? null : Instant.ofEpochMilli(result); + } + + @Override + public Instant getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + long result = cs.getLong(columnIndex); + return result == 0 && cs.wasNull() ? null : Instant.ofEpochMilli(result); + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/util/SqlUtils.java b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/util/SqlUtils.java new file mode 100644 index 0000000..b8e9501 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/mybatis/util/SqlUtils.java @@ -0,0 +1,21 @@ +package com.schbrain.framework.autoconfigure.mybatis.util; + +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.schbrain.framework.autoconfigure.mybatis.base.BaseEntityWithLogicDelete; +import org.springframework.util.ClassUtils; + +/** + * @author liaozan + * @since 2022/8/27 + */ +public class SqlUtils { + + public static String withLogicDeleteVersionIfNecessary(TableInfo tableInfo, String logicDeleteSql) { + Class entityType = tableInfo.getEntityType(); + if (ClassUtils.isAssignable(BaseEntityWithLogicDelete.class, entityType)) { + logicDeleteSql = logicDeleteSql + ", delete_version=#{deleteVersion}"; + } + return logicDeleteSql; + } + +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json b/starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..c21b8e9 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,113 @@ +{ + "groups": [ + { + "name": "schbrain.datasource.connection", + "type": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties" + }, + { + "name": "schbrain.mybatis", + "type": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties" + } + ], + "properties": [ + { + "name": "schbrain.datasource.connection.allow-multi-queries", + "type": "java.lang.Boolean", + "description": "是否允许一个 statement 用分号分割执行多个查询语句", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties", + "defaultValue": true + }, + { + "name": "schbrain.datasource.connection.allow-public-key-retrieval", + "type": "java.lang.Boolean", + "description": "允许从服务端获取公钥进行连接", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties", + "defaultValue": true + }, + { + "name": "schbrain.datasource.connection.character-encoding", + "type": "java.lang.String", + "description": "数据库连接字符编码", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties" + }, + { + "name": "schbrain.datasource.connection.rewrite-batched-statements", + "type": "java.lang.Boolean", + "description": "重写批处理sql", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties", + "defaultValue": true + }, + { + "name": "schbrain.datasource.connection.server-time-zone", + "type": "java.time.ZoneId", + "description": "连接数据库使用的时区", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties" + }, + { + "name": "schbrain.datasource.connection.tiny-int1is-bit", + "type": "java.lang.Boolean", + "description": "tinyint(1) 视为 boolean", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties", + "defaultValue": true + }, + { + "name": "schbrain.datasource.connection.use-ssl", + "type": "java.lang.Boolean", + "description": "使用 ssl 连接", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties", + "defaultValue": false + }, + { + "name": "schbrain.datasource.connection.zero-datetime-behavior", + "type": "com.mysql.cj.conf.PropertyDefinitions$ZeroDatetimeBehavior", + "description": "时间格式字段值为 0 的时候的处理方式", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.DataSourceConnectionProperties" + }, + { + "name": "schbrain.mybatis.add-block-attack-interceptor", + "type": "java.lang.Boolean", + "description": "阻断全表更新操作,禁止不带 where 更新,删除", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties", + "defaultValue": true + }, + { + "name": "schbrain.mybatis.add-page-interceptor", + "type": "java.lang.Boolean", + "description": "分页拦截器", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties", + "defaultValue": true + }, + { + "name": "schbrain.mybatis.convert-instant-to-long", + "type": "java.lang.Boolean", + "description": "Instant 转为 long", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties", + "defaultValue": true + }, + { + "name": "schbrain.mybatis.enable-table-constraint-check", + "type": "java.lang.Boolean", + "description": "是否开启表约束检查", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties", + "defaultValue": true + }, + { + "name": "schbrain.mybatis.name", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties" + }, + { + "name": "schbrain.mybatis.namespace", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties" + }, + { + "name": "schbrain.mybatis.prefix", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.mybatis.properties.MybatisProperties" + } + ], + "hints": [] +} \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring.factories b/starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..6911308 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.diagnostics.FailureAnalyzer=com.schbrain.framework.autoconfigure.mybatis.constraint.TableConstraintCheckFailureAnalyzer \ No newline at end of file diff --git a/starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..c1850e9 --- /dev/null +++ b/starters/mybatis-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.framework.autoconfigure.mybatis.MybatisAutoConfiguration \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/pom.xml b/starters/oss-spring-boot-starter/pom.xml new file mode 100644 index 0000000..e4c38dd --- /dev/null +++ b/starters/oss-spring-boot-starter/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + + com.schbrain.framework + starters + ${revision} + + + oss-spring-boot-starter + + + + com.aliyun.oss + aliyun-sdk-oss + + + com.schbrain.framework + apollo-spring-boot-starter + + + + \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/OssAutoConfiguration.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/OssAutoConfiguration.java new file mode 100644 index 0000000..4fc070e --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/OssAutoConfiguration.java @@ -0,0 +1,29 @@ +package com.schbrain.framework.autoconfigure.oss; + +import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils; +import com.schbrain.framework.autoconfigure.oss.properties.OssProperties; +import com.schbrain.framework.autoconfigure.oss.util.OssUtils; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * @author liaozan + * @since 2021/12/3 + */ +@AutoConfiguration +@EnableConfigurationProperties(OssProperties.class) +public class OssAutoConfiguration { + + public OssAutoConfiguration(ConfigurableApplicationContext applicationContext) { + initialize(applicationContext); + } + + private void initialize(ConfigurableApplicationContext applicationContext) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + OssProperties ossProperties = ConfigUtils.loadConfig(environment, OssProperties.class); + OssUtils.initialize(environment, ossProperties); + } + +} \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/CopyResult.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/CopyResult.java new file mode 100644 index 0000000..70649dd --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/CopyResult.java @@ -0,0 +1,41 @@ +package com.schbrain.framework.autoconfigure.oss.bean; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @author lik + * @since 2022/9/6 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CopyResult extends OssOperationResult { + + private static final long serialVersionUID = 7322484488471640978L; + + private String destinationKey; + + private String destinationBucket; + + public static CopyResult success(String sourceBucket, String sourceKey, String destinationBucket, String destinationKey) { + CopyResult result = new CopyResult(); + result.setSuccess(true); + result.setBucket(sourceBucket); + result.setObjectKey(sourceKey); + result.setDestinationBucket(destinationBucket); + result.setDestinationKey(destinationKey); + return result; + } + + public static CopyResult fail(String sourceBucket, String sourceKey, String destinationBucket, String destinationKey, String errorMsg) { + CopyResult result = new CopyResult(); + result.setSuccess(false); + result.setBucket(sourceBucket); + result.setObjectKey(sourceKey); + result.setDestinationBucket(destinationBucket); + result.setDestinationKey(destinationKey); + result.setErrorMsg(errorMsg); + return result; + } + +} diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/DeleteResult.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/DeleteResult.java new file mode 100644 index 0000000..1c0cf60 --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/DeleteResult.java @@ -0,0 +1,37 @@ +package com.schbrain.framework.autoconfigure.oss.bean; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * @author lik + * @since 2022/9/6 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DeleteResult extends OssOperationResult { + + private static final long serialVersionUID = -1964404722922995469L; + + private List deleteObjectKeys; + + public static DeleteResult success(String bucket, List objectKeys) { + DeleteResult result = new DeleteResult(); + result.setSuccess(true); + result.setBucket(bucket); + result.setDeleteObjectKeys(objectKeys); + return result; + } + + public static DeleteResult fail(String bucket, List objectKeys, String errorMsg) { + DeleteResult result = new DeleteResult(); + result.setSuccess(false); + result.setBucket(bucket); + result.setErrorMsg(errorMsg); + result.setDeleteObjectKeys(objectKeys); + return result; + } + +} diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/DownloadResult.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/DownloadResult.java new file mode 100644 index 0000000..7404574 --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/DownloadResult.java @@ -0,0 +1,37 @@ +package com.schbrain.framework.autoconfigure.oss.bean; + +import com.aliyun.oss.model.OSSObject; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @author liaozan + * @since 2021/12/3 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DownloadResult extends OssOperationResult { + + private static final long serialVersionUID = -9010079317621184645L; + + private OSSObject ossObject; + + public static DownloadResult success(String bucket, String objectKey, OSSObject ossObject) { + DownloadResult result = new DownloadResult(); + result.setSuccess(true); + result.setBucket(bucket); + result.setObjectKey(objectKey); + result.setOssObject(ossObject); + return result; + } + + public static DownloadResult fail(String bucket, String objectKey, String errorMsg) { + DownloadResult result = new DownloadResult(); + result.setSuccess(false); + result.setBucket(bucket); + result.setObjectKey(objectKey); + result.setErrorMsg(errorMsg); + return result; + } + +} \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/OssOperationResult.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/OssOperationResult.java new file mode 100644 index 0000000..9de1ce1 --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/OssOperationResult.java @@ -0,0 +1,25 @@ +package com.schbrain.framework.autoconfigure.oss.bean; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author liaozan + * @since 2021/12/19 + */ +@Data +public class OssOperationResult implements Serializable { + + private static final long serialVersionUID = 3651584115463313214L; + + protected boolean success; + protected String bucket; + protected String objectKey; + protected String errorMsg; + + public boolean isFailed() { + return !success; + } + +} \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/UploadCredentials.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/UploadCredentials.java new file mode 100644 index 0000000..34de5fe --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/UploadCredentials.java @@ -0,0 +1,47 @@ +package com.schbrain.framework.autoconfigure.oss.bean; + +import com.aliyuncs.auth.sts.AssumeRoleResponse.Credentials; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.*; +import java.time.format.DateTimeFormatter; + +/** + * @author liaozan + * @since 2021/12/4 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UploadCredentials extends OssOperationResult { + + private static final long serialVersionUID = 5546792221041679671L; + + private String accessKeyId; + private String accessKeySecret; + private String securityToken; + private LocalDateTime expiration; + + // for json deserialize + public UploadCredentials() { + + } + + public UploadCredentials(Credentials credentials) { + this.success = true; + this.accessKeyId = credentials.getAccessKeyId(); + this.accessKeySecret = credentials.getAccessKeySecret(); + this.securityToken = credentials.getSecurityToken(); + // example: 2021-12-04T11:03:37Z + this.expiration = LocalDateTime.parse(credentials.getExpiration(), DateTimeFormatter.ISO_DATE_TIME) + .toInstant(ZoneOffset.UTC) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + } + + public UploadCredentials(String errorMsg) { + this.success = false; + this.errorMsg = errorMsg; + } + +} \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/UploadResult.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/UploadResult.java new file mode 100644 index 0000000..75c5e52 --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/bean/UploadResult.java @@ -0,0 +1,36 @@ +package com.schbrain.framework.autoconfigure.oss.bean; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @author liaozan + * @since 2021/12/3 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UploadResult extends OssOperationResult { + + private static final long serialVersionUID = 7408490108665799098L; + + private String url; + + public static UploadResult success(String bucket, String objectKey, String url) { + UploadResult result = new UploadResult(); + result.setSuccess(true); + result.setBucket(bucket); + result.setObjectKey(objectKey); + result.setUrl(url); + return result; + } + + public static UploadResult fail(String bucket, String objectKey, String errorMsg) { + UploadResult result = new UploadResult(); + result.setSuccess(false); + result.setBucket(bucket); + result.setObjectKey(objectKey); + result.setErrorMsg(errorMsg); + return result; + } + +} \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/exception/OssException.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/exception/OssException.java new file mode 100644 index 0000000..d3d560e --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/exception/OssException.java @@ -0,0 +1,21 @@ +package com.schbrain.framework.autoconfigure.oss.exception; + +import com.schbrain.common.exception.BaseException; + +/** + * @author liaozan + * @since 2021/12/3 + */ +public class OssException extends BaseException { + + private static final long serialVersionUID = 7030196267316583562L; + + public OssException(String message) { + this(message, null); + } + + public OssException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/properties/OssProperties.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/properties/OssProperties.java new file mode 100644 index 0000000..bb15e49 --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/properties/OssProperties.java @@ -0,0 +1,55 @@ +package com.schbrain.framework.autoconfigure.oss.properties; + +import com.schbrain.common.util.support.ConfigurableProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * @author liaozan + * @since 2021/12/3 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ConfigurationProperties(prefix = "schbrain.oss") +public class OssProperties extends ConfigurableProperties { + + private String accessKeyId; + + private String secretAccessKey; + + private String endpoint; + + private String bucketName; + + private String directory; + + private String domain; + + @NestedConfigurationProperty + private StsProperties sts; + + public boolean isInValid() { + return accessKeyId == null || secretAccessKey == null; + } + + @Override + public String getDefaultNamespace() { + return "oss-common"; + } + + @Data + public static class StsProperties { + + private String endpoint; + + private String roleArn; + + private String roleSessionName; + + private Long durationSeconds = 900L; + + } + +} \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/util/OssUtils.java b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/util/OssUtils.java new file mode 100644 index 0000000..6ba2b0b --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/oss/util/OssUtils.java @@ -0,0 +1,390 @@ +package com.schbrain.framework.autoconfigure.oss.util; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.URLUtil; +import cn.hutool.http.HttpUtil; +import com.aliyun.oss.*; +import com.aliyun.oss.common.comm.ResponseMessage; +import com.aliyun.oss.model.*; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.auth.sts.AssumeRoleRequest; +import com.aliyuncs.auth.sts.AssumeRoleResponse; +import com.aliyuncs.auth.sts.AssumeRoleResponse.Credentials; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import com.schbrain.common.constants.DateTimeFormatters; +import com.schbrain.common.util.ApplicationName; +import com.schbrain.common.util.ValidateUtils; +import com.schbrain.framework.autoconfigure.oss.bean.*; +import com.schbrain.framework.autoconfigure.oss.exception.OssException; +import com.schbrain.framework.autoconfigure.oss.properties.OssProperties; +import com.schbrain.framework.autoconfigure.oss.properties.OssProperties.StsProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.*; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author liaozan + * @since 2021/12/3 + */ +@SuppressWarnings("unused") +@Slf4j +public class OssUtils { + + private static OSSClient ossClient; + private static DefaultAcsClient stsAcsClient; + private static OssProperties ossProperties; + private static StsProperties stsProperties; + private static String directory; + + public static void initialize(ConfigurableEnvironment environment, OssProperties properties) { + if (properties == null || properties.isInValid()) { + log.warn("ossProperties is invalid, OssUtils will not available until reinitialize with the correct configuration"); + return; + } + try { + ossProperties = properties; + ossClient = (OSSClient) initOssClient(properties); + stsAcsClient = initStsAcsClient(properties); + stsProperties = properties.getSts(); + directory = properties.getDirectory(); + if (directory == null) { + directory = ApplicationName.get(environment); + } + } catch (Exception e) { + log.warn("oss initialize fail, OssUtils will not available until reinitialize with the correct configuration", e); + } + } + + public static OssProperties getOssProperties() { + return ossProperties; + } + + public static OSSClient getOssClient() { + if (ossClient == null) { + throw new OssException("Oss client is null"); + } + return ossClient; + } + + // upload file + public static UploadResult upload(File file) { + return upload(file, file.getName()); + } + + public static UploadResult upload(File file, String objectKey) { + return upload(file, objectKey, false); + } + + public static UploadResult upload(File file, String objectKey, boolean allowOverwrite) { + return upload(file, objectKey, allowOverwrite, true); + } + + public static UploadResult upload(File file, String objectKey, boolean allowOverwrite, boolean appendPrefix) { + return upload(file, ossProperties.getBucketName(), objectKey, allowOverwrite, appendPrefix); + } + + public static UploadResult upload(File file, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix) { + return upload(file, bucket, objectKey, allowOverwrite, appendPrefix, null); + } + + public static UploadResult upload(File file, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix, ObjectMetadata metadata) { + return upload0(file, bucket, objectKey, allowOverwrite, appendPrefix, metadata); + } + + // upload stream + public static UploadResult upload(InputStream inputStream, String objectKey) { + return upload(inputStream, objectKey, false); + } + + public static UploadResult upload(InputStream inputStream, String objectKey, boolean appendPrefix) { + return upload(inputStream, objectKey, false, appendPrefix); + } + + public static UploadResult upload(InputStream inputStream, String objectKey, boolean allowOverwrite, boolean appendPrefix) { + return upload(inputStream, ossProperties.getBucketName(), objectKey, allowOverwrite, appendPrefix); + } + + public static UploadResult upload(InputStream inputStream, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix) { + return upload0(inputStream, bucket, objectKey, allowOverwrite, appendPrefix, null); + } + + public static UploadResult upload(InputStream inputStream, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix, ObjectMetadata metadata) { + return upload0(inputStream, bucket, objectKey, allowOverwrite, appendPrefix, metadata); + } + + public static DownloadResult download(String objectKey) { + return download(ossProperties.getBucketName(), objectKey); + } + + public static DownloadResult download(String bucket, String objectKey) { + boolean exist = exist(bucket, objectKey); + if (!exist) { + throw new OssException(String.format("object [%s] does not exist in bucket [%s]", objectKey, bucket)); + } + return download0(bucket, objectKey); + } + + public static String generatePreSignedUrl(String objectKey) { + return generatePreSignedUrl(objectKey, Duration.ofHours(1)); + } + + public static String generatePreSignedUrl(String objectKey, Duration expiration) { + return generatePreSignedUrl(objectKey, expiration, true); + } + + public static String generatePreSignedUrl(String objectKey, Duration expiration, boolean https) { + return generatePreSignedUrl(ossProperties.getBucketName(), objectKey, expiration, https); + } + + public static String generatePreSignedUrl(String bucket, String objectKey, Duration expiration, boolean https) { + return generatePreSignedUrl(bucket, objectKey, expiration, https, null); + } + + public static String generatePreSignedUrl(String bucket, String objectKey, Duration expiration, boolean https, ResponseHeaderOverrides headerOverrides) { + LocalDateTime expirationTime = LocalDateTime.now().plus(expiration); + Instant instant = expirationTime.atZone(ZoneId.systemDefault()).toInstant(); + + GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, objectKey); + request.setMethod(HttpMethod.GET); + request.setExpiration(Date.from(instant)); + request.setResponseHeaders(headerOverrides); + + String preSignedUrl = getOssClient().generatePresignedUrl(request).toString(); + if (HttpUtil.isHttp(preSignedUrl) && https) { + preSignedUrl = preSignedUrl.replaceFirst("http", "https"); + } + return replaceWithDefaultDomain(preSignedUrl); + } + + public static UploadCredentials generateUploadToken() { + AssumeRoleRequest request = new AssumeRoleRequest(); + request.setSysMethod(MethodType.POST); + request.setRoleArn(stsProperties.getRoleArn()); + request.setRoleSessionName(stsProperties.getRoleSessionName()); + request.setDurationSeconds(stsProperties.getDurationSeconds()); + AssumeRoleResponse response; + try { + response = getStsAcsClient().getAcsResponse(request); + } catch (com.aliyuncs.exceptions.ClientException e) { + return new UploadCredentials(e.getErrMsg()); + } + Credentials credentials = response.getCredentials(); + return new UploadCredentials(credentials); + } + + public static String buildUploadPath(String objectKey) { + objectKey = removePossibleSlash(objectKey); + String date = DateTimeFormatters.DATE_WITH_SLASH.format(LocalDate.now()); + return String.format("%s/%s/%s", directory, date, objectKey); + } + + public static String withBucketPrefix(String objectKey) { + return withBucketPrefix(objectKey, ossProperties.getBucketName()); + } + + public static String withBucketPrefix(String objectKey, String bucket) { + // oss supports https by default + String prefix = "https://" + bucket + "." + ossProperties.getEndpoint(); + return URLUtil.completeUrl(prefix, objectKey); + } + + public static String replaceWithDefaultDomain(String ossUrl) { + return replaceWithDomain(ossProperties.getDomain(), ossUrl); + } + + public static String replaceWithDomain(String domain, String ossUrl) { + if (!StringUtils.hasText(domain)) { + return ossUrl; + } + domain = URLUtil.normalize(domain); + URL originUrl = URLUtil.url(ossUrl); + URL domainUrl = URLUtil.url(domain); + try { + return new URL(domainUrl.getProtocol(), domainUrl.getHost(), domainUrl.getPort(), originUrl.getPath()).toString(); + } catch (MalformedURLException e) { + log.warn("replace domain fail, return the default url instead", e); + return ossUrl; + } + } + + public static CopyResult copyObject(String sourceKey, String destinationKey) { + return copyObject(ossProperties.getBucketName(), sourceKey, ossProperties.getBucketName(), destinationKey, false); + } + + public static CopyResult copyObject(String sourceKey, String destinationBucket, String destinationKey) { + return copyObject(ossProperties.getBucketName(), sourceKey, destinationBucket, destinationKey, false); + } + + public static CopyResult copyObject(String sourceBucket, String sourceKey, String destinationBucket, String destinationKey, boolean allowOverwrite) { + ValidateUtils.notEmpty(destinationKey, "destinationKey can not be empty"); + ValidateUtils.notEmpty(destinationBucket, "destinationBucket can not be empty"); + ValidateUtils.notEmpty(sourceKey, "sourceKey can not be empty"); + ValidateUtils.notEmpty(sourceBucket, "sourceBucket can not be empty"); + + boolean exist = exist(sourceBucket, sourceKey); + if (!exist) { + String errorMsg = StrFormatter.format("sourceKey:{} at sourceBucket:{} not exist", sourceKey, sourceBucket); + return CopyResult.fail(sourceBucket, sourceKey, destinationBucket, destinationKey, errorMsg); + } + + if (!allowOverwrite) { + boolean isExist = exist(destinationBucket, destinationKey); + if (isExist) { + String errorMsg = StrFormatter.format("destinationKey:{} at destinationBucket:{} has already exist"); + return CopyResult.fail(sourceBucket, sourceKey, destinationBucket, destinationKey, errorMsg); + } + } + + ossClient.copyObject(sourceBucket, sourceKey, destinationBucket, destinationKey); + return CopyResult.success(sourceBucket, sourceKey, destinationBucket, destinationKey); + } + + public static DeleteResult deleteObject(String objectKey) { + return deleteObject(List.of(objectKey)); + } + + public static DeleteResult deleteObject(List objectKeys) { + return deleteObject(ossProperties.getBucketName(), objectKeys); + } + + public static DeleteResult deleteObject(String bucket, List objectKeys) { + ValidateUtils.notEmpty(bucket, "bucket can not be empty"); + ValidateUtils.notEmpty(objectKeys, "objectKeys can not be empty"); + String notExistKeys = objectKeys.stream().filter(key -> !exist(bucket, key)).collect(Collectors.joining(",")); + if (StringUtils.hasText(notExistKeys)) { + String errorMsg = StrFormatter.format("objectKeys:[{}] not exist", notExistKeys); + return DeleteResult.fail(bucket, objectKeys, errorMsg); + } + DeleteObjectsRequest request = new DeleteObjectsRequest(bucket).withKeys(objectKeys); + DeleteObjectsResult deleteObjectsResult = getOssClient().deleteObjects(request); + List deletedObjects = deleteObjectsResult.getDeletedObjects(); + return DeleteResult.success(bucket, deletedObjects); + } + + public static boolean exist(String key) { + return exist(ossProperties.getBucketName(), key); + } + + public static boolean exist(String bucket, String key) { + return getOssClient().doesObjectExist(bucket, key); + } + + private static DownloadResult download0(String bucket, String objectKey) { + GetObjectRequest request = new GetObjectRequest(bucket, objectKey); + OSSObject ossObject = getOssClient().getObject(request); + return DownloadResult.success(bucket, objectKey, ossObject); + } + + private static UploadResult upload0(Object object, String bucket, String objectKey, boolean allowOverwrite, boolean appendPrefix, ObjectMetadata metadata) { + ValidateUtils.notEmpty(objectKey, "objectKey must not be empty"); + + String uploadPath = objectKey; + if (appendPrefix) { + uploadPath = buildUploadPath(objectKey); + } + + if (!allowOverwrite) { + boolean exist = exist(bucket, uploadPath); + if (exist) { + String errorMsg = String.format("object with path [%s] already exist in bucket [%s]", uploadPath, bucket); + return UploadResult.fail(bucket, objectKey, errorMsg); + } + } + + if (object instanceof File) { + File fileToUpload = (File) object; + if (fileToUpload.isDirectory()) { + if (FileUtil.isDirEmpty(fileToUpload)) { + return UploadResult.fail(bucket, objectKey, "upload directory is empty"); + } + String rootPath = FileUtil.getCanonicalPath(fileToUpload); + List fileList = FileUtil.loopFiles(fileToUpload); + for (File file : fileList) { + String subPath = FileUtil.subPath(rootPath, file); + doUpload(file, bucket, uploadPath + "/" + subPath, metadata); + } + // upload directory has no url to return + return UploadResult.success(bucket, null, null); + } + } + + return doUpload(object, bucket, uploadPath, metadata); + } + + private static UploadResult doUpload(Object object, String bucket, String uploadPath, ObjectMetadata metadata) { + return doUpload(createPutRequest(object, bucket, uploadPath, metadata)); + } + + private static UploadResult doUpload(PutObjectRequest request) { + PutObjectResult result; + String bucketName = request.getBucketName(); + String objectKey = request.getKey(); + try { + result = getOssClient().putObject(request); + } catch (OSSException | ClientException exception) { + log.error("upload object to oss fail", exception); + return UploadResult.fail(bucketName, objectKey, exception.getMessage()); + } + + ResponseMessage response = result.getResponse(); + if (response.isSuccessful()) { + String urlWithBucket = withBucketPrefix(objectKey, bucketName); + String finalUrl = replaceWithDefaultDomain(urlWithBucket); + return UploadResult.success(bucketName, objectKey, finalUrl); + } + + String errorMsg = response.getErrorResponseAsString(); + return UploadResult.fail(bucketName, objectKey, errorMsg); + } + + private static DefaultAcsClient getStsAcsClient() { + if (stsAcsClient == null) { + throw new OssException("stsAcsClient is null"); + } + return stsAcsClient; + } + + private static OSS initOssClient(OssProperties ossProperties) { + return new OSSClientBuilder().build(ossProperties.getEndpoint(), ossProperties.getAccessKeyId(), ossProperties.getSecretAccessKey()); + } + + private static DefaultAcsClient initStsAcsClient(OssProperties ossProperties) { + StsProperties stsProperties = ossProperties.getSts(); + DefaultProfile.addEndpoint("", "Sts", stsProperties.getEndpoint()); + IClientProfile profile = DefaultProfile.getProfile("", ossProperties.getAccessKeyId(), ossProperties.getSecretAccessKey()); + return new DefaultAcsClient(profile); + } + + private static String removePossibleSlash(String objectKey) { + while (objectKey.startsWith("/")) { + objectKey = objectKey.substring(1); + } + return objectKey; + } + + private static PutObjectRequest createPutRequest(Object object, String bucket, String objectKey, ObjectMetadata metadata) { + PutObjectRequest request; + if (object instanceof InputStream) { + request = new PutObjectRequest(bucket, objectKey, (InputStream) object, metadata); + } else if (object instanceof File) { + request = new PutObjectRequest(bucket, objectKey, (File) object, metadata); + } else { + throw new OssException("unknown upload object type"); + } + request.setProcess(""); + return request; + } + +} \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json b/starters/oss-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..1381501 --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,84 @@ +{ + "groups": [ + { + "name": "schbrain.oss", + "type": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.sts", + "type": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties", + "sourceMethod": "getSts()" + } + ], + "properties": [ + { + "name": "schbrain.oss.access-key-id", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.bucket-name", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.directory", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.domain", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.endpoint", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.name", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.namespace", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.prefix", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.secret-access-key", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties" + }, + { + "name": "schbrain.oss.sts.duration-seconds", + "type": "java.lang.Long", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties", + "defaultValue": 900 + }, + { + "name": "schbrain.oss.sts.endpoint", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties" + }, + { + "name": "schbrain.oss.sts.role-arn", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties" + }, + { + "name": "schbrain.oss.sts.role-session-name", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.oss.properties.OssProperties$StsProperties" + } + ], + "hints": [] +} \ No newline at end of file diff --git a/starters/oss-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/starters/oss-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..bff970b --- /dev/null +++ b/starters/oss-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.framework.autoconfigure.oss.OssAutoConfiguration \ No newline at end of file diff --git a/starters/pom.xml b/starters/pom.xml new file mode 100644 index 0000000..adcb13c --- /dev/null +++ b/starters/pom.xml @@ -0,0 +1,28 @@ + + + + 4.0.0 + + + com.schbrain.framework + schbrain-parent + ${revision} + + + + starters + pom + + + apollo-spring-boot-starter + cache-spring-boot-starter + dubbo-spring-boot-starter + logger-spring-boot-starter + mybatis-spring-boot-starter + oss-spring-boot-starter + xxl-job-spring-boot-starter + + + \ No newline at end of file diff --git a/starters/xxl-job-spring-boot-starter/pom.xml b/starters/xxl-job-spring-boot-starter/pom.xml new file mode 100644 index 0000000..13379a2 --- /dev/null +++ b/starters/xxl-job-spring-boot-starter/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + + com.schbrain.framework + starters + ${revision} + + + xxl-job-spring-boot-starter + + + + com.xuxueli + xxl-job-core + + + com.schbrain.framework + logger-spring-boot-starter + + + + \ No newline at end of file diff --git a/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/SchbrainXxlJobExecutor.java b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/SchbrainXxlJobExecutor.java new file mode 100644 index 0000000..e2fb868 --- /dev/null +++ b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/SchbrainXxlJobExecutor.java @@ -0,0 +1,83 @@ +package com.schbrain.framework.autoconfigure.xxl; + +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import com.xxl.job.core.thread.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; + +/** + * @author liaozan + * @since 2022/4/18 + */ +@Slf4j +public class SchbrainXxlJobExecutor extends XxlJobSpringExecutor implements InitializingBean, DisposableBean { + + private Field executorRegistryThreadStopField; + + private Field jobLogFileCleanThreadStopField; + + private Field triggerCallbackThreadStopField; + + private volatile boolean started = false; + + public SchbrainXxlJobExecutor() { + try { + this.executorRegistryThreadStopField = ExecutorRegistryThread.class.getDeclaredField("toStop"); + ReflectionUtils.makeAccessible(executorRegistryThreadStopField); + } catch (Exception e) { + this.executorRegistryThreadStopField = null; + } + try { + this.triggerCallbackThreadStopField = TriggerCallbackThread.class.getDeclaredField("toStop"); + ReflectionUtils.makeAccessible(triggerCallbackThreadStopField); + } catch (Exception e) { + this.triggerCallbackThreadStopField = null; + } + try { + this.jobLogFileCleanThreadStopField = JobLogFileCleanThread.class.getDeclaredField("toStop"); + ReflectionUtils.makeAccessible(jobLogFileCleanThreadStopField); + } catch (Exception e) { + this.jobLogFileCleanThreadStopField = null; + } + } + + @Override + public void start() throws Exception { + afterPropertiesSet(); + } + + @Override + public void afterPropertiesSet() throws Exception { + if (started) { + return; + } + super.start(); + started = true; + log.info("Xxl-job started"); + } + + @Override + public void destroy() { + if (!started) { + return; + } + super.destroy(); + resetThreadsStatus(); + started = false; + log.info("Xxl-job destroyed"); + } + + /** + * Reset xxl-job related thread to make it support restartable + */ + private void resetThreadsStatus() { + ReflectionUtils.setField(executorRegistryThreadStopField, ExecutorRegistryThread.getInstance(), false); + ReflectionUtils.setField(triggerCallbackThreadStopField, TriggerCallbackThread.getInstance(), false); + ReflectionUtils.setField(jobLogFileCleanThreadStopField, JobLogFileCleanThread.getInstance(), false); + } + +} \ No newline at end of file diff --git a/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/XxlJobAutoConfiguration.java b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/XxlJobAutoConfiguration.java new file mode 100644 index 0000000..378b44c --- /dev/null +++ b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/XxlJobAutoConfiguration.java @@ -0,0 +1,44 @@ +package com.schbrain.framework.autoconfigure.xxl; + +import com.schbrain.common.util.ApplicationName; +import com.schbrain.framework.autoconfigure.apollo.util.ConfigUtils; +import com.schbrain.framework.autoconfigure.logger.properties.LoggingFileProperties; +import com.schbrain.framework.autoconfigure.xxl.condition.XxlJobShouldAvailableCondition; +import com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties; +import com.xxl.job.core.executor.XxlJobExecutor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +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.Conditional; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.nio.file.Paths; + +/** + * @author liaozan + * @since 2022/1/8 + */ +@AutoConfiguration +@Conditional(XxlJobShouldAvailableCondition.class) +@EnableConfigurationProperties(XxlJobProperties.class) +public class XxlJobAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(XxlJobExecutor.class) + public SchbrainXxlJobExecutor schbrainXxlJobSpringExecutor(ConfigurableEnvironment environment) { + XxlJobProperties xxlJobProperties = ConfigUtils.loadConfig(environment, XxlJobProperties.class); + LoggingFileProperties loggingProperties = ConfigUtils.loadConfig(environment, LoggingFileProperties.class); + String applicationName = ApplicationName.get(environment); + SchbrainXxlJobExecutor executor = new SchbrainXxlJobExecutor(); + executor.setAdminAddresses(xxlJobProperties.getAdminAddresses()); + executor.setIp(xxlJobProperties.getIp()); + executor.setPort(xxlJobProperties.getPort()); + executor.setAppName(applicationName); + executor.setAccessToken(xxlJobProperties.getAccessToken()); + executor.setLogPath(Paths.get(loggingProperties.getLogPath(), "xxl-job").toString()); + executor.setLogRetentionDays(xxlJobProperties.getLogRetentionDays()); + return executor; + } + +} \ No newline at end of file diff --git a/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/condition/XxlJobShouldAvailableCondition.java b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/condition/XxlJobShouldAvailableCondition.java new file mode 100644 index 0000000..2154638 --- /dev/null +++ b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/condition/XxlJobShouldAvailableCondition.java @@ -0,0 +1,29 @@ +package com.schbrain.framework.autoconfigure.xxl.condition; + +import com.schbrain.common.util.EnvUtils; +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.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * @author liaozan + * @since 2022/4/20 + */ +public class XxlJobShouldAvailableCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + Environment environment = context.getEnvironment(); + if (EnvUtils.runningOnCloudPlatform(environment)) { + return ConditionOutcome.match("runningOnCloudPlatform"); + } + Boolean registerWithServer = environment.getProperty("schbrain.xxl.register", boolean.class, false); + if (Boolean.TRUE.equals(registerWithServer)) { + return ConditionOutcome.match("schbrain.xxl.register is true"); + } + return ConditionOutcome.noMatch("It is neither running on the cloud platform nor enabled"); + } + +} \ No newline at end of file diff --git a/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/handler/BaseJobHandler.java b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/handler/BaseJobHandler.java new file mode 100644 index 0000000..d8708be --- /dev/null +++ b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/handler/BaseJobHandler.java @@ -0,0 +1,36 @@ +package com.schbrain.framework.autoconfigure.xxl.handler; + +import com.fasterxml.jackson.databind.JsonNode; +import com.schbrain.common.util.JacksonUtils; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.log.XxlJobLogger; + +/** + * @author liaozan + * @since 2022/8/23 + */ +public abstract class BaseJobHandler extends IJobHandler { + + @Override + public ReturnT execute(String param) throws Exception { + JsonNode jsonNode = JacksonUtils.getJsonNode(param); + return execute(jsonNode); + } + + protected ReturnT execute(JsonNode param) throws Exception { + XxlJobLogger.log("{} does not implement the execute method, so return success directly", getClass()); + return success(); + } + + protected ReturnT success() { + return ReturnT.SUCCESS; + } + + protected ReturnT failed(String errMsg) { + ReturnT result = ReturnT.FAIL; + result.setMsg(errMsg); + return result; + } + +} \ No newline at end of file diff --git a/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/properties/XxlJobProperties.java b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/properties/XxlJobProperties.java new file mode 100644 index 0000000..61e2b1a --- /dev/null +++ b/starters/xxl-job-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/xxl/properties/XxlJobProperties.java @@ -0,0 +1,35 @@ +package com.schbrain.framework.autoconfigure.xxl.properties; + +import com.schbrain.common.util.InetUtils; +import com.schbrain.common.util.support.ConfigurableProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author liaozan + * @since 2022/1/8 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ConfigurationProperties(prefix = "schbrain.xxl") +public class XxlJobProperties extends ConfigurableProperties { + + private String adminAddresses; + + private String ip = InetUtils.findFirstNonLoopBackHostInfo().getIpAddress(); + + private int port = -1; + + private String accessToken; + + private int logRetentionDays = 7; + + private boolean register = false; + + @Override + public String getDefaultNamespace() { + return "xxl-job-common"; + } + +} \ No newline at end of file diff --git a/starters/xxl-job-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json b/starters/xxl-job-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..8c0f67c --- /dev/null +++ b/starters/xxl-job-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,60 @@ +{ + "groups": [ + { + "name": "schbrain.xxl", + "type": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties" + } + ], + "properties": [ + { + "name": "schbrain.xxl.access-token", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties" + }, + { + "name": "schbrain.xxl.admin-addresses", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties" + }, + { + "name": "schbrain.xxl.ip", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties" + }, + { + "name": "schbrain.xxl.log-retention-days", + "type": "java.lang.Integer", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties", + "defaultValue": 7 + }, + { + "name": "schbrain.xxl.name", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties" + }, + { + "name": "schbrain.xxl.namespace", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties" + }, + { + "name": "schbrain.xxl.port", + "type": "java.lang.Integer", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties", + "defaultValue": -1 + }, + { + "name": "schbrain.xxl.prefix", + "type": "java.lang.String", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties" + }, + { + "name": "schbrain.xxl.register", + "type": "java.lang.Boolean", + "sourceType": "com.schbrain.framework.autoconfigure.xxl.properties.XxlJobProperties", + "defaultValue": false + } + ], + "hints": [] +} \ No newline at end of file diff --git a/starters/xxl-job-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/starters/xxl-job-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..2b5583b --- /dev/null +++ b/starters/xxl-job-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.framework.autoconfigure.xxl.XxlJobAutoConfiguration \ No newline at end of file diff --git a/support/pom.xml b/support/pom.xml new file mode 100644 index 0000000..5902572 --- /dev/null +++ b/support/pom.xml @@ -0,0 +1,22 @@ + + + + 4.0.0 + + + com.schbrain.framework + schbrain-parent + ${revision} + + + support + pom + + + schbrain-spring-support + schbrain-base-dao + + + \ No newline at end of file diff --git a/support/schbrain-base-dao/pom.xml b/support/schbrain-base-dao/pom.xml new file mode 100644 index 0000000..ac1caf6 --- /dev/null +++ b/support/schbrain-base-dao/pom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + + + com.schbrain.framework + support + ${revision} + + + schbrain-base-dao + + + + com.schbrain.common + common-util + + + com.github.pagehelper + pagehelper + + + com.mysql + mysql-connector-j + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + + + + \ No newline at end of file diff --git a/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/BaseDao.java b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/BaseDao.java new file mode 100644 index 0000000..9b046ac --- /dev/null +++ b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/BaseDao.java @@ -0,0 +1,76 @@ +package com.schbrain.framework.dao; + +import com.github.pagehelper.Page; + +import java.util.List; + +/** + * description + * + * @author liwu on 2019/7/29 + */ +public interface BaseDao { + + String getTableName(); + + /** + * 插入单个领域对象 + *

注意:插入时只会指定非null的领域对象属性对应的列

+ * + * @param obj 待插入领域对象 + * @return 插入是否成功 + */ + Boolean add(T obj); + + /** + * 批量插入领域对象 + * + * @param objList 待插入领域对象列表 + * @param fields 插入时指定的领域对象属性列表,如果为空表示对象的所有属性 + * @return 影响行数 + */ + Integer addList(List objList, String... fields); + + T getById(long id); + + T getOneByObject(T obj); + + T getOneByCondition(String whereClause, Object... objs); + + List listByIdList(List idList); + + List listByCondition(String whereClause, Object... objs); + + List listByObject(T obj); + + Integer getCountByCondition(String whereClause, Object... objs); + + Page pageByCondition(int pageNum, int pageSize, String whereClause, Object... objs); + + /** + * 分页获取列表 + * + * @param pageNum 当前页码 + * @param pageSize 当前页记录数 + * @param whereClause where关键词后的条件语句 + * @param orderByClause order by关键词后的排序语句,注意:语句中不支持参数 + * @param objs where关键词后的条件语句中参数对应的值 + * @return page对象,包含记录及分页信息 + */ + Page pageByCondition(int pageNum, int pageSize, String whereClause, String orderByClause, Object... objs); + + Boolean deleteById(long id); + + Integer deleteByIdList(List idList); + + Integer deleteByCondition(String whereClause, Object... objs); + + Boolean updateById(T obj); + + Boolean updateByIdWithNull(T obj); + + Integer updateByCondition(T obj, String whereClause, Object... objs); + + Integer updateByCompleteSql(String completeSql, Object... objs); + +} \ No newline at end of file diff --git a/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/BaseMethodInvocationHandler.java b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/BaseMethodInvocationHandler.java new file mode 100644 index 0000000..812adf3 --- /dev/null +++ b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/BaseMethodInvocationHandler.java @@ -0,0 +1,65 @@ +package com.schbrain.framework.dao.mybatis; + +import com.schbrain.framework.dao.BaseDao; +import com.schbrain.framework.dao.mybatis.mapper.BaseMapper; +import org.apache.ibatis.reflection.ExceptionUtil; +import org.mybatis.spring.SqlSessionTemplate; + +import java.io.Serializable; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.*; + +/** + * description + * + * @author liwu on 2019/7/29 + */ +public class BaseMethodInvocationHandler implements InvocationHandler, Serializable { + + private static final long serialVersionUID = -5077848994288044943L; + + private final T originMapperProxy; + + private final BaseMapper baseMapper; + + public BaseMethodInvocationHandler(T originMapperProxy, SqlSessionTemplate sqlSession, Class mapperInterface) { + this.originMapperProxy = originMapperProxy; + baseMapper = new BaseMapper(sqlSession, mapperInterface); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + if (Object.class.equals(method.getDeclaringClass())) { + return method.invoke(this, args); + } else if (method.isDefault()) { + return invokeDefaultMethod(proxy, method, args); + } else if (isBaseMethod(method)) { + return invokeBaseMethod(method, args); + } + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + return method.invoke(originMapperProxy, args); + } + + private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { + final Constructor constructor = Lookup.class.getDeclaredConstructor(Class.class, int.class); + if (!constructor.isAccessible()) { + constructor.setAccessible(true); + } + final Class declaringClass = method.getDeclaringClass(); + return constructor + .newInstance(declaringClass, Lookup.PRIVATE | Lookup.PROTECTED | Lookup.PACKAGE | Lookup.PUBLIC) + .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args); + } + + private boolean isBaseMethod(Method method) { + return BaseDao.class.equals(method.getDeclaringClass()); + } + + private Object invokeBaseMethod(Method method, Object[] args) { + return baseMapper.invokeBaseMethod(method, args); + } + +} \ No newline at end of file diff --git a/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/CustomizeMapperFactoryBean.java b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/CustomizeMapperFactoryBean.java new file mode 100644 index 0000000..4632111 --- /dev/null +++ b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/CustomizeMapperFactoryBean.java @@ -0,0 +1,52 @@ +package com.schbrain.framework.dao.mybatis; + +import com.github.pagehelper.PageInterceptor; +import com.schbrain.framework.dao.BaseDao; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.session.Configuration; +import org.mybatis.spring.SqlSessionTemplate; +import org.mybatis.spring.mapper.MapperFactoryBean; + +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Properties; + +/** + * description + * + * @author liwu on 2019/7/29 + */ +public class CustomizeMapperFactoryBean extends MapperFactoryBean { + + public CustomizeMapperFactoryBean() { + super(); + } + + public CustomizeMapperFactoryBean(Class mapperInterface) { + super(mapperInterface); + } + + @Override + @SuppressWarnings("unchecked") + public T getObject() throws Exception { + T originMapperProxy = super.getObject(); + Class mapperInterface = getMapperInterface(); + if (!BaseDao.class.isAssignableFrom(mapperInterface)) { + return originMapperProxy; + } + // 判断是否已经添加分页过滤器 + SqlSessionTemplate sqlSession = getSqlSessionTemplate(); + Configuration configuration = sqlSession.getConfiguration(); + List interceptorList = configuration.getInterceptors(); + boolean hasPageInterceptor = interceptorList.stream().anyMatch(e -> PageInterceptor.class.isAssignableFrom(e.getClass())); + if (!hasPageInterceptor) { + PageInterceptor pageInterceptor = new PageInterceptor(); + pageInterceptor.setProperties(new Properties()); + configuration.addInterceptor(pageInterceptor); + } + // 创建代理 + BaseMethodInvocationHandler handler = new BaseMethodInvocationHandler<>(originMapperProxy, sqlSession, mapperInterface); + return (T) Proxy.newProxyInstance(getMapperInterface().getClassLoader(), new Class[]{mapperInterface}, handler); + } + +} \ No newline at end of file diff --git a/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/annotation/MapperConfig.java b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/annotation/MapperConfig.java new file mode 100644 index 0000000..d7dd032 --- /dev/null +++ b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/annotation/MapperConfig.java @@ -0,0 +1,19 @@ +package com.schbrain.framework.dao.mybatis.annotation; + +import java.lang.annotation.*; + +/** + * description + * + * @author liwu on 2019/8/1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +public @interface MapperConfig { + + Class domainClass(); + + String tableName(); + +} \ No newline at end of file diff --git a/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/exception/MapperParseException.java b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/exception/MapperParseException.java new file mode 100644 index 0000000..bf216fd --- /dev/null +++ b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/exception/MapperParseException.java @@ -0,0 +1,20 @@ +package com.schbrain.framework.dao.mybatis.exception; + +/** + * description + * + * @author liwu on 2019/8/1 + */ +public class MapperParseException extends RuntimeException { + + private static final long serialVersionUID = -4680549953650814744L; + + public MapperParseException() { + super(); + } + + public MapperParseException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/BaseMapper.java b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/BaseMapper.java new file mode 100644 index 0000000..dbd299d --- /dev/null +++ b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/BaseMapper.java @@ -0,0 +1,156 @@ +package com.schbrain.framework.dao.mybatis.mapper; + +import cn.hutool.core.bean.BeanUtil; +import com.github.pagehelper.PageHelper; +import com.schbrain.framework.dao.mybatis.annotation.MapperConfig; +import com.schbrain.framework.dao.mybatis.exception.MapperParseException; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.mybatis.spring.SqlSessionTemplate; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; + +/** + * 每一个Mapper实例对应一个BaseMapper实例 + * + * @author liwu on 2019/7/31 + */ +public class BaseMapper { + + private final SqlSessionTemplate sqlSession; + + private final Class mapperInterface; + private final BaseMapperStatement bms; + private Class domainClass; + private String tableName; + private Field[] fields; + + public BaseMapper(SqlSessionTemplate sqlSession, Class mapperInterface) { + this.sqlSession = sqlSession; + this.mapperInterface = mapperInterface; + parseMapperClass(); + bms = new BaseMapperStatement(sqlSession.getConfiguration(), mapperInterface, domainClass, tableName, fields); + } + + public Object invokeBaseMethod(Method method, Object[] args) { + String methodName = method.getName(); + switch (methodName) { + case "getTableName": + return tableName; + case "getById": + return getById(args); + case "listByIdList": + return getByIdList(args); + case "getCountByCondition": + return sqlSession.selectOne(bms.getCountByConditionMSId(), args); + case "getOneByCondition": + String sql = (String) args[0]; + if (!StringUtils.containsIgnoreCase(sql, "limit")) { + args[0] += " limit 1"; + } + return sqlSession.selectOne(bms.getListByConditionMSId(), args); + case "listByCondition": + return sqlSession.selectList(bms.getListByConditionMSId(), args); + case "getOneByObject": + return sqlSession.selectOne(bms.getGetOneByObjectMSId(), args[0]); + case "listByObject": + return sqlSession.selectList(bms.getListByObjectMSId(), args[0]); + case "pageByCondition": + return pageByCondition(args); + case "add": + return sqlSession.insert(bms.getAddMSId(), args[0]) > 0; + case "addList": + return sqlSession.insert(bms.getAddListMSId((String[]) args[1]), args[0]); + case "deleteById": + return sqlSession.delete(bms.getDeleteByIdMSId(), args[0]) > 0; + case "deleteByIdList": + return sqlSession.delete(bms.getDeleteByIdListMSId(), args[0]); + case "deleteByCondition": + return sqlSession.delete(bms.getDeleteByConditionMSId(), args); + case "updateById": + return updateById(args); + case "updateByIdWithNull": + return updateByIdWithNull(args); + case "updateByCondition": + return sqlSession.update(bms.getUpdateByConditionMSId(), args); + case "updateByCompleteSql": + return sqlSession.update(bms.getUpdateByCompleteSqlMSId(), args); + default: + return null; + } + } + + private void parseMapperClass() { + MapperConfig mapperConfig = mapperInterface.getAnnotation(MapperConfig.class); + if (null == mapperConfig) { + throw new MapperParseException(String.format("Can not find MapperConfig annotation in mapper class %s ", mapperInterface.getName())); + } + tableName = mapperConfig.tableName(); + if (StringUtils.isBlank(tableName)) { + throw new MapperParseException(String.format("Table name is blank in MapperConfig annotation in mapper class %s ", mapperInterface.getName())); + } + domainClass = mapperConfig.domainClass(); + if (Class.class.equals(domainClass)) { + throw new MapperParseException(String.format("Domain class is not set in MapperConfig annotation in mapper class %s ", mapperInterface.getName())); + } + fields = domainClass.getDeclaredFields(); + if (fields.length < 1) { + throw new MapperParseException(String.format("Domain class %s has no fields", domainClass.getName())); + } + } + + private Object getById(Object[] args) { + if (null == args[0]) { + throw new IllegalArgumentException("Parameter id can not be null"); + } + return sqlSession.selectOne(bms.getGetByIdMSId(), args[0]); + } + + private Object getByIdList(Object[] args) { + if (null == args[0]) { + throw new IllegalArgumentException("Parameter idList can not be null"); + } + // noinspection unchecked + List idList = (List) args[0]; + if (CollectionUtils.isEmpty(idList)) { + throw new IllegalArgumentException("Parameter idList can not be empty"); + } + return sqlSession.selectList(bms.getListByIdListMSId(), idList); + } + + private Object pageByCondition(Object[] args) { + Object[] conditionArgs; + String orderBy = null; + if (4 == args.length) { + conditionArgs = new Object[]{args[2], args[3]}; + } else { + conditionArgs = new Object[]{args[2], args[4]}; + orderBy = (String) args[3]; + } + if (StringUtils.isNotBlank(orderBy)) { + PageHelper.startPage((int) args[0], (int) args[1], orderBy); + } else { + PageHelper.startPage((int) args[0], (int) args[1]); + } + return sqlSession.selectList(bms.getListByConditionMSId(), conditionArgs); + } + + private Object updateById(Object[] args) { + Object updateObj = args[0]; + if (BeanUtil.getFieldValue(updateObj, "id") == null) { + throw new IllegalArgumentException("Id can not be null"); + } + return sqlSession.update(bms.getUpdateByIdMSId(), updateObj) > 0; + } + + private Object updateByIdWithNull(Object[] args) { + Object updateObj = args[0]; + if (BeanUtil.getFieldValue(updateObj, "id") == null) { + throw new IllegalArgumentException("Id can not be null"); + } + return sqlSession.update(bms.getUpdateByIdWithNullMSId(), updateObj) > 0; + } + +} \ No newline at end of file diff --git a/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/BaseMapperStatement.java b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/BaseMapperStatement.java new file mode 100644 index 0000000..f88f697 --- /dev/null +++ b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/mybatis/mapper/BaseMapperStatement.java @@ -0,0 +1,437 @@ +package com.schbrain.framework.dao.mybatis.mapper; + +import cn.hutool.crypto.SecureUtil; +import com.google.common.base.CaseFormat; +import com.google.common.base.Converter; +import com.schbrain.framework.dao.mybatis.mapper.sqlsource.*; +import org.apache.ibatis.builder.StaticSqlSource; +import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; +import org.apache.ibatis.mapping.*; +import org.apache.ibatis.scripting.LanguageDriver; +import org.apache.ibatis.session.Configuration; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; + +/** + * description + * + * @author liwu on 2019/8/2 + */ +public class BaseMapperStatement { + + private static final String GMT_UPDATE_FIELD = "gmtUpdate"; + + private static final String GMT_CREATE_FIELD = "gmtCreate"; + + private final Configuration configuration; + + private final Class mapperInterface; + + private final LanguageDriver languageDriver; + + private final Map fieldColumnMap = new LinkedHashMap<>(); + + private final String tableName; + + private final Class domainClass; + + private final Field[] fields; + private final List objectResultMapList = new ArrayList<>(1); + private final List intResultMapList = new ArrayList<>(1); + private String selectClause; + private String insertClause; + + public BaseMapperStatement(Configuration configuration, Class mapperInterface, Class domainClass, String tableName, Field[] fields) { + this.configuration = configuration; + this.languageDriver = configuration.getDefaultScriptingLanguageInstance(); + this.mapperInterface = mapperInterface; + this.tableName = tableName; + this.domainClass = domainClass; + this.fields = fields; + this.objectResultMapList.add(new ResultMap.Builder(configuration, "objectResultMap", domainClass, Collections.emptyList()).build()); + this.intResultMapList.add(new ResultMap.Builder(configuration, "intResult", int.class, Collections.emptyList()).build()); + setClause(); + addAddMS(); + addGetByIdMS(); + addListByIdListMS(); + addListByConditionMS(); + addListByObjectMS(); + addListOneByObjectMS(); + addCountByConditionMS(); + addDeleteByIdMS(); + addDeleteByIdListMS(); + addDeleteByConditionMS(); + addUpdateByIdMS(); + addUpdateByIdWithNullMS(); + addUpdateByConditionMS(); + addUpdateByCompleteSqlMS(); + } + + public String getAddMSId() { + String methodName = "add"; + return mapperInterface.getName() + "." + methodName; + } + + public String getAddListMSId(String... fields) { + String sql = getInsertListScript(fields); + String msId = mapperInterface.getName() + ".addList" + SecureUtil.md5(sql); + if (!configuration.hasStatement(msId)) { + addAddListMS(msId, sql); + } + return msId; + } + + public String getGetByIdMSId() { + String methodName = "getById"; + return mapperInterface.getName() + "." + methodName; + } + + public String getListByIdListMSId() { + String methodName = "getByIdList"; + return mapperInterface.getName() + "." + methodName; + } + + public String getListByConditionMSId() { + String methodName = "listByCondition"; + return mapperInterface.getName() + "." + methodName; + } + + public String getListByObjectMSId() { + String methodName = "listByObject"; + return mapperInterface.getName() + "." + methodName; + } + + public String getGetOneByObjectMSId() { + String methodName = "getOneByObject"; + return mapperInterface.getName() + "." + methodName; + } + + public String getCountByConditionMSId() { + String methodName = "getCountByCondition"; + return mapperInterface.getName() + "." + methodName; + } + + public String getDeleteByIdMSId() { + String methodName = "deleteById"; + return mapperInterface.getName() + "." + methodName; + } + + public String getDeleteByIdListMSId() { + String methodName = "deleteByIdList"; + return mapperInterface.getName() + "." + methodName; + } + + public String getDeleteByConditionMSId() { + String methodName = "deleteByCondition"; + return mapperInterface.getName() + "." + methodName; + } + + public String getUpdateByIdMSId() { + String methodName = "updateById"; + return mapperInterface.getName() + "." + methodName; + } + + public String getUpdateByIdWithNullMSId() { + String methodName = "updateByIdWithNull"; + return mapperInterface.getName() + "." + methodName; + } + + public String getUpdateByConditionMSId() { + String methodName = "updateByCondition"; + return mapperInterface.getName() + "." + methodName; + } + + public String getUpdateByCompleteSqlMSId() { + String methodName = "updateByCompleteSql"; + return mapperInterface.getName() + "." + methodName; + } + + private void setClause() { + Converter converter = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE); + StringBuilder selectClause = new StringBuilder("select "); + StringBuilder insertClause = new StringBuilder("insert into "); + insertClause.append(tableName).append("("); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + String fieldName = field.getName(); + String columnName = converter.convert(fieldName); + fieldColumnMap.put(fieldName, columnName); + selectClause.append(" ").append(columnName).append(","); + insertClause.append(" ").append(columnName).append(","); + } + + selectClause.replace(selectClause.length() - 1, selectClause.length(), " from ").append(tableName).append(" "); + insertClause.replace(insertClause.length() - 1, insertClause.length(), ") values "); + this.selectClause = selectClause.toString(); + this.insertClause = insertClause.toString(); + } + + ///////////////////add + private void addAddMS() { + String sql = getInsertScript(); + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, domainClass); + addUpdateMappedStatement(getAddMSId(), sqlSource, SqlCommandType.INSERT); + } + + private String getInsertScript() { + StringBuilder sb = new StringBuilder(""); + return sb.toString(); + } + + ///////////////////addList + private void addAddListMS(String msId, String sql) { + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, List.class); + addUpdateMappedStatement(msId, sqlSource, SqlCommandType.INSERT); + } + + private String getInsertListScript(String... fields) { + String tmpInsertClause; + Set fieldSet; + if (null == fields || 0 == fields.length) { + tmpInsertClause = insertClause; + fieldSet = fieldColumnMap.keySet(); + } else { + fieldSet = new LinkedHashSet<>(); + StringBuilder sb = new StringBuilder("insert into "); + sb.append(tableName).append("("); + + for (String field : fields) { + if (GMT_UPDATE_FIELD.equals(field) || GMT_CREATE_FIELD.equals(field)) { + continue; + } + String column = fieldColumnMap.get(field); + if (null == column) { + throw new IllegalArgumentException("Can not find column of field:" + field); + } + fieldSet.add(field); + sb.append(column).append(","); + } + // 判断是否有gmtUpdate和gmtCreate + String gmtUpdate = fieldColumnMap.get(GMT_UPDATE_FIELD); + if (null != gmtUpdate) { + fieldSet.add(GMT_UPDATE_FIELD); + sb.append(gmtUpdate).append(","); + } + String gmtCreate = fieldColumnMap.get(GMT_CREATE_FIELD); + if (null != gmtCreate) { + fieldSet.add(GMT_CREATE_FIELD); + sb.append(gmtCreate).append(","); + } + sb.replace(sb.length() - 1, sb.length(), ") values "); + tmpInsertClause = sb.toString(); + } + StringBuilder sql = new StringBuilder(""); + return sql.toString(); + } + + ////////////////getById + private void addGetByIdMS() { + String sql = selectClause + " where id = ?"; + ParameterMapping pm = new ParameterMapping.Builder(configuration, "id", Long.class).build(); + StaticSqlSource sqlSource = new StaticSqlSource(configuration, sql, Collections.singletonList(pm)); + addSelectMappedStatement(getGetByIdMSId(), sqlSource); + } + + ///////////////////listByIdList + private void addListByIdListMS() { + String sql = ""; + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, List.class); + addSelectMappedStatement(getListByIdListMSId(), sqlSource); + } + + /////////////////listByCondition + private void addListByConditionMS() { + SqlSource sqlSource = new ListByConditionSqlSource(configuration, selectClause); + addSelectMappedStatement(getListByConditionMSId(), sqlSource); + } + + /////////////////listByObject + private void addListByObjectMS() { + SqlSource sqlSource = languageDriver.createSqlSource(configuration, getListByObjectSql(false), domainClass); + addSelectMappedStatement(getListByObjectMSId(), sqlSource); + } + + private String getListByObjectSql(boolean onlyOne) { + StringBuilder sql = new StringBuilder(""); + return sql.toString(); + } + + /////////////////getOneByObject + private void addListOneByObjectMS() { + SqlSource sqlSource = languageDriver.createSqlSource(configuration, getListByObjectSql(true), domainClass); + addSelectMappedStatement(getGetOneByObjectMSId(), sqlSource); + } + + /////////////////getCountByCondition + private void addCountByConditionMS() { + SqlSource sqlSource = new CountByConditionSqlSource(configuration, tableName); + addCountMappedStatement(getCountByConditionMSId(), sqlSource); + } + + ///////////////////deleteById + private void addDeleteByIdMS() { + String sql = "delete from " + tableName + " where id = ?"; + ParameterMapping pm = new ParameterMapping.Builder(configuration, "id", Long.class).build(); + StaticSqlSource sqlSource = new StaticSqlSource(configuration, sql, Collections.singletonList(pm)); + addUpdateMappedStatement(getDeleteByIdMSId(), sqlSource, SqlCommandType.DELETE); + } + + ///////////////////deleteByIdList + private void addDeleteByIdListMS() { + String sql = ""; + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, List.class); + addUpdateMappedStatement(getDeleteByIdListMSId(), sqlSource, SqlCommandType.DELETE); + } + + /////////////////deleteByCondition + private void addDeleteByConditionMS() { + SqlSource sqlSource = new DeleteByConditionSqlSource(configuration, tableName); + addUpdateMappedStatement(getDeleteByConditionMSId(), sqlSource, SqlCommandType.DELETE); + } + + /////////////////updateById + private void addUpdateByIdMS() { + String sql = getUpdateScript(false) + " where id = #{id}"; + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, domainClass); + addUpdateMappedStatement(getUpdateByIdMSId(), sqlSource, SqlCommandType.UPDATE); + } + + private String getUpdateScript(boolean withNull) { + StringBuilder sb = new StringBuilder(""; + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, domainClass); + addUpdateMappedStatement(getUpdateByIdWithNullMSId(), sqlSource, SqlCommandType.UPDATE); + } + + /////////////////updateByCondition + private void addUpdateByConditionMS() { + SqlSource sqlSource = new UpdateByConditionSqlSource(configuration, languageDriver, getUpdateByConditionScript()); + addUpdateMappedStatement(getUpdateByConditionMSId(), sqlSource, SqlCommandType.UPDATE); + } + + private String getUpdateByConditionScript() { + StringBuilder sb = new StringBuilder(""; + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Map.class); + BoundSql boundSql = sqlSource.getBoundSql(paramMap); + paramMap.forEach(boundSql::setAdditionalParameter); + return boundSql; + } + +} \ No newline at end of file diff --git a/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/util/SQLUtil.java b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/util/SQLUtil.java new file mode 100644 index 0000000..5cfd055 --- /dev/null +++ b/support/schbrain-base-dao/src/main/java/com/schbrain/framework/dao/util/SQLUtil.java @@ -0,0 +1,49 @@ +package com.schbrain.framework.dao.util; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * description + * + * @author liwu on 2019/8/19 + */ +public class SQLUtil { + + public static String buidInClause(String columnName, Class valueType, List valueList) { + if (CollectionUtils.isEmpty(valueList)) { + throw new IllegalArgumentException("Value list can not be empty."); + } + valueList = valueList.stream().filter(Objects::nonNull).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(valueList)) { + throw new IllegalArgumentException("Value list can not be empty."); + } + StringBuilder sb = new StringBuilder(" "); + sb.append(columnName).append(" in ("); + if (Integer.class.isAssignableFrom(valueType) || Long.class.isAssignableFrom(valueType) || + Short.class.isAssignableFrom(valueType) || Byte.class.isAssignableFrom(valueType) || + Double.class.isAssignableFrom(valueType) || Float.class.isAssignableFrom(valueType)) { + sb.append(StringUtils.join(valueList, ',')).append(")"); + } else { + valueList.forEach(e -> { + sb.append("'").append(escapeSql(e)).append("',"); + }); + sb.deleteCharAt(sb.length() - 1).append(")"); + } + sb.append(" "); + return sb.toString(); + } + + private static String escapeSql(Object o) { + if (o instanceof String) { + return StringUtils.replace((String) o, "'", "''"); + } else { + return String.valueOf(o); + } + } + +} \ No newline at end of file diff --git a/support/schbrain-spring-support/pom.xml b/support/schbrain-spring-support/pom.xml new file mode 100644 index 0000000..c10ab1b --- /dev/null +++ b/support/schbrain-spring-support/pom.xml @@ -0,0 +1,36 @@ + + + + 4.0.0 + + + com.schbrain.framework + support + ${revision} + + + schbrain-spring-support + + + + com.schbrain.common + common-util + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-configuration-processor + + + org.springframework.boot + spring-boot-starter-data-elasticsearch + true + + + + \ No newline at end of file diff --git a/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/BeanPostProcessorAdapter.java b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/BeanPostProcessorAdapter.java new file mode 100644 index 0000000..fc77e4f --- /dev/null +++ b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/BeanPostProcessorAdapter.java @@ -0,0 +1,113 @@ +package com.schbrain.framework.support.spring; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.context.*; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * @author liaozan + * @since 2021/11/22 + */ +@SuppressWarnings({"unchecked", "unused"}) +public abstract class BeanPostProcessorAdapter implements SmartInstantiationAwareBeanPostProcessor, ApplicationContextAware { + + private final Class beanType; + + protected ConfigurableApplicationContext applicationContext; + + protected ConfigurableListableBeanFactory beanFactory; + + protected ConfigurableEnvironment environment; + + public BeanPostProcessorAdapter() { + ParameterizedType parameterizedType = (ParameterizedType) getClass().getGenericSuperclass(); + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + this.beanType = (Class) actualTypeArguments[0]; + } + + @Override + public final Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { + if (ClassUtils.isAssignable(beanType, beanClass)) { + return doPostProcessBeforeInstantiation((Class) beanClass); + } + return null; + } + + @Override + public final Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (ClassUtils.isAssignableValue(beanType, bean)) { + return doPostProcessBeforeInitialization((T) bean, beanName); + } + return bean; + } + + public final boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { + if (ClassUtils.isAssignableValue(beanType, bean)) { + return doPostProcessAfterInstantiation((T) bean); + } + return true; + } + + @Override + public final Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (ClassUtils.isAssignableValue(beanType, bean)) { + return doPostProcessAfterInitialization((T) bean, beanName); + } + return bean; + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + Assert.isInstanceOf(ConfigurableApplicationContext.class, context, "require ConfigurableApplicationContext"); + this.applicationContext = (ConfigurableApplicationContext) context; + this.beanFactory = this.applicationContext.getBeanFactory(); + this.environment = this.applicationContext.getEnvironment(); + } + + public final Class getBeanType() { + return beanType; + } + + // region Instantiation + protected T doPostProcessBeforeInstantiation(Class beanClass) { + return null; + } + + protected boolean doPostProcessAfterInstantiation(T bean) { + processAfterInstantiation(bean); + return true; + } + + protected void processAfterInstantiation(T bean) throws BeansException { + + } + // endregion + + // region Initialization + protected T doPostProcessBeforeInitialization(T bean, String beanName) throws BeansException { + processBeforeInitialization(bean, beanName); + return bean; + } + + protected void processBeforeInitialization(T bean, String beanName) throws BeansException { + + } + + protected T doPostProcessAfterInitialization(T bean, String beanName) throws BeansException { + processAfterInitialization(bean, beanName); + return bean; + } + + protected void processAfterInitialization(T bean, String beanName) throws BeansException { + + } + // endregion + +} \ No newline at end of file diff --git a/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/BootstrapContextListenerComposite.java b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/BootstrapContextListenerComposite.java new file mode 100644 index 0000000..0f5f3c9 --- /dev/null +++ b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/BootstrapContextListenerComposite.java @@ -0,0 +1,48 @@ +package com.schbrain.framework.support.spring; + +import cn.hutool.core.lang.Singleton; +import org.springframework.boot.BootstrapContextClosedEvent; +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author liaozan + * @since 2021/11/22 + */ +public class BootstrapContextListenerComposite implements ApplicationListener { + + private final List adapters = new ArrayList<>(); + + public static BootstrapContextListenerComposite getInstance() { + return Singleton.get(BootstrapContextListenerComposite.class); + } + + public void addListener(EnvironmentPostProcessorAdapter adapter) { + if (adapters.contains(adapter)) { + return; + } + adapters.add(adapter); + } + + @Override + public void onApplicationEvent(BootstrapContextClosedEvent event) { + if (adapters.isEmpty()) { + return; + } + + AnnotationAwareOrderComparator.sort(adapters); + + ConfigurableBootstrapContext bootstrapContext = (ConfigurableBootstrapContext) event.getBootstrapContext(); + ConfigurableApplicationContext applicationContext = event.getApplicationContext(); + + for (EnvironmentPostProcessorAdapter adapter : adapters) { + adapter.onBootstrapContextClosed(bootstrapContext, applicationContext); + } + } + +} diff --git a/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/EnvironmentPostProcessorAdapter.java b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/EnvironmentPostProcessorAdapter.java new file mode 100644 index 0000000..8731ec4 --- /dev/null +++ b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/EnvironmentPostProcessorAdapter.java @@ -0,0 +1,42 @@ +package com.schbrain.framework.support.spring; + +import lombok.Getter; +import org.apache.commons.logging.Log; +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * @author liaozan + * @since 2021/11/22 + */ +@Getter +public abstract class EnvironmentPostProcessorAdapter implements EnvironmentPostProcessor { + + private final Log log; + private final DeferredLogFactory deferredLogFactory; + private final ConfigurableBootstrapContext bootstrapContext; + + public EnvironmentPostProcessorAdapter(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext) { + this.log = logFactory.getLog(getClass()); + this.bootstrapContext = bootstrapContext; + this.deferredLogFactory = logFactory; + this.addListener(this.bootstrapContext); + } + + protected void onBootstrapContextClosed(ConfigurableBootstrapContext bootstrapContext, ConfigurableApplicationContext applicationContext) { + onBootstrapContextClosed(applicationContext); + } + + protected void onBootstrapContextClosed(ConfigurableApplicationContext applicationContext) { + + } + + private void addListener(ConfigurableBootstrapContext bootstrapContext) { + BootstrapContextListenerComposite listener = BootstrapContextListenerComposite.getInstance(); + listener.addListener(this); + bootstrapContext.addCloseListener(listener); + } + +} \ No newline at end of file diff --git a/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/defaults/DefaultPropertiesEnvironmentPostProcessor.java b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/defaults/DefaultPropertiesEnvironmentPostProcessor.java new file mode 100644 index 0000000..e6e032e --- /dev/null +++ b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/defaults/DefaultPropertiesEnvironmentPostProcessor.java @@ -0,0 +1,99 @@ +package com.schbrain.framework.support.spring.defaults; + +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.ArrayUtil; +import com.schbrain.common.constants.DateTimeFormatters; +import com.schbrain.common.util.EnvUtils; +import com.schbrain.common.util.PortUtils; +import com.schbrain.framework.support.spring.EnvironmentPostProcessorAdapter; +import org.springframework.boot.*; +import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show; +import org.springframework.boot.actuate.info.InfoPropertiesInfoContributor.Mode; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.boot.web.server.Shutdown; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.ClassUtils; +import org.springframework.util.unit.DataSize; + +import java.util.*; + +/** + * @author liaozan + * @since 2021/12/18 + */ +public class DefaultPropertiesEnvironmentPostProcessor extends EnvironmentPostProcessorAdapter implements Ordered { + + private static final String SPRING_PROFILE_ACTIVE = "spring.profiles.active"; + private static final String DUBBO_REGISTER_KEY = "dubbo.registry.register"; + + public DefaultPropertiesEnvironmentPostProcessor(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext) { + super(logFactory, bootstrapContext); + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + Map defaultProperties = new HashMap<>(); + // management + defaultProperties.put("management.endpoints.web.exposure.include", "*"); + defaultProperties.put("management.endpoints.enabled-by-default", true); + defaultProperties.put("management.endpoint.health.show-details", Show.ALWAYS); + defaultProperties.put("management.endpoint.health.show-components", Show.ALWAYS); + defaultProperties.put("management.info.git.mode", Mode.FULL); + defaultProperties.put("management.server.port", PortUtils.findAvailablePort(1024)); + // servlet + defaultProperties.put("spring.servlet.multipart.max-file-size", DataSize.ofBytes(-1)); + defaultProperties.put("spring.servlet.multipart.max-request-size", DataSize.ofBytes(-1)); + // mvc + defaultProperties.put("spring.mvc.throw-exception-if-no-handler-found", true); + // datetime + defaultProperties.put("spring.mvc.format.date", DateTimeFormatters.DATE_PATTERN); + defaultProperties.put("spring.mvc.format.time", DateTimeFormatters.TIME_PATTERN); + defaultProperties.put("spring.mvc.format.date-time", DateTimeFormatters.DATE_TIME_PATTERN); + defaultProperties.put("spring.jackson.date-format", DateTimeFormatters.DATE_TIME_PATTERN); + defaultProperties.put("spring.jackson.time-zone", TimeZone.getDefault()); + // others + defaultProperties.put("spring.web.resources.add-mappings", false); + defaultProperties.put("spring.main.allow-circular-references", true); + defaultProperties.put("spring.main.banner-mode", Banner.Mode.OFF); + defaultProperties.put("server.shutdown", Shutdown.GRACEFUL); + // dubbo + configureDubboRegistrationIfPresent(environment, defaultProperties); + // active profile + configureActiveProfileIfPresent(environment, defaultProperties); + environment.setDefaultProfiles(EnvUtils.DEVELOPMENT); + DefaultPropertiesPropertySource.addOrMerge(defaultProperties, environment.getPropertySources()); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + private void configureActiveProfileIfPresent(ConfigurableEnvironment environment, Map defaultProperties) { + if (ArrayUtil.isEmpty(environment.getActiveProfiles())) { + environment.setActiveProfiles(EnvUtils.DEVELOPMENT); + defaultProperties.put(SPRING_PROFILE_ACTIVE, EnvUtils.DEVELOPMENT); + getLog().info(StrFormatter.format("{} is unset, set to {} by default", SPRING_PROFILE_ACTIVE, EnvUtils.DEVELOPMENT)); + } + } + + private void configureDubboRegistrationIfPresent(ConfigurableEnvironment environment, Map defaultProperties) { + if (!dubboInClassPath()) { + return; + } + if (EnvUtils.runningOnCloudPlatform(environment)) { + return; + } + if (!environment.containsProperty(DUBBO_REGISTER_KEY)) { + getLog().info(StrFormatter.format("Not running on CloudPlatform, {} is set to false by default", DUBBO_REGISTER_KEY)); + getLog().info(StrFormatter.format("If you want force to register with Dubbo Registry, set {} = true", DUBBO_REGISTER_KEY)); + defaultProperties.put(DUBBO_REGISTER_KEY, false); + } + } + + private boolean dubboInClassPath() { + return ClassUtils.isPresent("org.apache.dubbo.config.bootstrap.DubboBootstrap", getClass().getClassLoader()); + } + +} \ No newline at end of file diff --git a/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/elasticsearch/ElasticsearchFeatureAutoConfiguration.java b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/elasticsearch/ElasticsearchFeatureAutoConfiguration.java new file mode 100644 index 0000000..ac6dbb2 --- /dev/null +++ b/support/schbrain-spring-support/src/main/java/com/schbrain/framework/support/spring/elasticsearch/ElasticsearchFeatureAutoConfiguration.java @@ -0,0 +1,61 @@ +package com.schbrain.framework.support.spring.elasticsearch; + +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig.Builder; +import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.apache.http.impl.nio.reactor.IOReactorConfig; +import org.apache.http.protocol.HttpContext; +import org.elasticsearch.client.RestClientBuilder; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.elasticsearch.RestClientBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * @author liaozan + * @since 2022/5/6 + */ +@AutoConfiguration +@ConditionalOnClass(ElasticsearchRestTemplate.class) +public class ElasticsearchFeatureAutoConfiguration { + + @Bean + public RestClientBuilderCustomizer elasticsearchRestClientBuilderCustomizer() { + return new RestClientBuilderCustomizer() { + @Override + public void customize(RestClientBuilder builder) { + } + + @Override + public void customize(HttpAsyncClientBuilder builder) { + IOReactorConfig ioReactorConfig = IOReactorConfig.custom().setSoKeepAlive(true).build(); + builder.setDefaultIOReactorConfig(ioReactorConfig); + builder.setKeepAliveStrategy(new KeepAliveStrategy()); + } + + @Override + public void customize(Builder builder) { + builder.setSocketTimeout((int) Duration.ofMinutes(1).toMillis()); + } + }; + } + + private static class KeepAliveStrategy extends DefaultConnectionKeepAliveStrategy { + + @Override + public long getKeepAliveDuration(HttpResponse response, HttpContext context) { + long keepAliveDuration = super.getKeepAliveDuration(response, context); + if (keepAliveDuration < 0) { + keepAliveDuration = TimeUnit.MINUTES.toMillis(10); + } + return keepAliveDuration; + } + + } + +} \ No newline at end of file diff --git a/support/schbrain-spring-support/src/main/resources/META-INF/spring.factories b/support/schbrain-spring-support/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..fb4a691 --- /dev/null +++ b/support/schbrain-spring-support/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.env.EnvironmentPostProcessor=com.schbrain.framework.support.spring.defaults.DefaultPropertiesEnvironmentPostProcessor \ No newline at end of file diff --git a/support/schbrain-spring-support/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/support/schbrain-spring-support/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..909f6b2 --- /dev/null +++ b/support/schbrain-spring-support/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.schbrain.framework.support.spring.elasticsearch.ElasticsearchFeatureAutoConfiguration \ No newline at end of file -- GitLab