diff --git a/.gitignore b/.gitignore index be3e648339a52f7c7d156f52366518f79622196e..662be2ee971c50c6a21c82e0ac3d49134e0897d0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,13 @@ logs *.log .logs *.iws +*.report *.classpath .DS_Store +.ossutil_checkpoint *.patch -~$* \ No newline at end of file +~$* +node_modules +yarn.lock +dist +package-lock.json diff --git a/README.md b/README.md index 5561be798f756e00f9774f1eae5e797130ccd358..f00db2a2e48bdb6004f50b325ae8bafcdc143e98 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ +![](./doc/docs/static/images/jboot-logo.png) + +Jboot 是一个基于 JFinal、Dubbo、Seata、Sentinel、ShardingSphere、Nacos 等开发的国产框架。 + +其特点是: + +- 1、基于 JFinal 完整的 MVC + ORM 支持。 +- 2、支持多数据源、分库分表和分布式事务。 +- 3、支持 Dubbo RPC 的完整功能,有超过 1亿+ 用户产品正在使用。 +- 4、完整的单点限流和分布式限流功能 +- 5、支持基基于 Apollo 和 Nacos 的分布式配置中心 +- 6、完整的分布式缓存、分布式session、分布式附件支持 +- 7、内置功能强劲的门户网关 +- 8、完整的单元测试支持 +- 9、完善代码生成工具 和 API 文档生成工具 +- 10、Docker、K8S 友好 ## 开始 @@ -8,7 +24,7 @@ io.jboot jboot - 3.0.1 + 4.1.4 ``` @@ -16,7 +32,7 @@ ```java @RequestMapping("/") -public class HelloworldController extends JbootController { +public class Helloworld extends JbootController { public void index(){ renderText("hello world"); @@ -31,39 +47,26 @@ public class HelloworldController extends JbootController { ## 帮助文档 -- [demos](./src/test/java/io/jboot/test) -- [安装](./doc/docs/install.md) -- [2分钟快速开始](./doc/docs/quickstart.md) -- [热加载](./doc/docs/hotload.md) -- [Undertow](./doc/docs/undertow.md) -- [配置](./doc/docs/config.md) -- [JFinalConfig](./doc/docs/jfinalConfig.md) -- [WebSocket](./doc/docs/websocket.md) -- [MVC](./doc/docs/mvc.md) -- [数据库](./doc/docs/db.md) -- [缓存](./doc/docs/cache.md) -- [RPC远程调用](./doc/docs/rpc.md) -- [MQ消息队列](./doc/docs/mq.md) -- [任务调度](./doc/docs/schedule.md) -- [限流](./doc/docs/limit.md) -- [监控](./doc/docs/metrics.md) -- [序列化](./doc/docs/serialize.md) -- [事件机制](./doc/docs/event.md) -- [SPI扩展机制](./doc/docs/spi.md) -- [代码生成器](./doc/docs/codegen.md) -- [项目构建](./doc/docs/build.md) -- [项目部署](./doc/docs/deploy.md) -- [Jboot与Docker](./doc/docs/docker.md) -- [1.x 升级到 2.x 教程](./doc/docs/upgrade.md) -- [交流社区、QQ群和微信群](./doc/docs/communication.md) -- 第三方组件的支持 - - [redis](./doc/docs/redis.md) - - [shiro](./doc/docs/shiro.md) - - [jwt](./doc/docs/jwt.md) - - [swagger](./doc/docs/swagger.md) +- 文档请访问:[www.jboot.com.cn](http://www.jboot.com.cn) +- Demos 请访问:[这里](./src/test/java/io/jboot/test) +## 广告 + +- 一个好用的在线代码格式化工具:[http://www.CodeFormat.CN](http://www.codeformat.cn) ## 微信交流群 -![](./doc/docs/imgs/jboot-wechat-group.png) +![](./doc/docs/static/images/jboot-wechat-group.png) + + +## JbootAdmin + +JbootAdmin 是 Jboot 官方推出的、收费的、企业级快速开发框架,真诚的为各位开发者提供一站式、保姆式的开发服务。 +关于 JbootAdmin 的更多的功能请咨询海哥(微信:wx198819880),或请访问以下网址: + +[http://jboot.io/jbootadmin/feature.html](http://jboot.io/jbootadmin/feature.html) + + +![](./doc/jbootadmin/images/jbootadmin-demo.jpg) + diff --git a/changes.txt b/changes.txt index 0222680079b80a3277f74aebfb46276e3ed4f60b..39f2a4402b19ec546894b92398ad0373104c7880 100644 --- a/changes.txt +++ b/changes.txt @@ -1,3 +1,1397 @@ +jboot v4.1.3 2023-06-21: +优化:线程池同一使用 NamedThreadPools.java 进行构建 +优化:修改版错别字 taked 为 took +优化:添加更多的扫描 jar 排除,减少启动消耗时间 + + + +jboot v4.1.2 2023-06-20: +修复:Oracle 数据库在某些极端情况下出错的问题 + + + +jboot v4.1.1 2023-06-19: +修复:修复 MQ 通知线程在高并发场景下无法及时回收的问题 +优化:升级 JFinal 等依赖到最新版本 + + + +jboot v4.1.0 2023-05-12: +新增:自定义 JFinalFilter 的支持 +优化:重命名 JbootAccessTokenCache 为 WechatAccessTokenCache +优化:JbootAppListenerManager 中重复的 foreach 操作 感谢 @梦行 +优化:升级 Jfinal/jfinal-undertow/jsoup 等到最新版本 + + + +jboot v4.0.9 2023-03-08: +优化:JbootResourceLoader 忽略掉 windows 和 mac 下的临时文件 +优化:修改 SqlBuilder.java 的错别字 +修复:Columns.orEqs 没有添加括号导致 sql 逻辑不对的问题 + + + +jboot v4.0.8 2023-02-05: +优化:升级 ShardingJDBC 到 5.x 最新版本 +优化:升级 Columns.in 和 notIn 等方法和错别字 +优化:升级 jfinal-undertow、jackson、metrics 等到最新版本 + + + +jboot v4.0.7 2023-01-06: +优化:重构 AttachmentManager 使之更加灵活易用 +新增:StrUtil.isStartsWithAny() 方法 +新增:FileUtil.getFileMD5() 等方法 +新增:CdnUtil.appendCdnDomain() 方法 + + + +jboot v4.0.6 2022-12-31: +修复:缓存注解自动生成 key 无法支持集合参数的问题 +优化:ApplicationUtil.java 使其在 window 下输出正确的 classpath 路径 +优化:修改 ValueFilterInterceptor.java 里的错别字 +优化:JbootConfigManager.java 配置独立目录时,再次读取 jboot.properties 文件 + + + +jboot v4.0.5 2022-12-29: +新增:jboot.cache.cacheSyncMqChannel 的配置,用于对分布式缓存的 channel 进行自定义 +优化:JbootRedisCacheImpl 初始化的错误提示内容 +优化:ClassUtil.newInstance() 方法,方便对构造器进行传参 +优化:删除无用的 GenTester.java 文件 +优化:不再对 JbootSerializerManager 进行 Aop 增强 +优化:不再对 JbootmqManager 进行 Aop 增强 +优化:不再对 JbootEventManager 进行 Aop 增强 + + + +jboot v4.0.2 2022-12-16: +新增:db.each 的 sql 打印输出 +新增:通过 Controller 获取参数时,自动对参数进行 trim 操作 +新增:配置文件可以外部的任意目录 +优化:升级 jfinal 到最新版本 + + + +jboot v4.0.1 2022-12-03: +新增:新增可以通过配置取消 AOP 缓存的功能 +新增:新增 jboot.cache.useFirstLevelOnly 配置的功能,可以在分布式下只开启 1 级缓存 +优化:修改 aop action 等默认缓存时间为 10 分钟 +优化:ModelUtil.keep 方法 +优化:ObjectUti.convert 方法 +修复:new Model().use("ds").update() 在某些场景下出错的问题 + + + +jboot v4.0.0 2022-11-27: +优化:正式支持 jdk 17.x + + +===================== v3.x ========================= + +jboot v3.17.1 2022-11-13: +优化:Columns.java 对空条件的判断 +优化:ClassScanner.java 支持扫描 jar 包里的 jar 包,感谢 @陈立刚 +优化:Redis 消息队列设置支持多个 database,感谢 @陈立刚 + + + +jboot v3.17.0 2022-11-12: +新增:自定义 Controller 缓存刷新 key 的功能 +优化:Jboot MQ 在启动和停止的时候,不对已启动进行错误抛出,方便多模块可以自由启动或停止 +优化:Jboot MQ 添加监听器的时候,自动添加 channel 信息 +优化:升级 jackson-core 等到最新版本 + + + +jboot v3.16.8 2022-11-06: +新增:Controller.getFilesOnly(Set paraNames) 方法 +优化:默认关闭缓存日志 + + + +jboot v3.16.7 2022-10-31: +新增:添加 CachePrinter 的支持,方便把缓存信息输出在控制台或者日志上 +优化:JbootResourceLoader 优化过滤掉 Windows 的临时文件 +优化:使用注解验证数据时,控制台不再抛出异常信息,只打印基本验证错误信息 +优化:为验证码等组件缓存过滤掉线程前缀的设置 + + + +jboot v3.16.6 2022-10-25: +修复:HttpUtil.download 出现 NPE 的问题 + + + +jboot v3.16.5 2022-10-23: +修复:Controller 使用返回值渲染并使用 cacheable 注解对其缓存时出错的问题 +修复:ObjectUtil.convert() 在某些极端情况下错误的问题 + + + +jboot v3.16.4 2022-10-19: +优化:doNotAlloVisitRedirect 错别字 +优化:修改 CacheUtil.setCurrentPrefix() 方法为 setThreadCacheNamePrefix +新增:添加 JbootCache.addThreadCacheNamePrefixIngore() 方法 + + + +jboot v3.16.3 2022-10-17: +新增:SqlBuilder.escapeOrderBySql 用于过滤 Order By 参数 +新增:InterceptorBuilder.Util.isChildClassOf 方法 +新增:JbootController.getParaToBigInteger(index) 方法 +优化:RequestUtil.getIpAddress() 对 127.0.0.1 的处理 +优化:JbootResourceLoader 的日志输出和目标路径的优化 + + + +jboot v3.16.2 2022-10-07: +新增:ObjectUtil.obtainNotNull 方法 +新增:StrUtil.obtainNotBlank 方法 +优化:升级 jfinal-undertow 到最新版本并移除 JbootApplication 的 URL 打印功功能。 + + + +jboot v3.16.1: +新增:APP 在启动的时候,输出 Local URL,方便通过控制台启动浏览器 +新增:FileUtil.ensuresParentExists() 方法 +新增:DAO.deleteAll() 方法 +修复:Controller 通过 return 渲染 Json 数据时,控制台不输出 Render 信息的问题 +修复:JbootController.getFileOnly(name) 返回的数据不正确 + + + +jboot v3.16.0: +优化:重构 @Cacheable 在 Controller 中的使用,使之更加灵活方便 +优化:FileUtil.unzip 方法,添加可以指定解压缩编码的参数 +优化:升级 JFinal、 jackson-core 等到最新版本 +修复:JbootServiceBase.initDao 在非泛型子类时初始化出错的问题 +修复:Sqlbuilder 在使用别名 + between 时,生成的 SQL 出错的问题 + + + +jboot v3.15.10: +优化:增强 http 工具类,默认支持携带 cookie 重定向 +优化:升级 Jsoup 到 v1.15.3 最新版本 +修复:对在中文目录下部署时,可能对环境判断错误的问题 + + + +jboot v3.15.9: +新增:Controller.getFileOnly(name) 和 getFilesOnly(names) +优化:升级 Jsoup 等相关依赖到最新版本 +修复:当 ActionKey 注解使用 ./ 相对路径时,swagger文档中的 path 会生成包含 ./ 的错误API路径的问题 + + + +jboot v3.15.8: +新增:FileUtil.delete 方法 +新增:JbootController.getFirstFileOnly() 方法 +优化:JbootController.getFile() 设置为删除方法 +优化:升级 JFinal 到最新版本 + + + +jboot v3.15.7: +修复:RPC 的 Reference 缓存 key 构建错误导致无法命中缓存的问题 +新增:FileUtil.removeSuffix() 方法 +新增:ModelUtil.keep(List models) 方法 +新增:CollectionUtil.toString() 方法 +新增:ArrayUtil.toString() 方法 +新增:DateUtil 解析 datetime-local 方法 + + + +jboot v3.15.6: +新增:JbootmqBase 添加自定义线程池的接口方法 +新增:JbootModel.processColumns() 方法,在某些场景下用于对 Columns 进行二次加工 +优化:Redis MQ 的 lpush 对应应该是 rpop 的问题,同时添加 interval 设置方法 +优化:JbootPaginateDirective 添加自动从 scope 获取 Page 对象的方法 +优化:移除 JbootModel 的 getBigInteger 和 getBigDecimal 方法,新版本的 JFinal 已经实现了 +修复:修复 Model.use("datasource").save() 的时候无法正确保存数据的问题 + + + +jboot v3.15.5: +修复:当开启 nacos 配置中心,有启用带参数内容 ${} 时,出现 StackOverFlow 的问题 +修复:HttpUtil 获取 GBK 网址内容时,出现乱码的问题 + + + +jboot v3.15.4: +优化:升级适配 Seata 到最新版本 1.5.1,感谢 @扫地猿 +优化:CookieUtil,方便在 Handler 里对 Cookie 进行操作 +文档:新增 dubbo2 升级到 dubbo3 的升级文档,感谢 @自由领主 + + + +jboot v3.15.3: +新增:代码生成器新增 白名单 配置的支持,感谢 @xujianxie +优化:升级 FastJson 到最新版本 + + + +jboot v3.15.2: +修复:DAO 配置了 loadColumns 时又配置了 distinct 时,无法获得正确结果的问题 +优化:优化 Columns.toString() 方法,防止在开发工具 debug 下显示 "null" 的问题 + + + +jboot v3.15.1: +新增:JbootOutputDirectiveFactory,用于忽略在生产环境下,模板引擎执行的错误输出 +新增:CodeGenHelpler 新增对 infomix 数据库的支持,感谢 @xujianxie +优化:升级 JFinal 到 v5.0 最新版本,其他相关依赖也升级到新版本 + + + +jboot v3.15.0: +优化:ValidErrorRender 使用 Ret.fail() 来创建,方便统一定义 message 字段 +优化:InterceptorBuilderManager 默认对所有 Interceptor 进行注入 +优化:Controller 新增 getAttrs() 和 renderToStringWithAttrs() 方法 +修复:Controller 接收 int[] 时,使用 @Size 等注解验证时出错的问题 +修复:Model.dao() 查询在某些情况下出错的问题 +修复:注解 @Size(max=xxx) 拦截错误的问题 + + + +jboot v3.14.9: +修复:在某些极端场景下,StrUtil.escapeHtml 无法通过 unEscapeHtml 还原的问题 +优化:删除某些无用的方法和类 + + + +jboot v3.14.8: +修复:DataSourceConfigManager 里的数据源在 APP 启动成功后会被清空的问题 +修复:配置 redis GlobalKeyPrefix,CacheUtil.getKeys(cacheName) 无法正确获取 keys 的问题 + + + +jboot v3.14.7: +修复:Jboot AppListener 启动时的错误信息,无法正确输出的问题 + + + +jboot v3.14.6: +修复:JbootLockCounter 和 JbootLocalLock 不可用的问题 +优化:删除一些无用的类或者空实现的类 + + + +jboot v3.14.5: +新增:JsonUtil.getList 和 JsonUtil.getSet 方法 +新增:RSAUtil 非对称加解密工具类 +新增:DESUtil 对称加解密工具类 + + + +jboot v3.14.4: +新增:生产环境忽略模板指令渲染错误的功能,保证其他内容正常渲染 +新增:模板错误渲染器 TemplateErrorRender,用于追加模板指令错误内容 +新增:是否开启 Controller Action 缓存的开关,方便在不同的场景下进行开启或者关闭 +新增:JbootEventManager 可以设置自己的线程池 +优化:ValidUtil 中ValidatorFactory初始化一次,供Validator、MessageInterpolator复用,感谢 @wtusrss +优化:升级 JFinal/JFinal-Undertow 等到最新版本 +修复:HttpUtil 在 POST 的时候,添加了参数又设置 body 内容时,参数失效的问题 +文档:修改文档错别字 + + + +jboot v3.14.3: +优化:JbootModel.findByColumn 当传入 null 值 value 时,直接返回 null +优化:Model 保存和之前版本一致的行为,只允许绑定到一个数据源 +优化:当 Model 配置指定数据源,但数据源不存在的时候,给出更为明确的错误提示 +优化:Rabbitmq 添加自由开启队列和广播的开关,默认全部开启 +优化:重构 CORS 模块,使之代码可以用户模块里进行 "复用" +修复: HttpUtil 在 put 请求时,某些情况下无法正常提交数据的问题 + + + +jboot v3.14.2: +新增:Informix 数据库方言,感谢 @xujianxie 同学 +新增:JbootModel 新增 findByIdWithoutCache() 方法 +新增:新增 JbootModel 的 CPI 类,用于开放保护方法 +新增:Nacos 远程配置中心添加多实例的配置支持 +新增:网关拦截器 GatewayConfig.interceptors 添加 SPI 名称配置的支持 +新增:JsonUtil.java 方便解析 Json 数据 +优化:ValidUtil.java,支持在非自定义消息的时候输出拦截的列名 +优化:ClassScanner.java 默认关闭 Class 扫描信息在控制台输出 +优化:修改缓存拦截器的默认权重为 100,当前情况下拦截器在最后执行 + + + +jboot v3.14.1: +新增:注解 @Table(datasource="xxx") 新增对数据源的配置支持 +新增:添加 jboo.app.listenerPackage 配置支持,用于只扫描哪些包的 listener +优化:完善 JbootJson 更多的代码注释 +优化:优化 JbootConfigManager.java 代码,对添加 key 进行 trim() +优化:进一步完善 jboot.properties 文件存放位置的探测 +优化:单元测试移除 mockHandler +优化:DAO.findFirstByColumn(),当传入 null 应抛出错误,防止查询出错误的数据 +优化:rabbitmq.queueDeclareDurable 的配置默认设置为 false +优化:对 QuietlyUtil 的方法进行重命名优化 +优化:ValidErrorRender 输出的 message 不带有 fieldName 信息 +优化:升级 Jfinal 等依赖到最新版本 +修复:@Cacheput() 注解在 Controller 上无效的问题 + + + +jboot v3.14.0: +新增:@Cacheable() 等系列缓存注解对 controller 的支持 +优化:优化 fastjson 序列化的功能,使用 config 而非 features +优化:重命名注解 @TxEnable() 修改为 @Transactional(),并为 @Transactional() 添加更多的配置功能 +优化:为 RabbitMQ 添加更多的自定义配置 +优化:自动检测是否依赖 jfinal-wexin 并自动配置 JbootAccessTokenCache +优化:删除 JacksonSerializer 等无用的代码文件 +优化:优化配置文件的的自动探测功能,防止在某些新手未编译直接运行找不到配置文件的问题 +优化:分布式任务注解 @EnableDistributedRunnable,并添加自定义的 redisKey 和 key 持有时间配置功能。 +文档:完善关于 Controller 和 数据库的相关文档 + + + +jboot v3.13.8: +修复:Junit 代码覆盖率测试可能出现多次启动的问题 +修复:JbootCron4jPlugin 停止后未移除已经停止任务的问题 + + + +jboot v3.13.7: +新增:MQ 新增 stopListening() 方法,可用于定制化关闭 MQ +修复: Controller 返回值有大写可能不正确的问题 + + + +jboot v3.13.6: +优化:强化 Controller 返回值,自动匹配相应的 render +优化:优化 JbootHttpImpl 和 JbootHttpResponse 代码,添加必要的日志输出 +优化:更新 Copyright + + + +jboot v3.13.5: +新增:CookieUtil 添加 "defaultPath" 和 "defaultDomain" 的配置 +新增:HttpUtil 添加 http 代理的配置支持 +新增:Columns.addToFirst() 方法 +新增:JbootDirectiveBase.getParaToString() +优化:RequestUtil.java +优化:升级 JFinal 到最新版本 + + + +jboot v3.13.4: +新增:AttachmentManager 新增 getFile(path,localFirst) 方法 +优化:升级相关依赖到最新版本 + + + +jboot v3.13.3: +新增:配置文件可以指定自己的文件名和路径的支持 +优化:JbootmqBase,使之代码更加简洁 +优化:Mysql 驱动,默认优先使用 MySql8 驱动 +优化:升级相关依赖到最新版本 + + + +jboot v3.13.2: +新增:AOP 新增 javassist 的实现,方便在 jdk17 去掉 cglib +修复:在配置前缀的场景下,修复 redis 缓存 removeAll() 无法正确移除数据的问题 +修复:SharedEnumObject 在 jdk17 下无法正常运行的问题 +修复:Fastjson 对某些数据无法正确序列化的问题,升级 fastjson 到最新版本 + + + +jboot v3.13.1: +新增:JbootDb 新增支持 Columns 的 findFirst 方法 +新增:QuietlyUtil.java 工具类 +新增:ReflectUtil.java 的若干方法,反射调用更加简单 +优化:Redis 缓存,存入 null 时,直接对 key 进行移除 +优化:MockHttpServletRequest 直接实现 HttpServletRequest 接口,而非 HttpServletRequestWrapper 继承 +优化:@TestConfig() 注解新增 printScannerInfo 参数,默认为 false +优化:MockMvcResult.java 提高性能 +优化:优化网关代码,使代码更加清晰 +修复:移除 JbootModelConfig.idCacheCachePrefix 配置,否则会造成重复配置的情况 + + + +jboot v3.13.0: +新增:Jbootmq 的监听器(Listener)新增 MessageContext 参数,支持更多的消息配置 +新增:MockMvc 新增 upload 方法,用于对上传文件的测试 +优化:404 403 401 等错误输出 info 信息,而不是 warn +优化:开放更多的 JbootMqImpl 方法 +优化:优化 Http 工具类,上传文件的时候添加 ContentType 信息 + + + +jboot v3.12.5: +新增:IdCache 默认的缓存前缀配置 +优化:AOP 缓存添加默认时间为 1 个小时 +优化:为 @Cacheable() 等注解添的key构建加更多的数据类型支持,支持集合和数组 +优化:升级相关依赖到最新版本 + + + +jboot v3.12.4: +新增:@FilterBy() 注解,用于对某些参数进行内容过滤 + + + +jboot v3.12.2: +新增:是否处理 404 页面的配置支持,方便与其他 web 框架整合 +新增:JbootAppListener 新增 onConstantConfigBefore() 方法 +优化:AttachmentManager.java,使之 getFile 不返回 null +优化:JsonManager.me().setDefaultDatePattern 的默认配置 + + + +jboot v3.12.1: +优化:当 cglib 创建对象出错时,错误信息不明确的问题 +优化:Cache、MQ、Redis 的 Manager 在某些情况下可能出现 NPE 的问题 +优化:升级相关依赖到最新版本 + + + +jboot v3.12.0: +新增:SPI 新增直接配置类名的功能 +新增:MQ 新增支持多个实例配置的支持 +新增:Redis 新增配置多个实例的支持 +新增:Cache 缓存新增配置多个实例的支持 +新增:限流组件新增 IP 限流功能 +新增:限流组件新增 IP 白名单功能 +优化:Rokect MQ 新增更多的配置支持 +优化:移动 AOP 的默认配置到 "jboot.aop.cache" +优化:"jboot.model.idCacheType" 配置名称修改为 "jboot.model.idCacheName" +优化:"jboot.web.session.cacheType" 配置名称修改为 "jboot.web.session.useCacheName" +优化:重命名 JbootConfigUtil 为 ConfigUtil +文档:修改文档错误已经添加部分文档 + + + +jboot v3.11.4: +新增:新增 DAO.distinct() 方法,用于对内容进行去重 +新增:MockMvc 新增 holdCookie 配置,用于在不同的测试用例里,保持 cookie 的功能 +优化:保证 Model 和 Record 的 getBigInteger 的行为一致 +优化:移除 codegen 下的 PathKit 类,使用 CodeGenHelpler 代替 + + + +jboot v3.11.3: +新增:重写 Model 的 getBigInteger,防止转换异常 +新增:代码生成器 生成的 Model,如果字段是 BigInteger ,自动调用 getBigInteger 方法的功能 +优化:升级相关依赖到最新版本 + + + +jboot v3.11.2: +新增:Columns.containsName() 方法 +新增:SqlDebugger 新增对 Sql 输出到日志的实现方案,只要只有接口需要自己实现 +优化:捕获 ScheduledThreadPoolExecutor 所有业务异常,防止其意外终止调度 +优化:PaginateDirectiveBase,当其有数据的时候,显示分页内容 +优化:ClassScanner,新增某些排除对象 +优化:删除 CacheableInterceptor 一些不必要的 debug 信息 + + + +jboot v3.11.1: +新增:SqlDebugPrinter 新增是否打印 SQL 的独立开关 +新增:新增更多的 JbootApplication.createServer(...) 方法 +优化:升级JFinal、Jedis、Naocs、Dubbo 等到最新版本 +修复:AttachmentManager.use(name) 无法使用的问题 + + + +jboot v3.11.0: +新增:实验性新增 Restful 支持,默认关闭,可以通过 "jboot.web.pathVariableEnable=true" 开启,感谢 @没牙的小朋友 +新增:shiro 配置添加 filter 参数,自定义shiro相关的filter,感谢 @orangeJ +优化:在shiro拦截器初始化时处理对应方法的注解,取消对路由、actionKey的倚赖,感谢 @orangeJ +优化:修改 JbootActionMapping 拼写错误 +优化:AttachmentManager 新增多实例的支持 +优化:优化缓存前缀配置的方法名,否则可能会造成误解 +优化:同时修改 "jboot.web.escapeParas"配置为 "jboot.web.escapeParasEnable" +修复:修正现有swagger实现中,会忽略ActionKey注解中的URL的问题,感谢 @没牙的小朋友 + + + +jboot v3.10.8: +新增:JbootCache 新增通过前缀了使用 "多实例" 的支持 +新增:Model、Db 动态设置默认数据源的支持 +新增:DatasourceConfig 新增表前缀配置的支持 +修复:由于 RPC 添加 Reference 缓存后,造成 Controller 多个 Service 无法注入的问题 + + + +jboot v3.10.7: +新增:JbootHttpRequest 添加 sslContext 的配置,方便自定义 ssl +新增:JbootConfigManager 添加 setBootProperties 方法,方便用于添加启动配置 +新增:工具类 ReflectUtil 新增 searchFieldList() 方法 +新增:添加 TypeConverterFunc,用于处理前端传入枚举内容 +优化:JbootShiroInvokeListener,在 onInvokeBefore() 方法添加返回值 AuthorizeResult,更加方便整合 Jwt sso 等 +优化:升级 jedis/jsoup/jfinal/fastjson 等到最新版本 + + + +jboot v3.10.6: +修复:ClassScanner 添加排除的前缀时,如果有大写字母不生效的问题 +修复:当使用 @Api(collect={}) 时,子 Controller 路径错误的问题 + + + +jboot v3.10.5: +新增:@EnableLimit() 注解新增通过 redis 实现集群的限流的功能,感谢 @orangeJ +新增:为 JbootRedis 增加 eval 方法,用于执行 lua 脚本,感谢 @orangeJ +新增:redis 缓存增加 globalKeyPrefix 配置,解决多个应用共用一个 redis 实例时的 key 冲突问题,感谢 @orangeJ +修复:JbootResourceLoader 在 Maven 二级目录下启动,无法扫描上级资源文件的问题 +修复:Sentinel 文档错误的问题 + + + +jboot v3.10.4: +优化:升级 JFinal-Weixin 到最新版本 +优化:添加 JbootActionReporter.colorRenderEnable 的配置 +修复:通过在 JFinal Routes 配置的 Controller,APIDoc 无法正常生成的问题 + + + +jboot v3.10.3: +新增:门户网关 Gateway 新增基于 Nacos 的自动服务发现的功能 +新增:JbootHttpImpl 新增默认的 Content-Type 配置 +新增:JbootHttpRequest 新增 "instanceFollowRedirects" 配置 +优化:允许 void 的 Controller 方法生成 retRemarks +优化:添加 ApiOper.containerClass 配置,用于对 void 类型的 Controller 设置返回值 +优化:添加 ApiDocConfig.defaultContainerClass 配置,用于配置默认的 void Controller 返回值 +优化:对 HttpRequest 进行优化,保证 headers 和 paras 的顺序 +优化:删除 JbootGatewayHandler 默认添加的必要,修改为默认不添加此 Handler +修复:@ApiPara.require() 在方法上不起作用的问题 +修复:Jboot 升级 JFinal 到最新版本后,代码生成器的路径错误的问题 +示例:新增 Gateway 通过 Nacos 自动发现的 Demo 示例 +示例:新增 WebSocket 的 Demo 示例 + + + +jboot v3.10.2: +新增:注解 @ApiPara() 增加 require 参数的配置,感谢 @lijiahong +优化:Json 增加递归深度配置,修复多层级数据返回不完全问题,感谢 @lijiahong +优化:升级 JFinal、jackson、Dubbo 等到最新版本 +修复:JbootPaginateDirective 在默认情况下无法获取 page 的问题 +修复:Redis 连接有密码时,Redis 集群时未设置 maxAttempts 连接权限不上问题,感谢 @lijiahong + + + +jboot v3.10.1: +新增:ApiDoc 新增 allInOne 模式,方便把所有 api 生成到一个文档里 +新增:@Api() 注解新增 orderNo 的配置 +新增:可以通过 api-remarks.json 和 api-mock.json 为文档配置 json 输出 +新增 "ApiJsonGenerator",用于生成通过数据库生成 api-remarks.json 和 api-mock.json 文件 +新增:ApiDoc 文档新增自定义排序 Comparator 配置的支持 +新增:ApiDoc 新增自定义 ApiMockBuilder 的支持,用于构建任意 Model 的 Mock 数据 +新增:"@ApiResp" 注解,用于对 JFinal 通过 render 而非返回值的形式的支持 +新增:ApiDocument 等信息添加序列化的支持 + + + +jboot v3.10.0: +新增:apidoc 模块 +优化:JbootCaptchaCache 在某些情况下可能出现 NPE 的问题 +优化:升级 JFinal 到 v4.9.14 最新版本 + + + +jboot v3.9.20: +优化:由于 JFinal 紧急发布新版本,因此同步到 JFinal 最新版本 +修复:因为 ehcache 依赖默认并没有引入,将 session 的默认存储改为 caffeine。感谢 @orangeJ +修复:@RequestMapping() @Path() 无法配置动态参数的问题 + + + +jboot v3.9.19: +新增:@MockClass() 注解,用于多 AOP 的 Class 进行 Mock +新增:MockMvc 新增 requestStartListener 和 requestFinishedListener 的支持 +新增:@TestConfig.devMode 和 @TestConfig.launchArgs 配置的支持 +新增:MockMvcResult.getContentAsJSONObject() 和 assertJson() 方法 +新增:MockHttpServletRequest 新增 addQueryParameter() 方法 +新增:@DefaultValue() 注解,用与对参数配置默认值 +新增:"jboot.sentinel.reqeustTargetPrefix" 配置的支持,用于配置 sentinel 对某些 url 进行拦截 +优化:优化控制台 action 的日志输出的性能 +优化:升级 JFinal 到最新版本 +优化:通过 MockMvc 进行单元测试的时候,若 Controller 发生 404 或者 500 等错误的时候,则不通过 junit 测试 +文档:添加验证器错误自定义渲染的相关文档 +文档:同步 junit 测试文档 +文档:修复 Sentinel 的里错误的配置文档 + + + +jboot v3.9.18: +新增:新增 @MockMethod 注解,方便对 AOP 方法进行 Mock +新增:@TestConfig(autoMockInterface=false) 配置,方便对接口进行 Mock 操作 +修复:Motan RPC 框架的 protocol 配置不生效的问题 +修复:ide 配置错误时给出的 JFinal 配置帮助文档网址错误 + + +jboot v3.9.17: +修复:JbootActionReporter 可能出现 NotFoundException 的问题 +修复:JbootActionReporter 可能出现 NPE 的问题 +修复:阿里云商业 MQ Aliyunmq 配置错误的问题,感谢 @不器 +文档:修改某些描述错误的文档 + + + +jboot v3.9.16: +修复:Junit 测试对于个别 ServletRequest 方法没有 mock 到而出错的问题 +修复:当 Jboot 有上层 session 时(比如使用 shiro),修改 Controller session 无法同步上层 session 的问题 + + + +jboot v3.9.15: +新增:新增单元测试的辅助类的支持 +新增:ActionReporter 新增 render 信息的输出功能 +新增:工具类 ReflectUtil.java +优化:调整默认的JbootShiroInvokeListener实现,保存被拦截的请求便于后续跳转使用,感谢 @没牙的小朋友 +修复:jboot.properties配置文件中 jboot.shiro.ini 配置未生效的问题,感谢 @没牙的小朋友 +修复:在某些情况下 PathKit.getWebRootPath 得到错误结果的问题 + + + +jboot v3.9.14: +新增:为 undertow 新增默认的 content-type,解决 mp4 等视频不能播放的问题 +新增:ValidErrorRender,方便用户自定义 "数据验证" 错误的渲染器 +修复:当 Interceptor 被 cglib 代理时,无法正确输出其日志的问题 + + + +jboot v3.9.13: +新增:新增配置 "jboot.app.listener",用于配置可以执行的 appListener +新增:新增配置 "jboot.json.skipModelAttrs" 和 "jboot.json.skipBeanGetters" 配置 +优化:JbootGatewayHandler 默认添加在系统里,方便进行动态路由 +优化:JbootGatewayHealthChecker 的代码 +优化:升级 JFinal 等依赖到最新版本 +优化:对 ClickHouse 高级版本 驱动 进行适配 +修复:代码生成器在某些情况下输出的 html 不是 utf8 编码的问题 +修复:WeightUtil 判断错误的 bug + + + +jboot v3.9.12: +新增:devModel 可以动态配置,方便在某些场景下切换 devMode +修复:在某些极端场景下,PathKit.getWebRootPath() 可能出错的问题 +修复:当查询的 Page 为 null 时,分页的总页数数据错误的问题 + + + +jboot v3.9.11: +新增:注解 @ActionKey() 支持 ./ 相对路径的配置 +新增:门户网关拦截器新增对 Header 的配置 +新增:JbootActionReporter 的开关配置,而不是由 devMode 决定 +优化:门户网关的健康检查代码抽离为独立的类 JbootGatewayHealthChecker +优化:门户网关 NoneHealthUrlErrorRender 重构为 GatewayErrorRender,支持更多的错误渲染 +优化:门户网关可以通过拦截器自定义前端渲染功能 +优化:简化 RPC 的默认配置 +优化:优化 JWT 的代码逻辑 +优化:升级 JFinal、Seata 等到最新版本 +优化:优化 ConfigUtil 的参数解析方法 +优化:优化 FastJsonSerializer 的逻辑代码 +文档:优化序列化的相关文档 + + + +jboot v3.9.10: +新增:Dubbo Method 的 oninvoke/onreturn/onthrow 配置的支持 +新增:不同的 @RPCIject 持有不同 RPC 对象的支持 +优化:当 provider 未启动,但是开启 check 后无法注入,但提示信息不明确的问题 +优化:升级 jackson、dubbo 等到最新版本 +修复:ConfigUtil 在读取 Object 时可能出错的问题 +修复:RPCUtil 在配置 default 有时可能无效的问题 +修复:Dubbo 版本升级后,不再支持对 consumer 的 protocol 配置 +修复:Dubbo 升级到 2.7.10 后,若不配置直连协议,启动出错的问题 + + + +jboot v3.9.9: +新增:@TxEnable() 注解的支持,方便在 Service 进行事务处理 +优化:优化 JsonBodyParseInterceptor.java 代码 +优化:升级 JFinal、FastJson 等到最新版本 +修复:泛型的 Controller 无法覆盖子类 Action 的问题 [#I3FG0B](https://gitee.com/JbootProjects/jboot/issues/I3FG0B) +修复:使用 @JsonBody 注解,同时使用 泛型 的 Controller 在 openJdk 下可能出错的问题 +修复:paginateByColumns 传入复杂的 Order By 可能产生错误的问题 +文档:修复 Dubbo 文档 url 路径错误的问题 + + + +jboot v3.9.8: +新增:@GetMapping() 和 @PostMapping() 的支持 +新增:@JsonBody() 注解支持 LocalDate 和 LocalDateTime +新增:@JsonBody() 注解支持在 Class 定义泛型的功能 +优化:控制输出的 Action 时间执行时间,包含了模板引擎的渲染时间。 +优化:Jwt 拦截器构建器的代码 +优化:门户网关的 Http 代理代码 +优化:升级 Nacos 等依赖到最新版本 + + + +jboot v3.9.7: +新增:@Lazy 懒加载注入的功能 +新增:JbootController.getOrginalRequest() 方法,用于获取进过 Xss 处理后的原始 Request +新增:JbootDirectiveBase.getParaToBigDecimal() 的系列方法 +新增:Interceptors.addIfNotExist() 方法,用于对某些注解进行单次添加 +新增:JbootController.getParaToBigDecimal() 和 getParaToBigInteger() 方法 +优化:重命名 JFinalEnumObject 为 SharedEnumObject +优化:Validator 验证错误的时候,错误信息给出错误的相关字段 +修复:通过 @JFinalShareEnum 添加枚举,在某些极端情况下无法调用枚举静态方法的问题 + + + +jboot v3.9.6: +新增:门户网关动态配置拦截器的功能(之前只能通过配置文件进行配置) +新增:门户网关动态配置负载均衡策略的功能(之前只能通过配置文件进行配置) +新增:columns 新增 groupBy() 和 having() 的方法,方便构建 group by 的 SQL +新增:新增 jboot.web.escapeParas 配置,方便全局对 xss 进行防护 +优化:升级 JFinal、HikariCP、metrics、Shiro 等到最新版本 +修复:使用 @Bean 注解,然后 Jboot.getBean() 通过 Bean Name 获取不到对象的问题 +修复:Redis 缓存的 removeAll() 和 getKeys() 在某些情况返回数据不正确的问题 + + + +jboot v3.9.5: +新增:CacheInterceptorBuilder.Util 工具类,用于对拦截器的判断 +优化:重构 @Configuration 对 @Bean 的初始化工作 +优化:JbootCglibCallback 对没有拦截器的方法的调用 +修复:在热加载的情况下,JbootCoreConfig 无法转换为 JFinalConfig 的问题 + + + +jboot v3.9.4: +新增:@JfinalSharedEnum 注解的支持,方便把枚举添加到模板引擎里使用 +新增:@PostConstruct 注解的支持,方便 Bean 在被创建的时候进行初始化 +优化:ApplicationUtil.runInFatjar 的判断 +文档:更新 MVC、MQ、Gateway 等文档 + + + +jboot v3.9.3: +新增:门户网关没有健康网关时可以自定义渲染器的功能 +优化:Gateway 门户网关的性能 +优化:GatewayInterceptpor 拦截器可以获得当前的代理目标 URL 地址功能 +修复:@DecimalMax 和 @DecimalMin 在某些极端情况下可能验证错误的问题 +文档:更新 MQ、Sentinel、Gateway、Validator 等文档 + + + +jboot v3.9.2: +新增:门户网关 Gateway 的健康检查功能 +新增:JbootModel.closeIdCacheTemporary() 通过这个方法可以一次关闭ID缓存 +新增:Controller 新增 getJwtParaToInt 等系列方法 +新增:JbootHttpRequest 新增 readBody 配置,用于在某些情况下不读取 http body +优化:StrUtil.queryStringToMap 方法 +优化:优化门户网关的若干方法,添加必要的注释 +优化:升级 JFinal、Sentinel、Seata 等到最新版本 +修复:修复 clickhouse 在某些情况下,分页可能出错的问题 + + + +jboot v3.9.1: +修复:JbootModel.loadCache 在某些情况下出错的问题 +修复:分布式配置启用 Apollo 后可能出现空指针的问题 + + + +jboot v3.9.0: +新增:Clickhouse 数据源的配置和支持 +新增:Rocketmq 作为 mq 底层通信的支持 +新增:ValidUtil.setValidator() 方法用于自定义自己的 Validator +新增:ValidateInterceptorUtil.setValidExceptionRetBuilder 用于渲染自定义验证错误信息 +新增:jboot.model.idCacheByCopyEnable 配置用于开启 id 缓存的时候是否返回 copy 的对象 +优化:优化 Rabbitmq 和 阿里云商业 MQ 的支持 +优化:修改 Validate 返回数据的 errorCode 的值为 400 +优化:重命名 ControllerUtil 为 JbootShiroUtil +优化:JbootController 去获取 Jwt 内容的时候,不配置 secret 只会发出警告而不是抛出异常 +优化:重置 Rabbitmq 的 broadcastQueuePrefix 配置为 broadcastChannelPrefix + + + +jboot v3.8.1: +优化:增强 Sentinel 与其控制面板设置的能力,增加数据源配置,并可以通过阿里云 AHAS 进行完全控制 +优化:升级 Undertow 的 devMode 默认值为 false,防止在 devMode 的情况下,不同的 classloader 导致获取不到数据查询实例的问题 +修复:修复分布式 Seata 的 bug 、升级 Seata 到最新版本并添加相关测试代码,感谢 @菜农 +修复:修复 Rabbitmq 在某些情况下出现 bug 的问题,并添加相关测试代码 + + + +jboot v3.8.0: +新增:代码生成器生成 Service 的前缀和后缀配置 +优化:升级 Jfinal、Jfinal-undertow、Jfinal-weixin 等到最新版本 +优化:AopFactory 优化对 @Configration 的空注解的构建 +优化:完全重构 Metrics,添加对 Prometheus 的输出 + + + +jboot v3.7.8: +修复:修复 v3.7.7 的 Aop.get(Interface.class) 在某些情况下无法获取服务的问题 + + + +jboot v3.7.7: +优化:优化 @Configuration 注解初始化流程 +优化:AOP 对接口或者抽象类进行注入,但有找不到其实现类的时候可能出现 methodNoFund 的错误 +修复:通过 @Bean(name=xxx) 注解去定义 Service 时在某些情况下可能无法正常获取的问题 + + + +jboot v3.7.6: +新增:新增默认的 DriverClassNames,当用户不配置的时候使用默认的进行配置 +新增:JbootController.getJsonBody() 的方法 +优化:默认添加 validation-api 验证框架依赖 +优化:DateUtil 当前端传入 null 值的时候有适合的返回值 +优化:jwt 拦截器提高性能,并新增清空 jwt 数据的方法 +修复:JbootController.toBigDecimal 当已 N 开头时解析不对的问题 + + + +jboot v3.7.5: +新增:支持更多的验证注解,比如 Digits DecimalMax DecimalMin Positive Negative 等 +新增:支持在 Service 任何被注入的方法里进行验证 +新增:JbootApplication 运行下 fatjar 的时候,支持在同级目录下读取 jboot.properties 文件 + + + +jboot v3.7.4: +新增:NotNull、NotBlank、NotEmpty、Valid、Pattern、Min、Max、Size、Email 等验证注解 +新增:RequestUtil.isJsonContentType() 方法 +优化:NotEmpty、Regex 等验证返回更加当前请求类型返回 json 内容 +优化:JbootErrorRender,当发生错误的时候,对 ajax 请求返回 json 内容 +优化:移除注解 Weight 的默认值 +修复:JbootController.getRawObject 在某些情况下可能出现异常的问题 +修复:使用 Autoload 注解,同时配置 Weight 不生效的问题 + + + +jboot v3.7.3: +新增:JbootController 新增 getRawObject(TypeDef) 方法,可以获得指定泛型数据 +优化:JsonBodyParseInterceptor 的解析效率 +优化:JbootController 的 getRawObject 的效率 +修复:当最后一个节点是数组而去获取指定 index 的 Object 时,返回 null 的问题 + + + +jboot v3.7.2: +新增:@JsonBody 新增新的 JsonKey 语法支持 +新增:JbootController 新增传入 JsonKey 获取数据 + + + +jboot v3.7.1: +新增:DateUtil 新增 getStartOfDay() 和 getEndOfDay() 两个方法 +新增:@JsonBody 支持多 Date 类型的注入 +优化:DateUtil 支持更多的自动 parse 方法 +修复:@JsonBody 对原始数据类型的注入为 null 是不正确的问题 + + + +jboot v3.7.0: +优化:提高在 Controller 有多个 @JsonBody 参数时 JsonBodyParseInterceptor 解析性能 +修复:JsonBodyParseInterceptor 在解析原始数据参数出错的问题 +修复:JbootJedisImpl 在某些情况下配置 database 无效的问题 + + + +jboot v3.6.9: +新增:@JsonBody 新增对 set 的支持 +优化:升级 JFinal、Undertow、Jackson 等到最新版本 + + + +jboot v3.6.8: +新增:Controller 参数新增 @JsonBody 的支持 +新增:Http 工具模块添加更多的配置,方便在 fatjar 模式下能配置 https 相关证书 +新增:ErrorRender 自动判断前端是否需要 json 渲染,当请求头是 application/json 的时候自动渲染错误的 json + + + +jboot v3.6.7: +优化:JbootSimpleApplication,使之代码更加简洁 +优化:优化缓存拦截器对方法的 key 进行构建,提高性能 +优化:默认为 JbootAccessTokenCache 添加 2 个小时的缓存时间 +优化:LocalAttachmentContainer,默认保存文件的时候,对文件进行相同文件验证 +优化:JwtManager 对 Jwt 解析出错或没有 Jwt 数据时,返回常量 map + + + +jboot v3.6.6: +新增:AttachmentManager 新增保存文件的若干方法 +修复:DateUtil 某些方法不正确的问题 + + + +jboot v3.6.5: +新增:控制台信息打印新增 RawData 的内容打印,方便开发调试 +优化:优化通过 JbootSimpleApplication 启动 RPC 服务时控制台的输出信息 +修复:Jboot 在某些特殊场景下对 fatjar 运行模式判断不正确的 bug +修复:通过 JbootSimpleApplication 启动时,Pathkit 无法正确获取路径的问题 + + + +jboot v3.6.4: +修复:DateUtil 的 bug,和新增若干方法 +优化:StrUtil.mapToQueryString 的方法 + + + +jboot v3.6.3: +新增:DateUtil 工具类,方便对日期进行计算或转换 +新增:StrUtil.queryStringToMap 和 StrUtil.mapToQueryString +优化:JbootCaptchaCache 进行缓存 Captcha 时,配置过期时间 +修复:CORSInterceptorBuilder 构建跨域注解拦截器时,注解在类上获取错误 + + + +jboot v3.6.2: +新增:控制台新增 Jwt 参数内容的打印,方便调试开发 +新增:jboot.app.resourceLoaderEnable 配置,在 dev 模块下可以关闭 resourceLoader +优化:重命名 JbootInvocation 为 JbootActionInvocation + + + +jboot v3.6.1: +新增:JbootCaptchaCache 方便在分布式的场景下进行验证码验证 +新增:JbootTokenCache 方便在分布式下进行 token 验证 +新增:JbootLock,方便在本地模式下分布式模型下进行锁机制编写 +优化:RequestMapping 注解添加空字符串 "" 配置的支持 +优化:移动 JbootAccessTokenCache 的包名 +优化:重构 JbootCounter,使其在单体模型下和分布式场景下有一致的特征 + + + +jboot v3.6.0: +新增:ClassUtil.hasClass 方法,用于判断一些第三方组件依赖 +新增:Model 新增 @JsonIgnore 注解,用于配置某些不输出的字段 +新增:Model 新增 FastJson 的注解 @JsonField 的配置支持 +新增:新增对 JFinal 最新版本 @Path 的配置支持 +新增:DAO 查询的时候,支持通过 loadColumns() 方法配置其查询的列 +修复:Columns组装sql工具类in()、notIn()方法新增安全模式检查参数是否为空,解决安全模式下,List不为null,但没有元素时跳过安全检查。感谢 @liuenxin +优化:Columns.in() 和 Columns.notIn 修改参数 List 为 Collection +优化:升级 JFinal、Fastjson、Nacos 等到最新版本 +优化:为 Json 的相关代码创建独立包 +优化:重构 JbootJson 使之代码更加简洁 +优化:重命名 SqlDebugger.debug 为 SqlDebugger.run,删除其不必要的方法 +优化:优化 AopCache,使之在缓存数据错误时,自动刷新缓存信息 +文档:更新 json 的相关文档 + + + +jboot v3.5.9: +新增:StrUtil.obtainDefault 方法用于替代 obtainDefaultIfBlank 方法 +新增:jboot.web.cookieMaxAge 的配置支持,用于配置默认的 cookie 保存时间 +优化:提升配置参数 ${xxx} 解析性能 +优化:JbootController 的代码注释错误 +优化:ClassScanner +文档:优化 motan rpc 的构建文档,增加必要的说明,感谢 @zcoder +文档:新增 Nacos 分布式配置下的一些文档 + + + +jboot v3.5.8: +新增:StrUti.obtainDefault 替代 obtainDefaultIfBlank +优化:JbootAopFactory,新增 @Bean 注解对非接口类的支持 +优化:ApolloConfigManager 对 Apollo 配置的管理 +修复:修复 Redis 缓存 removeAll 在某些情况下错误的问题 + + + +jboot v3.5.7: +新增:JbootRedis 新增 scan 方法 +优化:JbootRedisCache 的 getKeys removaAll 等方法使用 scan 去读取 keys +优化:重构 Jwt 模块,使之代码更加简洁 + + + +jboot v3.5.6: +新增:Jboot 通过 columns 查询新增设置主表别名的支持 +优化:重构 SqlDebugger,控制台实时输出 Sql 的执行时间,方便对 Sql 进行优化 +优化:SwaggerController,兼容请求地址结尾没斜杠json加载失败问题,感谢 @xiaoyu512 +优化:优化 JbootRpcBase onStart 方法,减少子类复写时调用不必要的方法 +修复:高并发下,第一次去获取 RPC 服务时,可能为 null 的问题,感谢 @huangzekai_1 +文档:优化 json 相关文档注释不明确的问题 + + + +jboot v3.5.5: +新增:JbootCaptchaRender 渲染,可以自定义动态码的内容 +新增:AopCache 新增 setAopCache 方法,方便通过代码配置 Aop 缓存 +优化:重构 JWT 模块,使之在调整 Jwt 拦截器顺序时也可以正常工作 +优化:移除 JbootAopInvocation,使代码更加简洁 +优化:Utils.putDataToCache 方法,使其更加便于阅读 +优化:优化 Redis 订阅模块,使其在应用关闭时主动断开连接 +优化:升级 fastjson、druid 等到最新版本 + + + +jboot v3.5.4: +优化:使用 InterceptorBuilder 重构 Sentinel 模块,使代码更加简洁 +优化:使用 InterceptorBuilder 重构 Seata Tcc 模块,使代码更加简洁 +优化:Interceptors 可以通过 Class 直接添加,而无需添加具体的实例 +优化:更新 JFinal、JFinal-undertow 等到最新版本 +优化:移动 MixedByteArrayOutputStream 所在的包目录 +修复:解决CGLIB代理下获取不到 @Weight 注解问题,感谢 @huangzekai_1 + + + +jboot v3.5.3: +优化:CaffeineCacheBuilder 构建器可能根据名称进行构建 +优化:提高 CDN 渲染的构建性能 +修复: Caffeine Cache 在某些极端情况下可能存在多份 Cache 的问题,感谢波总 @JFinal +修复:当 Render 发生错误时,html 页面无法再次渲染 500 错误页面或者异常信息的问题 + + + +jboot v3.5.2: +优化:InterceptorBuilderManager,添加移除 Builder 等方法及其相关测试 +优化:ClassUtil,完善 singleton 等方法 +优化:Jboot 缓存默认类型 由 ehcache 修改为 caffeine +优化:优化启动输出内容 和 sql 打印内容 +优化:JbootRender,当不启用 CDN 的时候进一步提升性能 +修复:AttachmentManager,当分布式文件不存在时,访问文件出现空指针的问题 + + + +jboot v3.5.1: +优化:JbootRedisCacheImpl buildKey() 方法 +优化:JbootJson 并新增更多的配置 +优化:InterceptorBuilderManager 的方法名并添加更多可配置的方法 + + + +jboot v3.5.0: +新增:InterceptorBuilder 组件,方便对 Controller 或者 AOP 对象的拦截器进行构建 +优化:移除 FixedInterceptor 组件,其可以通过 InterceptorBuilder 进行替代 +优化:新增 Controller 方法是否被正常执行的 log +优化:优化 AttachmentManager 对分布式文件的渲染流程 + + + +jboot v3.4.3: +新增:指令 @JFinalDirective 新增 override 配置,用于覆盖系统已经内置的指令 +优化:优化 AttachmentManager,使之更加方便的上传 获取文件 + + + +jboot v3.4.2: +修复:当使用 Apollo 配置中心时,在某些场景下会导致 devMode 判断不正确的问题 +优化:重构 PaginateDirectiveBase,使之显示更加 "人性化" 和支持更多的功能配置 + + + +jboot v3.4.1: +修复:在 fatjar 模式下,通过 --jboot.app.mode 启动参数配置应用模式无效的问题 +修复:在 fatjar 模式下,不同位置的 java -jar 启动可能会导致 ClassScanner 两次扫描 jar 拖慢启动速度的问题 +修复:paginate 分页方法无法正确输出 sql 的问题 + + + +jboot v3.4.0: +新增:门户网关 Gateway 新增自定义负载均衡策略的支持 +新增:AttachmentContainer 组件,方便自定义把附件上传到其他第三方任何平台 +新增:全新的文档地址 和 Jboot 官网 +修复:数据源 Datasource 的 validationQuery 属性配置不生效的问题 +修复:SqlDebugPrinter 对参数为 Boolean 数据输出的格式不正确的问题 +优化:升级 Sentinal、Metrics、JFinal-Weixin 等到最新版本 +优化:删除 JbootActionReporter 一些不必要的方法 +优化:删除一些不必要的 注释信息 +优化:修改 JbootPaginateDirective 自动去获取当前的 page 信息,而不需配置 +优化:移除 JbootHttpImpl 默认的 content-type 配置 +优化:重构 Metrics 读取的相关处理,在 Metrics 未配置的时候,没必要添加相关 Handler,提升性能 + + + +jboot v3.3.5: +修复:通过门户网关下载文件 或者 渲染图片可能出现乱码的问题 +优化:重构 Http 工具类里的 HttpRequest 里的某些方法 +优化:增强 JbootActionReporter 功能,使之可以输出未被执行的拦截器 以及 Controller 的执行时间 + + + +jboot v3.3.4: +修复:当 Action 定义在父类后,JbootActionReporter 获取不到 Method 而出错的问题 +优化:升级 nacos 和 Apollo 客户端到最新版本 +优化:JbootHttpImpl Post 提交数据资源可能存在不正常关闭的情况 + + + +jboot v3.3.3: +修复:JbootActionReporter 对拦截器 Interceptor 输出的行号不正确的问题 + + + +jboot v3.3.2: +新增:新增 JbootActionReporter 用于代替 JFinal 的 ActionReporter,更精准的地位方法 +优化:重命名 ParaValidateInterceptor 为 ValidateInterceptor +修复:@EmptyValidate @RegexValidate 的 message 在 ajax 上提示不正确的问题 + + + +jboot v3.3.1: +新增:Seata tcc 的支持 +新增:RegexValidate 注解对 Controller 进行验证的支持 +优化:ClassScanner 排除对 protobuf 扫描 +优化:JbootUndertowConfig 排除对 Jboot.java 的依赖 +优化:升级 JFinal、FastJson 等到最新版本 + + + +jboot v3.3.0: +新增:ObjectUtil 工具类,用于对 Object 进行对比等操作 +新增:JbootModel.useFirst() 方法,更加方便在读写分离的场景下进行使用 +新增:JsonTimestampPattern 配置,方便控制 json 的日期输出 +新增:JbootServiceBase.findListByIds() 方法 +新增:Columns 新增 safeCreate 方法 +优化:重构 JbootConfigChangeListener ,方便监听远程配置的每个值的变化 +优化:JbootServiceBase.syncModels 由传入 List 修改为 Collection +优化:升级 Seata 到最新版本 1.3.0 +优化:升级远程配置 nacos、Apollo 到最新版本 +优化:删除 JbootrpcManager 里的某些无用的逻辑判断 +修复:当有多数据源时,join 会参数数据不正确的问题 +修复:当主键是 String 类型时,DAO.findListByIds() 没有返回数据的问题 +修复:CookieUtil 当设置的时间为 0 或者 -1 的时候,时间验证不正确的问题 + + + +jboot v3.2.9: +修复:Columns.in() 对传入 int[] long[] short[] 不能正确支持的问题 + + + +jboot v3.2.8: +新增:Columns.toWherePartSql() 方便构建 sql +新增:ObjectFunc.java 方便通过 Java8 lambda 调用 +新增:JbootServiceJoiner.joinMany 方法,方便进行一对多的查询 +新增:JbootServiceJoiner.joinManyByTable 方法,方便通过第三映射表进行多对多查询 +新增:JbootServiceJoiner.syncModels 方法,用过同步数据到数据库 +修复:Columns.likeAppendPrecent 传入空数据时结果出错的问题 +修复:Seata 对 dubbo 的 SPI 过滤器文件名错误的问题 +优化:当 jwt 解析出错时,输出错误的日志信息 +优化:重命名 joinById() 为 joinByValue(),因为传入的值不一定只是 id +文档:新增 一多一、一对多、多对多查询的相关文档 + + + +jboot v3.2.7: +优化:修改 JbootModel.buildIdCacheKey 为 protected 修饰,方便在某些情况下进行重写。 +优化:Columns 在 safeMode 模式下,当传入 null 值时,直接抛出空指针异常,更加方便开发调试。 + + + +jboot v3.2.6: +修复:Columns 调用 unUseSafeMode 后可能存在问题的 bug + + + +jboot v3.2.5: +新增:Columns 查询添加 safeMode ,使用 safeMode 当传入的查询值 null 值的 sql 参数时,不对齐忽略直接返回空数据。 +优化:升级 fastjson、jackson Json 等到最新版本 +修复:JbootAppListener 无法进行正确注入的问题 + + + +jboot v3.2.4: +优化:ClassScanner 和 JbootJson +新增:为 JbootJson 新增 camelCaseToLowerCaseAnyway 配置,默认为 false + + + +jboot v3.2.3: +优化:升级 fastsjon 到最新版本 1.2.71 +修复:Model 的 getter 方法无法输出 json 的问题 + + + +jboot v3.2.2: +优化:当项目启动的时候,优先初始化中央配置,以防止 undertow 端口等无法在中央仓库配置的问题 +优化:新增 Columns.in(list) 和 Columns.notIn(list) 方法 +优化:新增 Columns.append() 方法用于追加一个新的 columns +优化:Json输出默认使用驼峰的字段风格 +优化:升级 Nacos、Fastjson 等到最新版本 +修复:通过 Columns 查询 count,当有 left join 时会出现数量不正确的问题 +修复:当 RPC 注解有 ${} 时,无法读取配置内容的问题 +文档:修改配置文件里的示例带有双引号的错误配置 +文档:优化 PRC 的相关文档 + + + +jboot v3.2.1: +优化:升级 JFinal-Undertow、JFinal-Weixin 到最新版本 +优化:完善支持更多关于 druid 的数据源配置 +优化:当未配置任何第三方日志组件的时候,自动使用 JDK 日志进行输出 +优化:添加 JbootRedirectRender,防止 nginx -> jboot 跳转时的错误问题 +优化:移除 @ValidatePara 注解 和 UrlParaValidate 验证拦截器 +优化:移除 Jboot 的 @EnableCORS 注解,使用 JFinal 自带的来替代 +优化:修改某些变量命名不直观的问题 +优化:默认情况下完全禁用 Fastjson 的 autoType 功能 +文档:添加 dubbo rpc 下的 restful 配置文档 + + + +jboot v3.2.0: +新增:JbootController 新增 getParaToBigInteger()、getParaToBigDecimal() 等方法 +新增:门户网关新增 hasException() 方法,用于判断目标地址是否可以正常访问 +优化:升级 JFinal、jackson、HikariCP、Dubbo 等相关依赖到最新版本 +文档:配置相关文档添加动态配置的相关描述 +文档:数据库配置相关添加多数据源的相关描述 + + + +jboot v3.1.9: +新增:Jboot.configValue(key,default) 方法 +新增:JbootAppListener.onStartFinish()方法,用于不同的 Module 在 onStart 进行操作。 +修复:当在配置文件中配置的内容为 '{' 字符开头的时候会出现 ArrayIndexOutOfBoundsException 异常的问题 +优化:升级 Seata 到 v1.2.0 最新版本 +优化:移除 rpc 服务暴露成功后的日志输出 +优化:统一 JbootCron4jPlugin.addTask() 中的 deamon 参数默认为 false +优化:重命名 app/config/Utils 为 ConfigUtil +优化:重构 Restructure AnnotationUtil.get(),以便支持更加灵活的参数配置 + + + +jboot v3.1.8: +新增:Gateway 新增动态注册路 和 移除由配置的功能 +新增:Gateway 被 Sentinel 拦截后自定义返回 Json 的功能 +新增:Gateway 新增对多个 host 的支持,默认走随机匹配的负载均衡机制 +新增:新增 Dubbo 下的对 consumer/provider/register/protocol 的默认配置的支持 +优化:修改 JbootServiceBase 的 DAO 属性类型为 JbootModel +优化:优化 JbootrpcBase 和 Prop 的一些输出日志 +优化:重命名 PRCUtils 为 PRCUtil,保持 Jboot 工具类统一 +优化:重命名 JbootRpcApplication 为 JbootSimpleApplication +优化:重构 DubboUtil 代码,使之更加简洁 +优化:使用JsonKit来替换 FastJson 的直接使用,解决开发场景中使用其它json库的情况下,不会出错,感谢 @ yangyao +优化:升级 Guava 等相关的 Maven 依赖到最新版本 +修复:修复 ClassScanner 扫描不到 Shiro 指令的问题 +修复:JbootGatewayManager 默认名字配置错误的问题 +修复:PRCUtil 无法添加某些注解注解属性导致@RPCInject某些参数无效的问题 +修复:启用分布式配置 Nacos 时,在 Nacos 配置的中文会出现乱码的问题 +修复:使用 Motan RPC 框架时,出现引用错误的问题 +修复:Dubbo 下的一些 consumer 配置失效的问题 +修复:修复 fatjar 打包的时候,需要单独配置 BaseTemplatePath,否则出错的问题 +文档:优化 gateway 的相关文档 +文档:优化 config配置 的相关文档 +文档:优化 rpc 的相关文档 + + + +jboot v3.1.7: +新增:JWT 可以通过 request para 传入数据的支持 +修复:复写 ActionHandler.getAction 可能无效的问题 +优化:排除 Jboot 内部不必要的 Class 扫描 + + + +jboot v3.1.6: +新增:JbootReturnValueRender,用于可以在 Action 里进行返回值渲染 +新增:ResponseEntity,可以直接在 Action 返回其进行渲染 +优化:对 JbootGatewayConfig 网关配置进行优化,用户配置了错误的 uri 时会给出提示 +优化:升级 Dubbo、Motan、Sentinel、Nacos 等到最新版本 +优化:ClassScanner 添加一些常用的排除 jar 和 class +优化:更多关于 Nacos 分布式配置中心的配置支持 +修复:RPCUtils.copyFields() 无法正确复制配置内容的问题 +修复:JbootRpcApplication 无法正确启动插件和拦截器的问题 +文档:修改 AOP 文档的内容错误问题 +文档:完善分布式配置中心对 Nacos 以及 Apollo 支持的相关文档 + + + +jboot v3.1.5: +新增:新增 Motan RPC 的 export 和 host 的相关配置以及test代码 +新增:JbootJson 支持驼峰式 JsonKey 输出,感谢Gitee的 @herowjun +新增:新增 Controller 对返回值自动渲染的功能 +新增:JbootRPCConfig 新增默认 version 和 group 配置的支持 +优化:ClassScanner 添加无需扫码的 jar 排除,速度更快 +修复:ClassScanner 在 Windows 平台下可能存在重复扫码而拖慢启动速度的问题 +修复:门户网关 GatewayHttpProxy 在 POST 时的某些情况下会出现无法正确代理的问题 +文档:完善 RPC 配置的相关文档 +文档:完善 Gateway 配置的相关文档 +文档:添加 fatjar 打包的相关文档 +文档:完善 fatjar 部署运行的相关文档 + + + +jboot v3.1.4: +优化:重构 ClassScanner ,提高在 fatjar 模式的扫描性能 +优化:重构 ConfigManager ,以便更好的支持 fatjar 模式下的配置文件读取 +优化:重构 JbootCoreConfig,方便在 fatjar 下能够准确读取 html 等资源文件 +优化:重构 JbootCoreConfig,把 JFinal-Weixin 设置非必须依赖 + + + +jboot v3.1.3: +新增:新增对 JDK11+ 的支持 + + + +jboot v3.1.1: +新增:Gateway 网关自定义拦截器的支持,方便通过网关进行鉴权等操作 +新增:Gateway 网关新增错误重试次数的配置,方便网关重试配置 +新增:jboot.properties 配置里的 value 值添加 ${para} 参数的支持 +新增:undertow 端口配置,添加 -1 的支持,通过 -1 配置可用的随机端口 +新增:JbootRpcApplication,方便用于只启动 RPC Service 服务,用于给消费者提供服务 +修复:修复 @RPCInject 在拦截器使用的时候出错的问题 +修复:RPC 工具类 Utils.appendAnnotation 无法正确给 int 、boolean 参数赋值的问题 +优化:当不对 Dubbo 的 qos 配置的时候,设置为默认关闭,方便单机情况下进行开发调试 +文档:更新 Gateway 网关的相关文档 + + + +jboot v3.1.0: +新增:新增网关的支持,路由规则支持 host、path、query等 三种模式,同时支持基于 Sentinel 的限流配置 +新增:新增对 Dubbo 多协议、多注册中心等的支持 +新增:新增对注解 @Configuration 的支持,可以通过其构建 name 实例 +新增:JbootAopInterceptor 可以动态的添加或者移除拦截器 +新增:配置文件可以配置 Map、Set、List 和 数组的支持 +新增:JbootCache 新增可以获取素有 Names 的功能,方便对缓存进行运维 +新增:JbootCache 新增可以对分布式缓存进行刷新,方便对缓存进行运维 +新增:jboot-system.properties 的支持,用于替代启动参数的 -D,使用第三方组件的时候更加方便 +修复:修复 JbootHttpImpl 无法上传文件的bug +修复:修复 JbootHttpImpl 无法正确获取 gzip 压缩内容的问题 +优化:重构 RPC 的模块,使之更加简单清晰 +优化:升级 fastsjon 等相关依赖到最新版本,修改某些错误单词拼写的方法名等 +文档:完善 AOP 的相关文档 +文档:完善 Sentinel 限流的文档 +文档:完善 RPC 的相关文档 +文档:新增 网关配置使用的相关文档 + + + +jboot v3.0.5: +修复:SqlBuilder 在某些极端情况下生成 SQL 的一些问题 +优化:移除 Jboot 的一些过时的方法 + + + +jboot v3.0.4: +新增:Jboot MQ 新增 Local 类型 +新增:sqlPart 支持参数设置 +优化:修改 Columns.string() 为 Columns.sqlPart() +优化:JbootModelConfig 的 primarykeyValueGenerator 和 Filter 支持通过 api 动态配置 +优化:StrUtil.isNumeric、isDecimal、isEmail 等方法 +优化:完善 JbootCounter 功能 +修复:当 Columns.group() 内容为空的时候,构建了错误Sql的问题 +修复:Columns.isNullIf 条件判断错误的问题 + + + +jboot v3.0.3: +新增:Cache 模块新增 refresh() 的方法,在分布式缓存在某些极端情况下出现不同步的时候进行刷新。 +新增:对 Columns 优化,新增 gourpIf()/stringIf()/isNullIf() 等方法 +新增:新增 PrimaryKeyValueGenerator 对 Model 的主键值生成策略配置 +新增:新增 JbootModelFilter 对 Model 的过滤器策略配置,可以用于在 save 或者 update 的时候防止 Model 存在 xss 等问题 +优化:对 MQ 进行优化,当不配置的时候可能返回一个错误的 MQ 对象的问题 +优化:升级 JFinal-Wexin、Fastjson、Fastxml 等依赖到最新版本 +优化:JbootDirectiveBase 当传入空值的时候明确返回 null +修复:当 Columns 查询的时候,使用 group 会出现 value 内容缺失的问题 + + + +jboot v3.0.2: +新增:StrUtil 新增 splitToSetByComma() 方法 +新增:StrUtil 新增 escapeModel() 和 escapeMap() 方法 +优化:StrUti.isDecimal() 方法,防止在某些极端情况下出现判断不正确的问题 +优化:对 pom.xml 进行优化,排除非必要的依赖 +优化:重构 Sentinel 模块,修改为非必须依赖 + + + jboot v3.0.1: 修复:紧急修复 v3.0.0 必须依赖 nacos-client 的问题 diff --git a/doc/.vuepress/config.js b/doc/.vuepress/config.js new file mode 100644 index 0000000000000000000000000000000000000000..44c3e1413ffb9b1256045b08c754c5c316a5dbeb --- /dev/null +++ b/doc/.vuepress/config.js @@ -0,0 +1,157 @@ +//参考: +// https://github.com/vuejs/vuepress/blob/master/packages/docs/docs/.vuepress/config.js +// https://vuepress-theme-reco.recoluan.com/views/1.x/ +module.exports = { + title: 'Jboot 官方网站', + description: 'Jboot 一个开源的分布式、商业级微服务框架。', + // base:'/docs/', + + theme: 'vuepress-theme-reco', + themeConfig: { + //腾讯 404 公益配置 + noFoundPageByTencent: false, + + mode: 'light', // 默认 auto,auto 跟随系统,dark 暗色模式,light 亮色模式 + modePicker: false, // 默认 true,false 不显示模式调节按钮,true 则显示 + subSidebar: 'auto',//在所有页面中启用自动生成子侧边栏,原 sidebar 仍然兼容 + // author + author: 'jboot', + + // if your docs are in a different repo from your main project: + docsRepo: 'yangfuhai/jboot', + // if your docs are in a specific branch (defaults to 'master'): + docsBranch: 'master', + // if your docs are not at the root of the repo: + docsDir: 'doc', + // defaults to false, set to true to enable + editLinks: true, + // custom text for edit link. Defaults to "Edit this page" + editLinkText: '编辑此页面', + + + lastUpdated: '更新时间', // string | boolean + + nav: [ + {text: '首页', link: '/'}, + {text: 'Jboot文档', link: '/docs/'}, + {text: 'JbootAdmin', link: '/jbootadmin/'}, + {text: '提问', link: 'https://gitee.com/JbootProjects/jboot/issues'}, + {text: 'JPress', link: 'http://www.jpress.io'}, + { + text: '源码下载', items: [ + {text: 'Gitee', link: 'https://gitee.com/JbootProjects/jboot'}, + {text: 'Github', link: 'https://github.com/yangfuhai/jboot'} + ] + }, + ], + + sidebar: { + '/docs/': [{ + title: '认识 Jboot', + collapsable: false, + children: [ + {title: 'Jboot 简介', path: '/docs/'}, + {title: '快速开始', path: '/docs/start'} + ], + }, + + { + title: '开发文档', + collapsable: false, + children: [ + {title: '安装', path: '/docs/install'}, + {title: '配置', path: '/docs/config'}, + {title: 'JFinalConfig', path: '/docs/jfinalConfig'}, + {title: 'MVC', path: '/docs/mvc'}, + {title: 'Validator', path: '/docs/validator'}, + {title: 'WebSocket', path: '/docs/websocket'}, + {title: 'Json', path: '/docs/json'}, + {title: 'Jwt', path: '/docs/jwt'}, + {title: 'AOP', path: '/docs/aop'}, + {title: '数据库', path: '/docs/db'}, + {title: '缓存', path: '/docs/cache'}, + {title: 'Redis', path: '/docs/redis'}, + {title: 'RPC 调用', path: '/docs/rpc'}, + {title: 'MQ 消息队列', path: '/docs/mq'}, + {title: 'Gateway 网关', path: '/docs/gateway'}, + {title: '任务调度', path: '/docs/schedule'}, + {title: 'Jboot限流', path: '/docs/limit'}, + {title: 'Sentinel限流', path: '/docs/sentinel'}, + {title: '分布式附件管理', path: '/docs/attachment'}, + {title: '监控', path: '/docs/metrics'}, + {title: '事件机制', path: '/docs/event'}, + {title: '序列化', path: '/docs/serialize'}, + {title: 'SPI扩展', path: '/docs/spi'}, + {title: '单元测试', path: '/docs/junit'}, + {title: '代码生成器', path: '/docs/codegen'}, + {title: 'API 文档生成', path: '/docs/apidoc'}, + {title: '项目构建', path: '/docs/build'}, + {title: '项目部署', path: '/docs/deploy'}, + {title: 'Docker', path: '/docs/docker'}, + {title: '热加载', path: '/docs/hotload'}, + {title: 'Shiro', path: '/docs/shiro'}, + {title: 'Swagger', path: '/docs/swagger'}, + ], + }, + + { + title: '性能', + collapsable: false, + children: [ + {title: '性能测试', path: '/docs/benchmark'}, + ], + } + ], + + + '/jbootadmin/': [{ + title: '认识 JbootAdmin', + collapsable: false, + children: [ + {title: '简介', path: '/jbootadmin/'}, + {title: '功能介绍', path: '/jbootadmin/feature'}, + {title: '我要购买', path: '/jbootadmin/buy'} + ], + }, + { + title: '开发文档', + collapsable: false, + children: [ + {title: '开始', path: '/jbootadmin/start'}, + {title: '数据库设计', path: '/jbootadmin/db'}, + {title: '后台菜单', path: '/jbootadmin/menu'}, + {title: '权限设计', path: '/jbootadmin/permission'}, + {title: '前端组件', path: '/jbootadmin/front'}, + {title: '安全防护', path: '/jbootadmin/safety_precautions'}, + ], + }, + { + title: '运维和部署', + collapsable: false, + children: [ + {title: '部署', path: '/jbootadmin/deploy'}, + {title: 'CDN配置', path: '/jbootadmin/cdn'}, + {title: '文件同步', path: '/jbootadmin/attachment'}, + {title: '配置中心', path: '/jbootadmin/config'}, + {title: '门户网关', path: '/jbootadmin/gateway'}, + {title: '服务器管理', path: '/jbootadmin/server'}, + ], + }, + ] + }, + sidebarDepth: 1 + }, + + head: [ + ['link', {rel: 'icon', href: '/logo.png'}], + ['script', {}, ` + var _hmt = _hmt || []; + (function() { + var hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?d6b9b94a6fafaa41c63920e1af80bcaf"; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + })(); + `] + ] +} diff --git a/doc/.vuepress/styles/index.styl b/doc/.vuepress/styles/index.styl new file mode 100644 index 0000000000000000000000000000000000000000..0dd1d9126ce94ed060db99e9366b53b2c1e0d6ed --- /dev/null +++ b/doc/.vuepress/styles/index.styl @@ -0,0 +1,28 @@ +#app .page-title, #app .theme-reco-content.content__default, #app footer, #app .page-nav, #app .comments-wrapper { + max-width: 960px !important; + // padding: 1.5rem !important; +} + +.page { + /* 处理文本溢出 */ + word-wrap: break-word !important; + /* 允许再单词内换行 */ + word-break: break-all; + padding-top: 4rem !important; +} + +.page .page-title { + // 调整标题容器边距 + padding: 0rem 1.5rem !important; +} + + //, #app .theme-reco-content.content__default > p:nth-child(2) +#app .theme-reco-content.content__default > h1{ + // 处理文档的第一行标题 + display: none !important; +} + +.home-center.content__default{ + padding-left: 0 !important; + padding-right: 0 !important; +} \ No newline at end of file diff --git a/doc/CNAME b/doc/CNAME new file mode 100644 index 0000000000000000000000000000000000000000..cf681f252a279283e9947d930b2fa6e0ac105f32 --- /dev/null +++ b/doc/CNAME @@ -0,0 +1 @@ +jboot.io \ No newline at end of file diff --git a/doc/deploy.sh b/doc/deploy.sh new file mode 100755 index 0000000000000000000000000000000000000000..69b97ca042d48a107f05d817ea10db222f489eea --- /dev/null +++ b/doc/deploy.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env sh + +# abort on errors +set -e + +# build +yarn build + + + +cp CNAME .vuepress/dist + +# navigate into the build output directory +cd .vuepress/dist + + +# if you are deploying to a custom domain +# echo 'www.example.com' > CNAME + +git init +git add -A +git commit -m 'deploy' + +# if you are deploying to https://.github.io +git push -f git@github.com:yangfuhai/yangfuhai.github.io.git master + +git push -f git@github.com:yangfuhai/yangfuhai.github.io.git master:gh-pages + +# if you are deploying to https://.github.io/ +# git push -f git@github.com:/.git master:gh-pages + +cd - \ No newline at end of file diff --git a/doc/deploy_to_alioss.sh b/doc/deploy_to_alioss.sh new file mode 100755 index 0000000000000000000000000000000000000000..aac1c6f428d86d44cd2433211c04dc46ae9c69c3 --- /dev/null +++ b/doc/deploy_to_alioss.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +# abort on errors +set -e + +# build +yarn build + +ossutil rm oss://jboot-doc-site/ -rf +ossutil cp -rf .vuepress/dist oss://jboot-doc-site/ \ No newline at end of file diff --git a/doc/deploy_to_gitee.sh b/doc/deploy_to_gitee.sh new file mode 100755 index 0000000000000000000000000000000000000000..b7edd49c798045ee45eced3c6281bb00889c120d --- /dev/null +++ b/doc/deploy_to_gitee.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env sh + +# abort on errors +set -e + +# build +yarn build + + + +cp CNAME .vuepress/dist + +# navigate into the build output directory +cd .vuepress/dist + + +# if you are deploying to a custom domain +# echo 'www.example.com' > CNAME + +git init +git add -A +git commit -m 'deploy' + +# need add config base:'/docs/', +# if you are deploying to https://.github.io +git push -f https://gitee.com/JbootProjects/docs.git master + +# if you are deploying to https://.github.io/ +# git push -f git@github.com:/.git master:gh-pages + +cd - \ No newline at end of file diff --git a/doc/docs/aop.md b/doc/docs/aop.md new file mode 100644 index 0000000000000000000000000000000000000000..9bb9fc20f933cd330fa3f58b13434d71483f512a --- /dev/null +++ b/doc/docs/aop.md @@ -0,0 +1,446 @@ +# 面向切面编程 + +## 目录 + +- JFinal AOP +- JBoot AOP +- @Inject +- @RPCInject +- @Bean +- @BeanExclude +- @Configuration +- @ConfigValue +- @StaticConstruct +- @PostConstruct +- InterceptorBuilder + +## JFinal AOP + +参考文档:https://jfinal.com/doc/4-6 + +## JBoot AOP + +JBoot AOP 在 JFinal AOP 的基础上,新增了我们在分布式下常用的功能,同时借鉴了 Spring AOP 的一些特征,对 JFinal AOP 做了增强,但是又没有 Spring AOP 体系的复杂度。 + +## @Inject + +我们可以通过 @Inject 对任何 Bean 的属性进行注入,例如 Controller + +```java +@RequestMapping("/helloworld") +public class MyController extends Controller{ + + @Inject + private UserService userService; + + public void index(){ + renderJson(userService.findAll()); + } +} +``` + +在以上的例子中,在默认情况下,JFinal AOP 会去实例化一个 UserService 实例,并注入到 MyController 的 userService 中,因此需要注意的是:UserService 必须是一个可以被实例化的类,**不能是**抽象类或者接口(Interface)。 + +如果说,UserService 是一个接口,它有实现类比如 `UserServiceImpl.class`,JFinal 提供了另一种方案,代码如下: + +```java +@RequestMapping("/helloworld") +public class MyController extends Controller{ + + @Inject(UserServiceImpl.class) + private UserService userService; + + public void index(){ + renderJson(userService.findAll()); + } +} +``` + +## @Bean +在以上的例子中,我们认为 `@Inject(UserServiceImpl.class)` 这可能不是最好的方案,因此,JBoot 提供了可以通过注解 `@Bean` 给 `UserServiceImpl.class` 添加在类上,这样 Jboot 在启动的时候,会自动扫描到 `UserServiceImpl.class` ,并通过 JbootAopFactory 把 UserService 和 UserServiceImpl 添加上关联关系。 + +代码如下: + +Controller: + +```java +@RequestMapping("/helloworld") +public class MyController extends Controller{ + + @Inject + private UserService userService; + + public void index(){ + renderJson(userService.findAll()); + } +} +``` + +UserService: + +```java +public interface UserService{ + List findAll(); +} +``` + + +UserServiceImpl: + +```java +@Bean +public class UserServiceImpl implements UserService{ + public List findAll(){ + //do sth + } +} +``` + +当一个接口有多个实现类,或者当在系统中存在多个实例对象,比如有两份 Cache 对象,一份可能是 Redis Server1,一份可能是 Redis Server2,或者有两份数据源 DataSource 等,在这种情况下,我们注入的时候就需要确定注入那个实例。 + +这时候,我们就需要用到 `@Bean(name= "myName")` 去给不同的子类去添加注释。 + +例如: + + +```java +@RequestMapping("/helloworld") +public class MyController extends Controller{ + + @Inject + @Bean(name="userServieImpl1") //注入名称为 userServieImpl1 的实例 + private UserService userService1; + + @Inject + @Bean(name="userServieImpl2") //注入名称为 userServieImpl2 的实例 + private UserService userService2; + + public void index(){ + renderJson(userService.findAll()); + } +} +``` + +UserService: + +```java +public interface UserService{ + List findAll(); +} +``` + + +UserServiceImpl1: + +```java +@Bean(name="userServiceImpl1") +public class UserServiceImpl1 implements UserService{ + public List findAll(){ + //do sth + } +} +``` + + +UserServiceImpl2: + +```java +@Bean(name="userServiceImpl2") +public class UserServiceImpl2 implements UserService{ + public List findAll(){ + //do sth + } +} +``` + +这种情况,只是针对一个接口有多个实现类的情况,那么,如果是一个接口只有一个实现类,但是有多个实例,如何进行注入呢? + +参考 @Configuration 。 + + +## @BeanExclude + +当我们使用 `@Bean` 给某个类添加上注解之后,这个类会做好其实现的所有接口,但是,很多时候我们往往不需要这样做,比如: + +```java +@Bean +public class UserServiceImpl implements UserService, +OtherInterface1,OtherInterface2...{ + + public List findAll(){ + //do sth + } +} +``` + +在某些场景下,我们可能只希望 UserServiceImpl 和 UserService 做好映射关系,此时,`@BeanExclude` 就派上用场了。 + +如下代码排除了 UserServiceImpl 和 OtherInterface1,OtherInterface2 的映射关系。 + +```java +@Bean +@BeanExclude({OtherInterface1.cass,OtherInterface2.class}) +public class UserServiceImpl implements UserService, +OtherInterface1,OtherInterface2...{ + + public List findAll(){ + //do sth + } +} +``` + +## @Configuration + + +在 Jboot 中的 `@Configuration` 和 Spring 体系的 `@Configuration` 功能类似。 + + +我们可以在一个普通类中添加注解 `@Configuration` , 然后在其方法通过 `@Bean` 去对方法进行添加注解。 + +例如: + +```java +@Configuration +public class AppConfiguration { + + @Bean(name = "myCommentServiceFromConfiguration") + public CommentService myCommentService1(){ + CommentService commentService = new CommentServiceImpl(); + return commentService; + } + + @Bean + public CommentService myCommentService2(){ + CommentService commentService = new CommentServiceImpl(); + return commentService; + } +} + +``` + +这样,在一个 Jboot 应用中,就会存在两份 `CommentService` 他们的名称分别为:myCommentServiceFromConfiguration 和 myCommentService2(当只用了注解 @Bean 但是未添加 name 参数时,name 的值为方法的名称) + +这样,我们就可以在 Controller 里,通过 `@Inject` 配合 `@Bean(name = ... )` 进行注入,例如: + + +```java +@RequestMapping("/aopcache") +public class AopCacheController extends JbootController { + + @Inject + @Bean(name="myCommentService2") + private CommentService commentService; + + @Inject + @Bean(name = "myCommentServiceFromConfiguration") + private CommentService myCommentService; + + + public void index() { + System.out.println("commentService:"+commentService); + System.out.println("myCommentService:"+myCommentService); + } +} +``` + + +## @ConfigValue + +在 AOP 注入中,可能很多时候我们需要注入的只是一个配置内容,而非一个对象实例,此时,我们就可以使用注解 `@ConfigValue`,例如: + +```java +@RequestMapping("/aop") +public class AopController extends JbootController { + + @ConfigValue("undertow.host") + private String host; + + @ConfigValue("undertow.port") + private int port; + + @ConfigValue(value = "undertow.xxx") + private int xxx; +} +``` + +此时,配置文件 jboot.properties (包括分布式配置中心) 里配置的 undertow.host 的值自动赋值给 host 属性。其他属性同理。 + + +## @StaticConstruct + +静态的构造方法。 + +在某些类中,这个类的创建方式并不是通过 new 的方式进行创建的,或者可能构造函数是私有的,或者这个类可能是一个单例示例的类,比如: + +```java +public class JbootManager { + + private static JbootManager me = new JbootManager(); + + public static JbootManager me() { + return me; + } + + private JbootManager(){ + //do sth + } +} +``` + +我们在其他类注入 JbootManager 的时候,并不希望通过 new JbootManager() 的方式进行创建注入,还是希望通过其方法 `me()` 进行获取。 + +此时,我们就可以给 JbootManager 类添加 `@StaticConstruct` 注解,例如: + +```java +@StaticConstruct +public class JbootManager { + + private static JbootManager me = new JbootManager(); + + public static JbootManager me() { + return me; + } + + private JbootManager(){ + //do sth + } +} +``` + +但如果 JbootManager 有多个返回自己对象的静态方法,我们就可以使用 `@StaticConstruct` 的 value 参数来指定。 + +例如: + +```java +@StaticConstruct("me") +public class JbootManager { + + private static JbootManager me = new JbootManager(); + + public static JbootManager me() { + return me; + } + + public static JbootManager create(){ + return new JbootManager(); + } + + private JbootManager(){ + //do sth + } +} +``` + +## @PostConstruct + +`@PostConstruct` 是 Java 自带的注解,用于在对 Java 对象初始化、并进行注入成功之后,进行初始化的工作。 + +比如: + +```java +@Bean +public class YourServiceImpl implements YourService{ + + @Inject + private OtherService1 otherService1; + + @Inject + private OtherService2 otherService2; + + + private Service3 service3; + + public YourServiceImpl(){ + // 构造方法运行的时候, otherService1 和 otherService2 都为 null, + // 因为还没有开始注入 + } + + @PostConstruct + private void initService3() { + // 构造函数执行完毕后,Jboot 会自动立即执行此方法 + // 此时 otherService1 和 otherService2 已经有值了 + + otherService1.doSth(); + otherService2.doSth(); + + service3 = createService3(otherService1,otherService2); + } + + public void doSomething(){ + service3.xxx(); + } +} +``` + +此时,我们就可以通过如下方法使用 YourService 了 + +```java +YourService service = Aop.get(YourService.class); +service.doSomething(); +``` + +`@PostConstruct` 的特点: + +- 必须是 `非静态` 方法 +- 必须是 `无参数` 方法 +- 在同一个类中,只能存在一个方法被 `@PostConstruct` 修饰。 +- 如果子类和父类都存在 `@PostConstruct` 修饰的方法,子类优先执行,父类后执行。 + + + +## InterceptorBuilder + +InterceptorBuilder : 拦截器构建器。他能够根据当前 Controller、Service 等的方法信息(比如方法注解、方法名称、方法参数),来对当前方法动态 添加 或者 删除 拦截器, +在之前 JFinal 的体系里,给某个方法添加拦截器只能通过 @Before({MyInterceptor.class}) 或者 configInterceptor() 配置方法里添加全局拦截器。 + +这样带来一个不够优雅的地方是,如果我们想给某个方法 ”增强“,只能以添加全局拦截器的方式来做,这样所有方法都会被该拦截器拦截, +然后再通过其判断进行 ”放行“ 。这样就带来了性能效率的下降,因为在我们整个系统中,有非常多的方法是没有必要走拦截器的。 + +有了 InterceptorBuilder 之后,我们可以通过 InterceptBuilder 给某个方法来构建其特定的拦截器,而不是全局拦截器, +这样,极大的提高了性能,同时减少了不必要的调用堆栈。 + + +如何使用 InterceptorBuilder 呢?如下代码: + +```java +public class YourAppListener extends JbootAppListenerBase{ + + public void onInit() { + InterceptorBuilderManager.me().addInterceptorBuilder(new MyInterceptorBuilder()); + } +} +``` + +或者 `CacheInterceptorBuilder` 添加主键 `@AutoLoad` + + +> 注意:使用注解 `@Autoload` 之后,就不要通过 `InterceptorBuilderManager` 来添加了,`@Autoload` 的作用就是在其启动的时候自动添加进去。 + +比如 `@Cacheable` 的拦截器构建 CacheInterceptorBuilder 代码如下: + +```java +@AutoLoad //自动把当前的 CacheInterceptorBuilder 添加到 InterceptorBuilderManager 里去。 +public class CacheInterceptorBuilder implements InterceptorBuilder { + + /** + * 开始都某个方法进行构建 + * @param serviceClass 方法所在的类 + * @param method 方法信息 + * @param interceptors 该方法已经有的拦截器 + */ + @Override + public void build(Class serviceClass, Method method, Interceptors interceptors) { + + Cacheable cacheable = method.getAnnotation(Cacheable.class); + if (cacheable != null) { + interceptors.add(new CacheableInterceptor()); + + // 或者使用如下的方式添加,这样,所有的方法都会被同一个 "实例" 拦截 + // interceptors.add(CacheableInterceptor.class) + } + + } +} +``` + +在 `build()` 方法中的 Interceptors,我们不仅仅可以通过 Interceptors 来添加拦截器,我们还可以通过其来删除拦截器、或者修改拦截器的顺序等等操作。 \ No newline at end of file diff --git a/doc/docs/apidoc.md b/doc/docs/apidoc.md new file mode 100644 index 0000000000000000000000000000000000000000..07830aaae7edcfc2b46d7126c2ff9fbc6470e581 --- /dev/null +++ b/doc/docs/apidoc.md @@ -0,0 +1,237 @@ +# Jboot API 文档生成 + +在 Jboot 中内置了 4 个注解,用于生成帮助开发者生成 API 文档。它们分别是 + +- @Api :给 Controller 配置,一个 Controller 生成一个文档文件。 +- @ApiOper :给 Controller 的方法配置。 +- @ApiPara :给 Controller 的参数进行配置。 +- @ApiResp :给 Controller 的 Render 内容进行配置。 + +## 基本使用 + +生成文档的过程,需要自己写一个 `main()` 方法,然后通过 ApiDocManager 来生成,代码如下: + +```java +public class ApiDocGenerator { + + public static void main(String[] args) { + + ApiDocConfig config = new ApiDocConfig(); + config.setBasePath("./doc/api"); + + ApiDocManager.me().genDocs(config); + } +} +``` + +Controller 代码如下: + +```java +@RequestMapping("/api/user") +@Api("用户相关API") +public class UserApiController extends ApiControllerBase { + + @Inject + private UserService userService; + + + @ApiOper("用户登录") + @ApiResp(field="Jwt",notes="Jwt 的 Token 信息") + public Ret login( + @ApiPara(value = "登录账户", notes = "可以是邮箱") @NotNull String loginAccount + , @ApiPara("登录密码") @NotNull String password) { + //.... + return Ret.ok().set("Jwt","...."); + } + + + @ApiOper("用户详情") + public Ret detail(@ApiPara("用户ID") @NotNull Long id) { + //.... + } + + + @ApiOper("更新用户信息") + public Ret update(@ApiPara("用户 json 信息") @JsonBody @NotNull User user) { + //.... + } +} +``` + +默认情况下,` ApiDocManager.me().genDocs(config)` 生成的是 Markdown 文档,内容如下: + +![](./static/images/apidoc1.jpg) + +## 不同的文档生成在不同的目录 + +一般情况下,如下的代码会去找到所有带有 `@Api` 注解的 `Controller` 生成文档,并生成在同一个目录: + +```java +public class ApiDocGenerator { + + public static void main(String[] args) { + + ApiDocConfig config = new ApiDocConfig(); + config.setBasePath("./doc/api"); + + ApiDocManager.me().genDocs(config); + } +} +``` + +不同的 `Controller` 生成在不同的目录,代码如下: + +```java +public class ApiDocGenerator { + + public static void main(String[] args) { + + ApiDocConfig config1 = new ApiDocConfig(); + config1.setBasePath("./doc/api1"); + config1.setPackagePrefix("com.xxx.package1"); + + ApiDocManager.me().genDocs(config1); + + + + ApiDocConfig config2 = new ApiDocConfig(); + config2.setBasePath("./doc/api2"); + config2.setPackagePrefix("com.xxx.package2"); + + ApiDocManager.me().genDocs(config2); + } +} +``` + +## 多个 `Controller` 生成一个文档 + +`@Api` 注解提供了一个 `collect` 的配置,用于汇总其他 `Controller` 的接口。 例如: + +```java +@RequestMapping("/api/user") +@Api(value="用户相关API",collect={Controler1.class, Controller2.class}) +public class UserApiController extends ApiControllerBase { + + + @ApiOper("用户登录") + public Ret login(@ApiPara(value = "登录账户", notes = "可以是邮箱") @NotNull String loginAccount + , @ApiPara("登录密码") @NotNull String password) { + //.... + } + + +} +``` + +`@Api(value="用户相关API",collect={Controler1.class,Controller2.class})` 表示 `UserApiController` 生成 +的文档会把 `Controler1` 和 `Controller2` 的接口也汇总到此文档里来。 + +> 注意:此时,`Controler1` 和 `Controller2` 不再需要添加 `@Api` 注解。 + +## 所有的 API 生成在 1 文档里 + +```java +public class ApiDocGenerator { + + public static void main(String[] args) { + + ApiDocConfig config = new ApiDocConfig(); + config.setBasePath("./doc/api"); + + //所有的 api 信息生成在同一个文档里 + config.setAllInOneEnable(true); + + ApiDocManager.me().genDocs(config); + } +} +``` + + + + +## 无参数的 Action 生成 API 文档 +在 Jboot 和 JFinal 中,有很多 Controller 的方法可能不是带有参数的,而是通过 `getPara()` 等方法来获取参数。 + +```java +@RequestMapping("/api/user") +@Api(value="用户相关API") +public class UserApiController extends ApiControllerBase { + + public Ret login() { + String loginName = getPara("loginName"); + String password = getPara("password"); + + //.... + } +} +``` + +我们可以使用 `@ApiParas` 注解,代码如下: + +```java +import io.jboot.apidoc.annotation.ApiPara; +import io.jboot.apidoc.annotation.ApiParas; + +@RequestMapping("/api/user") +@Api(value = "用户相关API") +public class UserApiController extends ApiControllerBase { + + + @ApiParas({ + @ApiPara(value="登录名",name="loginName"), + @ApiPara(value="密码",name="password"), + }) + public Ret login() { + String loginName = getPara("loginName"); + String password = getPara("password"); + + //.... + } +} +``` + +## 生成 word、html 或更多... + +默认情况下,在如下的代码中,`ApiDocManager` 生成的是 Markdown 文档。 + +```java +public class ApiDocGenerator { + + public static void main(String[] args) { + + ApiDocConfig config = new ApiDocConfig(); + config.setBasePath("./doc/api"); + + ApiDocManager.me().genDocs(config); + } +} +``` + + +如果我们需要生成 word、html 等其他文档,需要自定义 `ApiDocRender` ,并配置给 `ApiDocManager`。 + +```java +public class MyApiDocRender extends ApiDocRender { + + void render(List apiDocuments, ApiDocConfig config){ + //自定义渲染,生成 html 或者 word 等等... + } + +} +``` + +配置 `MyApiDocRender` 并开始生成文档。 + +```java +public class ApiDocGenerator { + + public static void main(String[] args) { + + ApiDocConfig config = new ApiDocConfig(); + config.setBasePath("./doc/api"); + + ApiDocManager.me().setRender(new MyApiDocRender()); + ApiDocManager.me().genDocs(config); + } +} +``` \ No newline at end of file diff --git a/doc/docs/attachment.md b/doc/docs/attachment.md new file mode 100644 index 0000000000000000000000000000000000000000..0362bd11418247840a0de51b90fab026c28b1706 --- /dev/null +++ b/doc/docs/attachment.md @@ -0,0 +1,304 @@ +# Attachment 附件管理 + +Jboot 定位是分布式的开发系统,在项目进行分布式部署的时候,用户在上传文件时,我们需要对文件进行分布式同步也是必须的。 + +在分布式部署的需求下,我们假设我们把我们的应用部署在 A/B/C 三台服务器上,这三台服务器通过 nginx 或者 SLB 等做负载均衡。 + +此时,也就意味着当用户每次访问我们的应用的时候,可能访问到了 A 服务器,也有可能访问到 B 服务器 或者 C 服务器。 + +当我们的应用假设有一个图片上传的功能,用户在上传图片的时候,假设上传到了 A 服务器,但是第二次去访问附件的时候,可能访问到了 B 服务器,但是 B 服务器却不存在 A 用户刚刚上传的图片,Jboot 的 AttachmentContainer 就是为了解决这一系列问题而存在的。 + +在使用 AttachmentContainer 之前,我们先来了解下以下的几个概念。 + +- AttachmentContainer : 附件容器,就是专门用来存放图片、读取图片和渲染http请求图片的。 +- AttachmentManager : 用来管理 AttachmentContainer 的,一个应用里可以有多个 AttachmentContainer 。比如,阿里云OSS存储的,我们可以来定义一个 AliyunOSSAttachmentContainer; fastDFS 存储我们一样可以来定义一个 FastDFSAttachmentContainer;同时,Attachment 内置了一个默认的 AttachmentContainer,用来存在 ”本地“ 附件的。 + +在使用 AttachmentContainer 之前,我们需要需要编写自己的一个类,来实现 AttachmentContainer 接口,并添加到 AttachmentManager 里去。 + +```java +AliyunOssAttachmentContainer aliyunOss = new AliyunOssAttachmentContainer(); +AttachmentManager.me().addContainer(aliyunOss); +``` + +当我们的 Controller 有文件上传的时候,我们需要调用 AttachmentManager 进行保存,AttachmentManager 最终会保持到其所有的容器里去。 + +例如: + +```java +public void upload() { + if (!isMultipartRequest()) { + renderError(404); + return; + } + + UploadFile uploadFile = getFile(); + if (uploadFile == null) { + renderJson(Ret.fail().set("message", "请选择要上传的文件")); + return; + } + + //通过 AttachmentManager 去保存文件 + String relativePath = AttachmentManager.me().saveFile(file); + file.delete(); + + renderJson(Ret.ok().set("success", true).set("src", relativePath)); +} +``` + +通过 `AttachmentManager.me().saveFile(file);` 保存文件,AttachmentManager 会保持到所有的容器里。 + +当需要读取文件的时候,我们也可以通过 AttachmentManger 去读文件。 + +```java +File attachment = AttachmentManager.me().getFile(relativePath); +``` + +`AttachmentManager.me().getFile(relativePath);` 读取文件的时候,会优先从 默认 的 ”容器“ 去读取,当默认 ”容器“ 不存在该文件的时候,AttachmentManager 会遍历所有的 Container ,直到读到为此。 + + +以下是 `AttachmentManager.me().getFile()` 代码的实现逻辑: + +```java +public File getFile(String relativePath) { + + AttachmentContainer defaultContainer = getDefaultContainer(); + + //优先从 默认的 container 去获取 + File file = defaultContainer.getFile(relativePath); + if (file != null && file.exists()) { + return file; + } + + for (Map.Entry entry : containerMap.entrySet()) { + AttachmentContainer container = entry.getValue(); + try { + if (container != defaultContainer) { + file = container.getFile(relativePath); + if (file != null && file.exists()) { + return file; + } + } + } catch (Exception ex) { + LOG.error("get file error in container :" + container, ex); + } + } + return null; +} +``` + +以下是阿里云 Oss 的代码实现逻辑,可供参考: + +```java +public class AliyunOssAttachmenetContainer implements AttachmentContainer { + + private String basePath = PathKit.getWebRootPath(); + private AliyunOssAttachmentConfig config = JbootConfigManager.me().get(AliyunOssAttachmentConfig.class); + private static ExecutorService fixedThreadPool = NamedThreadPools.newFixedThreadPool(3, "aliyun-oss-upload"); + + + public AliyunOssAttachmenetContainer() { + } + + @Override + public String saveFile(File file) { + if (!config.isEnable()) { + return null; + } + String relativePath = getRelativePath(file); + fixedThreadPool.execute(() -> { + upload(relativePath, file); + }); + return relativePath; + } + + + @Override + public boolean deleteFile(String relativePath) { + if (!config.isEnable()) { + return false; + } + relativePath = removeFirstFileSeparator(relativePath); + OSSClient ossClient = createOSSClient(); + try { + ossClient.deleteObject(config.getBucketname(), relativePath); + } catch (Exception ex) { + LogKit.error(ex.toString(), ex); + } finally { + ossClient.shutdown(); + } + return true; + } + + + @Override + public File getFile(String relativePath) { + if (!config.isEnable()){ + return null; + } + File localFile = new File(basePath, relativePath); + if (localFile.exists()) { + return localFile; + } + if (download(relativePath, localFile)) { + return localFile; + } + return null; + } + + + @Override + public String getRelativePath(File file) { + return FileUtil.removePrefix(file.getAbsolutePath(), basePath); + } + + + /** + * 同步本地文件到阿里云OSS + * + * @param path + * @param file + * @return + */ + public boolean upload(String path, File file) { + if (StrUtil.isBlank(path)) { + return false; + } + + path = removeFirstFileSeparator(path); + path = path.replace('\\', '/'); + + String ossBucketName = config.getBucketname(); + OSSClient ossClient = createOSSClient(); + + try { + ossClient.putObject(ossBucketName, path, file); + boolean success = ossClient.doesObjectExist(ossBucketName, path); + if (!success) { + LogKit.error("aliyun oss upload error! path:" + path + "\nfile:" + file); + } + return success; + + } catch (Throwable e) { + LogKit.error("aliyun oss upload error!!!", e); + return false; + } finally { + ossClient.shutdown(); + } + } + + /** + * 如果文件以 / 或者 \ 开头,去除 / 或 \ 符号 + */ + private static String removeFirstFileSeparator(String path) { + while (path.startsWith("/") || path.startsWith("\\")) { + path = path.substring(1); + } + return path; + } + + /** + * 下载 阿里云 OSS 到本地 + * + * @param path + * @param toFile + * @return + */ + public boolean download(String path, File toFile) { + if (StrUtil.isBlank(path)) { + return false; + } + path = removeFirstFileSeparator(path); + OSSClient ossClient = createOSSClient(); + try { + if (!toFile.getParentFile().exists()) { + toFile.getParentFile().mkdirs(); + } + + if (!toFile.exists()) { + toFile.createNewFile(); + } + ossClient.getObject(new GetObjectRequest(config.getBucketname(), path), toFile); + return true; + } catch (Throwable e) { + LogKit.error("aliyun oss download error!!! path:" + path + " toFile:" + toFile, e); + if (toFile.exists()) { + toFile.delete(); + } + return false; + } finally { + ossClient.shutdown(); + } + } + + + private OSSClient createOSSClient() { + String endpoint = config.getEndpoint(); + String accessId = config.getAccessKeyId(); + String accessKey = config.getAccessKeySecret(); + return new OSSClient(endpoint, new DefaultCredentialProvider(accessId, accessKey), null); + } + + +} +``` + +```java +@ConfigModel(prefix = "aliyunoss") +public class AliyunOssAttachmentConfig { + + private boolean enable = false; + private String endpoint; + private String accessKeyId; + private String accessKeySecret; + private String bucketname; + private boolean delSync; + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getAccessKeyId() { + return accessKeyId; + } + + public void setAccessKeyId(String accessKeyId) { + this.accessKeyId = accessKeyId; + } + + public String getAccessKeySecret() { + return accessKeySecret; + } + + public void setAccessKeySecret(String accessKeySecret) { + this.accessKeySecret = accessKeySecret; + } + + public String getBucketname() { + return bucketname; + } + + public void setBucketname(String bucketname) { + this.bucketname = bucketname; + } + + public boolean isDelSync() { + return delSync; + } + + public void setDelSync(boolean delSync) { + this.delSync = delSync; + } +} +``` \ No newline at end of file diff --git a/doc/docs/benchmark.md b/doc/docs/benchmark.md new file mode 100644 index 0000000000000000000000000000000000000000..5b41d66959178b8185d360dec9db24b36902cedc --- /dev/null +++ b/doc/docs/benchmark.md @@ -0,0 +1,109 @@ +--- +sidebar: auto +--- +# Jboot 性能测试 + +## 测试方式 + +通过 apache benchmark 工具进行压力测试 + +## 测试环境 + +* JDK信息: + * java version "1.8.0_25" + * Java(TM) SE Runtime Environment (build 1.8.0_25-b17) + * Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode) + +* 硬件信息 + * 处理器:2.3 GHz Intel Core i7 + * 内存:16 GB 1600 MHz DDR3 + * 系统:macOS 10.13.4 (17E202) + * 硬件:MacBook Pro (Retina, 15-inch, Late 2013 + +## 测试代码 + +测试代码: + +```java +@RequestMapping("/") +public class HelloDemo extends JbootController { + + public static void main(String[] args) { + Jboot.setBootArg("jboot.mode","product"); + Jboot.run(args); + } + + public void index() { + renderText("hello jboot ..."); + } +} +``` + +代码含义: + +* 默认使用 undertow 服务器 +* 把配置设置为生产模式,生产模式不会打印调试日志 + + +## 测试结果 + +模拟10个并发,10000次访问: + +`ab -c10 -n10000 http://localhost:8080/` + + +``` +This is ApacheBench, Version 2.3 <$Revision: 1807734 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking localhost (be patient) +Completed 1000 requests +Completed 2000 requests +Completed 3000 requests +Completed 4000 requests +Completed 5000 requests +Completed 6000 requests +Completed 7000 requests +Completed 8000 requests +Completed 9000 requests +Completed 10000 requests +Finished 10000 requests + + +Server Software: +Server Hostname: localhost +Server Port: 8080 + +Document Path: / +Document Length: 15 bytes + +Concurrency Level: 10 +Time taken for tests: 0.716 seconds +Complete requests: 10000 +Failed requests: 0 +Total transferred: 2330000 bytes +HTML transferred: 150000 bytes +Requests per second: 13970.07 [#/sec] (mean) +Time per request: 0.716 [ms] (mean) +Time per request: 0.072 [ms] (mean, across all concurrent requests) +Transfer rate: 3178.74 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.1 0 1 +Processing: 0 0 0.1 0 3 +Waiting: 0 0 0.1 0 3 +Total: 0 1 0.1 1 4 + +Percentage of the requests served within a certain time (ms) + 50% 1 + 66% 1 + 75% 1 + 80% 1 + 90% 1 + 95% 1 + 98% 1 + 99% 1 + 100% 4 (longest request) + ``` \ No newline at end of file diff --git a/doc/docs/build.md b/doc/docs/build.md index 840323c013bf6e3249b9a2cceeadee3311c05377..ea6adace44f69c61c2311f52a29ba3878d8983f7 100644 --- a/doc/docs/build.md +++ b/doc/docs/build.md @@ -1,11 +1,12 @@ -# 项目构建 +# 项目打包 ## 目录 -- 单模块 maven 项目构建 -- 多模块 maven 项目构建 +- 单模块 maven 项目打包 +- 多模块 maven 项目打包 +- fatjar 打包(全部打包到一个jar里) -## 单模块 maven 项目构建 +## 单模块 maven 项目打包 在单一模块的maven项目开发中,我们通常在 `src/main/resources` 编写我们的配置文件,因此,在 maven 构建的时候,我们需要添加如下配置: @@ -107,7 +108,7 @@ package.xml 在 项目的根目录下,创建 `jboot.sh` 文件,内容如下: -``` +```shell #!/bin/bash # ---------------------------------------------------------------------- # name: jboot.sh @@ -175,7 +176,7 @@ fi 复制该文件夹到服务器,然后执行里面的 `jboot.sh start` 命令即可上线。 -## 多模块 maven 项目构建 +## 多模块 maven 项目打包 多模块项目在以上配置的基础上,添加 `maven-resources-plugin` maven 插件,用于拷贝其他maven模块的资源文件和html等内容到此运行模块。 @@ -210,4 +211,167 @@ maven 配置如下: ``` -这部分可以参考 jpress 项目,网址:https://gitee.com/fuhai/jpress/blob/v2.0/starter/pom.xml \ No newline at end of file +这部分可以参考 jpress 项目,网址:https://gitee.com/fuhai/jpress/blob/v2.0/starter/pom.xml + +## fatjar 打包(全部打包到一个jar里) + +fatjar 打包指的是,把所有资源(html、css、js)以及项目依赖全部打包到一个 jar 包里,这样我们可以通过 +命令 `java -jar xxx.jar` 启动,更加方便部署,特别是方便在微服务下的多模块部署。 + +fatjar 打包第一步,在 pom.xml 添加如下配置 + +```xml + + + + src/main/resources + + **/*.* + + false + + + + src/main/webapp + + **/*.* + + false + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + UTF-8 + -parameters + + + + + + maven-resources-plugin + + + copy-resources + validate + + copy-resources + + + ${basedir}/target/classes/webapp + + + ${basedir}/src/main/webapp + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + make-assembly + package + + single + + + + + io.jboot.app.JbootApplication + + + + + + jar-with-dependencies + + + + + + + +``` + +第二步:通过 Maven 打包 + +执行命令 `mvn clean package` 进行打包,在 pom.xml 对应的模块下会生成一个 xxx-with-dependencies.jar 的 jar 包,复制该 jar +到服务器上,执行 `java -jar xxx-with-dependencies.jar` 即可启动项目。 + +注意:如果您使用了motan,那么在打包fatjar时需要将spi扩展声明文件需要使用追加模式,可参考 + +```xml + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + + package + + shade + + + + + + META-INF/spring.schemas + + + META-INF/spring.handlers + + + META-INF/services/com.weibo.api.motan.registry.Registry + + + META-INF/services/com.weibo.api.motan.registry.RegistryFactory + + + META-INF/services/com.weibo.api.motan.rpc.Protocol + + + META-INF/services/com.weibo.api.motan.cluster.LoadBalance + + + META-INF/services/com.weibo.api.motan.cluster.Cluster + + + META-INF/services/com.weibo.api.motan.codec.Codec + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + +``` diff --git a/doc/docs/cache.md b/doc/docs/cache.md index 6e7a8afb010b72591097badc0937c74ccd6558e2..70bee8d2b2324aee42bb90420589c0164e437c9d 100644 --- a/doc/docs/cache.md +++ b/doc/docs/cache.md @@ -8,6 +8,8 @@ - EhCache - Redis - EhRedis +- caffeine +- caredis - J2Cache - NoneCache @@ -18,21 +20,23 @@ Jboot 定位为高性能的微服务框架,然而高性能离不开合理的 - ehcache - redis - ehredis +- caffeine +- caredis - j2cache ## 配置 -默认情况下,用户无需做任何配置就可以使用 Jboot 的缓存功能,默认情况下 Jboot 是使用 `Ehcache` 作为 Jboot 的缓存方案。 +默认情况下,用户无需做任何配置就可以使用 Jboot 的缓存功能,默认情况下 Jboot 是使用 `caffeine` 作为 Jboot 的缓存方案。 -如果需要修改把 `Ehcahce` 方案修改为使用 `redis` ,则可以添加如下的配置: +如需把 `caffeine` 方案修改为使用 `redis` ,则可以添加如下的配置: -``` +```properties jboot.cache.type = redis ``` 在使用 `redis` 作为默认的缓存方案时,需要配置上 `redis` 的相关信息,例如: -``` +```properties jboot.cache.redis.host = 127.0.0.1 jboot.cache.redis.port = 3306 jboot.cache.redis.password @@ -54,7 +58,7 @@ jboot.cache.redis.serializer ``` 当,以上未配置的时候,Jboot 自动会去寻找 `redis` 模块来使用,`redis` 的配置为: -``` +```properties jboot.redis.host jboot.redis.port jboot.redis.password @@ -111,7 +115,7 @@ public class JbootRedisCacheImpl extends JbootCacheBase { Jboot 提供了一个工具类 CacheUtil,我们可以直接通过 CacheUtil 来操作缓存。 -``` +```java # 添加内容到缓存 CacheUtil.put("cacheName","key","value") @@ -130,9 +134,9 @@ CacheUtil.setTtl("cacheName","key") ``` 当一个系统有多个缓存组件的时候,可能有 redis 或者 ehcache 等,则可以使用如下use("type") 进行操作。 -``` -CacheUtil.use("redis").put("cacheName","key","value") +```java +CacheUtil.use("redis").put("cacheName","key","value") CacheUtil.use("ehcache").put("cacheName","key","value") ``` @@ -190,3 +194,59 @@ public class CommentServiceImpl implements CommentService { - `getCommentByIdWithCacheTime` 使用 `@Cacheable` 注解,但是添加了 `5秒` 的时间限制,因此,在 5秒钟内,无论调用多少次,返回的随机数都是一样的,5秒之后缓存被删除,再次调用之后会是一个新的随机数,新的随机数会继续缓存 5秒钟。 - `updateCache` 使用了注解 `@CachePut` ,每次调用此方法之后,会更新掉该 id 值的缓存 - `delCache` 使用了 `@CacheEvict` 注解,每次调用会删除该 id 值的缓存 + +**@Cacheable 在 Controller 中使用时注意事项** + +默认情况下,@Cacheable 在 Controller 中使用时,系统会缓存 Request 的所有 Attributes 内容,这些内容可能来源于 +Controller 的 Action 通过 setAttr 进行设置,也可能来源于 Interceptor 设置。 + +当下次请求的时候,Jboot 会直接使用缓存的数据进行直接渲染,而不再次执行 Controller 的 Action。 + +但是,在很多时候,我们并不希望 Jboot 缓存所有的数据,比如:用户的登录状态。此时,我们可以通过如下的方法过滤掉缓存数据, +保证每个请求都是最新的,而非缓存的数据。 + +```java +ActionCachedContent.addIgnoreAttr("ACCOUNT") +``` + +通过这个配置后,我们在拦截器 Interceptor 中添加的 ACCOUNT 属性,不会进行缓存。从而达到每次请求,都是都是最新的 ACCOUNT +数据。 + +```java +class AccountInterceptor extends Interceptor { + + public void intercept(Invocation inv) { + Accont account = accountService.findBy.... + inv.getController().setAttr("ACCOUNT",account); + + //.... + } +} +``` + +此时,虽然我们给 Controller 添加了 @Cacheable 属性,但是 ACCOUNT 的数据每个请求都是最新的。值得一提的是: + +```java +ActionCachedContent.addIgnoreAttr("ACCOUNT") +``` +是全局配置的,如果我们想要单独为每个请求配置不缓存的 attribute 时,可以在通过如下方式添加 + +```java +class AccountInterceptor extends Interceptor { + + public void intercept(Invocation inv) { + Set ignoreAttrs = new HashSet<>(); + ignoreAttrs.add("attr1"); + ignoreAttrs.add("attr2"); + ignoreAttrs.add("attr..."); + + //设置当前请求不进行缓存的 attr 属性 + inv.getController().set(CacheableInterceptor.IGNORE_CACHED_ATTRS,ignoreAttrs); + + Accont account = accountService.findBy.... + inv.getController().setAttr("ACCOUNT", account); + + //.... + } +} +``` \ No newline at end of file diff --git a/doc/docs/codegen.md b/doc/docs/codegen.md index f43bf2d3b976befbb54ca6e44fd240645d8b857e..30dc3e38ffcc1e069cddc788485422dec9232d7c 100644 --- a/doc/docs/codegen.md +++ b/doc/docs/codegen.md @@ -2,7 +2,7 @@ Jboot 内置了一个简易的代码生成器,可以用来生成model层和Service层的基础代码,在生成代码之前,请先配置jboot.properties关于数据库相关的配置信息,Jboot 代码生成器会通过该配置去链接数据库。 -``` +```properties jboot.datasource.type=mysql jboot.datasource.url=jdbc:mysql://127.0.0.1:3306/jbootdemo jboot.datasource.user=root @@ -30,8 +30,8 @@ public class GenTester { String baseModelPackage = "io.jboot.test.codegen.modelbase"; //生成的BaseModel的包名 //Model存放的路径,一般情况下是 /src/main/java 下,如下是放在 test 目录下 - String modelDir = PathKit.getWebRootPath() + "/src/test/java/" + modelPackage.replace(".", "/"); - String baseModelDir = PathKit.getWebRootPath() + "/src/test/java/" + baseModelPackage.replace(".", "/"); + String modelDir = CodeGenHelpler.getUserDir() + "/src/test/java/" + modelPackage.replace(".", "/"); + String baseModelDir = CodeGenHelpler.getUserDir() + "/src/test/java/" + baseModelPackage.replace(".", "/"); System.out.println("start generate..."); System.out.println("generate dir:" + modelDir); @@ -46,14 +46,14 @@ public class GenTester { //设置 service 层代码的存放目录 - String serviceOutputDir = PathKit.getWebRootPath() + "/src/test/java/" + servicePackage.replace(".", "/"); - String serviceImplOutputDir = PathKit.getWebRootPath() + "/src/test/java/" + serviceImplPackage.replace(".", "/"); + String serviceOutputDir = CodeGenHelpler.getUserDir() + "/src/test/java/" + servicePackage.replace(".", "/"); + String serviceImplOutputDir = CodeGenHelpler.getUserDir() + "/src/test/java/" + serviceImplPackage.replace(".", "/"); //开始生成代码 new JbootServiceInterfaceGenerator(servicePackage, serviceOutputDir, modelPackage).generate(); - new JbootServiceImplGenerator(servicePackage, serviceImplOutputDir, modelPackage).setImplName("provider").generate(); + new JbootServiceImplGenerator(servicePackage, serviceImplPackage, serviceImplOutputDir, modelPackage).setImplName("provider").generate(); } } -```` \ No newline at end of file +```` diff --git a/doc/docs/communication.md b/doc/docs/communication.md index 6242f8b143fc9c3e97e53f4200e72bb9542c5efd..bb1d13fec172fba29d09e76418bcf55af4422579 100644 --- a/doc/docs/communication.md +++ b/doc/docs/communication.md @@ -8,5 +8,5 @@ - 微信群 - ![](./imgs/jboot-wechat-group.png) + ![](./static/images/jboot-wechat-group.png) \ No newline at end of file diff --git a/doc/docs/config.md b/doc/docs/config.md index f34be1a95295aa8d41c3aeb42ef39fda6f55aaf9..c20efc35fa5d0fb35d807a0c9ba490ab5fabaa8a 100644 --- a/doc/docs/config.md +++ b/doc/docs/config.md @@ -7,10 +7,12 @@ - 描述 - 读取配置 - 注入配置 +- 动态配置 - 注解配置 - 配置实体类 +- 开启 Nacos 分布式配置中心 +- 开启 Apollo 分布式配置中心 - 配置内容加密解密 -- 设计原因 - 常见问题 - Jboot所有配置参考 @@ -19,12 +21,17 @@ 在 Jboot 应用中,可以通过几下几种方式给 Jboot 应用进行配置。 - jboot.properties 配置文件 +- jboot-xxx.properties 配置文件 - 环境变量 - Jvm 系统属性 - 启动参数 +- 分布式配置中心(目前支持 Apollo 和 Nacos) -> 注意:如果同一个属性被多处配置,那么 Jboot 读取配置的优先顺序是: -> `启动参数` > `Jvm 系统属性` > `环境变量` > `jboot.properties 配置` +> 注意: +> 1、如果同一个属性被多处配置,那么 Jboot 读取配置的优先顺序是: +> `分布式配置中心` > `启动参数` > `Jvm 系统属性` > `环境变量` > `jboot-xxx.properties` > `jboot.properties`。 +> +> 2、jboot-xxx.properties 的含义是:当配置 jboot.app.mode=dev 时,默认去读取 jboot-dev.properties,同理当配置 jboot.app.mode=product 时,默认去读取 jboot-product.properties,jboot-xxx.properties 的文件名称是来源于 jboot.app.mode 的配置。jboot-xxx.properties 这个文件并不是必须的,但当该配置文件存在时,其优读取顺序先于 jboot.properties。 @@ -58,6 +65,39 @@ public class AopController extends JbootController { } ``` +## 动态配置 +在 Jboot 的所有配置中,我们可以通过 ${key} 来指定替换为 value。 + +示例1: + +```xml +key1 = value1 +key2 = ${key1}/abc +``` + +那么读取到的 key2 的值为 `value1/abc`。 + +示例2: + +```xml +key1 = value1 +key2 = ${key1}/abc +key3 = abc/${key2}/xyz +``` +那么,key2 的值为 `value1/abc` ,key3 的值为 `abc/value1/abc/xyz` + + +示例2: + +```xml +key1 = value1 +key2 = ${otherkey}/abc +``` +那么,因为系统中找不到 otherkey 的值,key2 的值为 `/abc`,如果我们在系统中,通过 `java -jar xxx.jar --otherkey=othervalue`, +那么, key2 的值为 `othervalue/abc` + + + ## 注解配置 在应用开发中,我们通常会使用注解,Jboot 内置了多个注解。 @@ -86,12 +126,13 @@ public class UserServiceProvider extends UserService{ } ``` -但是,无论是 `@RequestMapping("/user")` 或者是 `@RPCBean(group="myGroup",version="myVersion",port=...)` , 其参数配置都是固定的,因此,Jboot 提供了一种动态的配置方法,可以用于读取配置文件的内容。 +但是,无论是 `@RequestMapping("/user")` 或者是 `@RPCBean(group="myGroup",version="myVersion",port=...)` , +其参数配置都是固定的,因此,Jboot 提供了一种动态的配置方法,可以用于读取配置文件的内容。 例如: ```java -@RequestMapping("${user.mapping}") +@RequestMapping("${user.mapping}/abc") public class UserController extends Controller{ //.... } @@ -106,7 +147,7 @@ user.mapping = /user 其作用是等效于: ```java -@RequestMapping("/user") +@RequestMapping("/use/abcr") public class UserController extends Controller{ //.... } @@ -144,7 +185,7 @@ public class Component1Config{ private String password; private long timeout; - // 下方应该还有 getter setter, 略 + // 下方应还有 getter setter, 此处略 } ``` @@ -158,15 +199,137 @@ Component1Config config = Jboot.config(Component1Config.class); ``` -> 备注:`@ConfigModel(prefix="component1")` 注解的含义是 `Component1Config` 的前缀是 `component1` ,因此,其属性 `host` 是来至配置文件的 `component1.host` 的值。 +配置内如如下: + +```properties +component1.host = xxx +component1.port = xxx +component1.accout = xxx +component1.password = xxx +component1.timeout = xxx +``` + +> 备注:`@ConfigModel(prefix="component1")` 注解的含义是 `Component1Config` 的前缀是 `component1` ,因此,其属性 `host` 是来至配置文件的 `component1.host` 的值。 + + + + +## 开启 Nacos 分布式配置中心 + +**第一步,添加 nacos 客户端的 Maven 依赖** + +```xml + + com.alibaba.nacos + nacos-client + 1.3.2 + +``` + + + +**第二步:启动 nacos** + +- Clone Nacos 项目 + +``` +git clone https://github.com/nacos-group/nacos-docker.git +cd nacos-docker +``` + +单机模式 Derby +``` +docker-compose -f example/standalone-derby.yaml up +``` + +单机模式 Mysql +``` +docker-compose -f example/standalone-mysql.yaml up +``` + +集群模式 +``` +docker-compose -f example/cluster-hostname.yaml up +``` + +Nacos 控制台 + +link:http://127.0.0.1:8848/nacos/ + +nacos 的相关文档在 + +https://nacos.io/zh-cn/docs/quick-start.html + +或者 + +https://nacos.io/zh-cn/docs/quick-start-docker.html + + +**第三步,在 jboot.properties 添加如下配置** + +```properties +jboot.config.nacos.enable = true +jboot.config.nacos.serverAddr = 127.0.0.1:8848 +jboot.config.nacos.dataId = jboot +jboot.config.nacos.group = jboot +``` + +nacos 支持如下更多配置,但是只需要以上配置就可以正常运行。 + + +```properties +jboot.config.nacos.isUseCloudNamespaceParsing = xxx +jboot.config.nacos.isUseEndpointParsingRule = xxx +jboot.config.nacos.endpoint = xxx +jboot.config.nacos.endpointPort = xxx +jboot.config.nacos.namespace = xxx +jboot.config.nacos.username = xxx +jboot.config.nacos.password = xxx +jboot.config.nacos.accessKey = xxx +jboot.config.nacos.secretKey = xxx +jboot.config.nacos.ramRoleName = xxx +jboot.config.nacos.serverAddr = xxx +jboot.config.nacos.contextPath = xxx +jboot.config.nacos.clusterName = xxx +jboot.config.nacos.encode = xxx +jboot.config.nacos.configLongPollTimeout = xxx +jboot.config.nacos.configRetryTime = xxx +jboot.config.nacos.maxRetry = xxx +jboot.config.nacos.enableRemoteSyncConfig = xxx +``` + +## 开启 Apollo 分布式配置中心 + +**第一步:添加 Apollo 客户端的 Maven 依赖** + +```xml + + com.ctrip.framework.apollo + apollo-client + 1.7.0 + +``` + +**第二步,启动 Apollo** + +相关文档在 https://github.com/ctripcorp/apollo/wiki/Quick-Start + +**第三步,在 jboot.properties 添加如下配置** + +``` +jboot.config.apollo.enable = true +jboot.config.apollo.appId = SampleApp +jboot.config.apollo.meta = http://106.54.227.205:8080 +``` + ## 配置内容加密解密 -为了安全起见,很多时候我们需要对配置里的一些安全和隐私内容进行加密,比如数据库的账号密码等,防止web服务器被黑客入侵时保证数据库的安全。 +为了安全起见,我们需要对配置里的一些内容进行加密,比如数据库的账号、密码等,防止 web 服务器被黑客入侵时保证数据库的安全。 配置的内容加密是由用户自己编写加密算法。此时,Jboot 读取的只是加密的内容,为了能正常还原解密之后的内容,用户需要给 `JbootConfigManager` 配置上解密的实现 JbootConfigDecryptor。 -一般情况下,我们需要在 JbootAppListener 的 onInit() 里去配置。例如: +一般情况下,我们需要在 JbootAppListener 的 `onInit()` 里去配置。例如: ```java public MyApplicationListener implements JbootAppListener { @@ -191,11 +354,6 @@ public MyConfigDecriptor implements JbootConfigDecryptor { } ``` -## 设计原因 - -由于 Jboot 定位是微服务框架,同时 Jboot 假设:基于 Jboot 开发的应用部署在 Docker 之上。 - -因此,在做 Devops 的时候,编排工具(例如:k8s、mesos)会去修改应用的相关配置,而通过环境变量和启动配置,无疑是最方便快捷的。 ## 常见问题 @@ -208,12 +366,19 @@ public MyConfigDecriptor implements JbootConfigDecryptor { > 答:和启动参数一样,只需要把 `--` 换成 `-D`,例如: java -jar -Dundertow.port=8080 -Dundertow.host=0.0.0.0 -2、如何设置系统环境变量 ? +3、如何设置系统环境变量 ? > 答:在 Docker 下,启动 Docker 容器的时候,只需要添加 -e 参数即可,例如: `docker run -e undertow.port=8080 xxxx` > Linux、Window、Mac 搜索引擎自行搜索关键字: `环境变量配置` +注意:在设置的系统环境变量的key、value中,例如:jboot.app.mode = dev 可以修改为 JBOOT_APP_MODE = dev ,其他同理把全部小写 +修改为大写,符号点(.)修改为下划线(_)。 + + +## RPC 配置 + +参考 [这里](./rpc.md) -## Jboot 所有配置参考 +## Jboot 其他配置参考 ``` undertow.devMode=true # 设置undertow为开发模式 @@ -267,7 +432,7 @@ jboot.datasource.type jboot.datasource.url jboot.datasource.user jboot.datasource.password -jboot.datasource.driverClassName = "com.mysql.jdbc.Driver" +jboot.datasource.driverClassName = com.mysql.jdbc.Driver jboot.datasource.connectionInitSql jboot.datasource.poolName jboot.datasource.cachePrepStmts = true @@ -291,66 +456,6 @@ jboot.datasource.activeRecordPluginClass jboot.datasource.needAddMapping = true //是否需要添加到映射,当不添加映射的时候,只能通过 model.use("xxx").save()这种方式去调用该数据源 -jboot.rpc.type -jboot.rpc.callMode -jboot.rpc.requestTimeOut -jboot.rpc.registryType -jboot.rpc.registryAddress -jboot.rpc.registryName -jboot.rpc.registryUserName -jboot.rpc.registryPassword -jboot.rpc.registryFile -jboot.rpc.registryCheck -jboot.rpc.consumerCheck -jboot.rpc.providerCheck -jboot.rpc.directUrl -jboot.rpc.host -jboot.rpc.defaultPort -jboot.rpc.defaultGroup -jboot.rpc.defaultVersion -jboot.rpc.proxy -jboot.rpc.filter -jboot.rpc.serialization -jboot.rpc.retries -jboot.rpc.autoExportEnable - -jboot.rpc.dubbo.protocolName -jboot.rpc.dubbo.protocolServer -jboot.rpc.dubbo.protocolContextPath -jboot.rpc.dubbo.protocolTransporter -jboot.rpc.dubbo.protocolThreads -jboot.rpc.dubbo.protocolHost -jboot.rpc.dubbo.protocolPort -jboot.rpc.dubbo.protocolContextpath -jboot.rpc.dubbo.protocolThreadpool -jboot.rpc.dubbo.protocolIothreads -jboot.rpc.dubbo.protocolQueues -jboot.rpc.dubbo.protocolAccepts -jboot.rpc.dubbo.protocolCodec -jboot.rpc.dubbo.protocolSerialization -jboot.rpc.dubbo.protocolCharset -jboot.rpc.dubbo.protocolPayload -jboot.rpc.dubbo.protocolBuffer -jboot.rpc.dubbo.protocolHeartbeat -jboot.rpc.dubbo.protocolAccesslog -jboot.rpc.dubbo.protocolExchanger -jboot.rpc.dubbo.protocolDispatcher -jboot.rpc.dubbo.protocolNetworker -jboot.rpc.dubbo.protocolClient -jboot.rpc.dubbo.protocolTelnet -jboot.rpc.dubbo.protocolPrompt -jboot.rpc.dubbo.protocolStatus -jboot.rpc.dubbo.protocolRegister -jboot.rpc.dubbo.protocolKeepAlive -jboot.rpc.dubbo.protocolOptimizer -jboot.rpc.dubbo.protocolExtension -jboot.rpc.dubbo.protocolIsDefault -jboot.rpc.dubbo.qosEnable -jboot.rpc.dubbo.qosPort -jboot.rpc.dubbo.qosAcceptForeignIp - -jboot.rpc.zbus.serviceName -jboot.rpc.zbus.serviceToken jboot.mq.type jboot.mq.channel diff --git a/doc/docs/db.md b/doc/docs/db.md index 4b86e87ab6f3eb042b748038e97621b4991e286e..1fb31814224f8d40ed8b612130a5767ad5d54cc7 100644 --- a/doc/docs/db.md +++ b/doc/docs/db.md @@ -8,11 +8,12 @@ Jboot 数据库功能基于 JFinal 的 ActiveRecordPlugin 插件和 Apache shard - 基本增删改查 - Db + Record 模式 - Model 映射方式 - - Column 查询方式 + - Columns 查询方式 - 关联查询 - 分页查询 -- 批量插入 +- 一对一、一对多、多对一、多对多 - 事务操作 +- 多数据源 - 读写分离 - 分库分表 - 分布式事务 @@ -25,10 +26,73 @@ Jboot 的数据库是依赖 JFinal 的 ORM 做基本的数据库操作,同时 - Apache Sharding-Sphere 文档:http://shardingsphere.io/document/current/cn/overview/ - Seata 的帮助文档:https://github.com/seata/seata/wiki/Home_Chinese +## 配置 + +基本配置 + +```properties +jboot.datasource.type=mysql +jboot.datasource.url=jdbc:mysql://127.0.0.1:3306/dbname +jboot.datasource.user=root +jboot.datasource.password=123456 +``` + +更多配置 +```properties +# 数据源名称 +jboot.datasource.name +# 数据源类型,支持 mysql oracle sqlserver sqlite ansisql postgresql clickhouse 等 +jboot.datasource.type +# 链接url +jboot.datasource.url +# 数据库登录账号 +jboot.datasource.user +#数据库登录密码 +jboot.datasource.password +#数据源驱动,一般只需要配置 type,程序自动会寻找适合的驱动 +jboot.datasource.driverClassName = com.mysql.jdbc.Driver +jboot.datasource.connectionInitSql +jboot.datasource.poolName +jboot.datasource.cachePrepStmts = true +jboot.datasource.prepStmtCacheSize = 500 +jboot.datasource.prepStmtCacheSqlLimit = 2048 +jboot.datasource.maximumPoolSize = 10 +jboot.datasource.maxLifetime +jboot.datasource.idleTimeout +jboot.datasource.minimumIdle = 0 + +#sql 模板文件存放路径 +jboot.datasource.sqlTemplatePath +# sql 模板文件的名称,多个模板文件使用英文都还隔开 +jboot.datasource.sqlTemplate + +#数据源创建的工厂类 DataSourceFactory +jboot.datasource.factory +#分布式分库分表的 sharding 配置文件 +jboot.datasource.shardingConfigYaml +#jfinal 的 dbPro 的创建工厂类,该类需要实现 IDbProFactory 接口 +jboot.datasource.dbProFactory +#jfinal ActiveRecordPlugin 插件里需要配置的 containerFactory,填写类名 +jboot.datasource.containerFactory +#jfinal 默认配置的事务隔离级别 +jboot.datasource.transactionLevel +# 此数据源包含哪些表 +jboot.datasource.table +# 该数据源排除哪些表 +jboot.datasource.exTable +# 数据方言的 class +jboot.datasource.dialectClass +# 自定义的 activeRecordPlugin 类,该类需要继承 ActiveRecordPlugin +jboot.datasource.activeRecordPluginClass +# 是否需要添加到映射,当不添加映射的时候,只能通过 model.use("xxx").save()这种方式去调用该数据源 +jboot.datasource.needAddMapping = true +``` + ## 基本增删改查 JFinal 操作数据库,提供了两种方式对数据库进行操作,他们分别是: + - Db + Record 方式 - Model 映射方式 @@ -38,9 +102,9 @@ JFinal 操作数据库,提供了两种方式对数据库进行操作,他们 参考 JFinal 的文档:https://jfinal.com/doc/5-5 -### Model 映射方式 +### 使用 @Table 注解来玩喷子和 Model 映射 -Model是 MVC 模式中的 M 部分。以下是 Model 定义示例代码: +Model 是 MVC 模式中的 M 部分。以下是 Model 定义示例代码: ```java @Table(tableName = "user", primaryKey = "id") @@ -68,8 +132,9 @@ public abstract class BaseUser> extends JbootModel impl ``` 需要注意的是: + - 以上的 `User` 和 `BaseUser` 都是通过代码生成器自动生成的,无需手写。 -- 多次执行代码生成器,`User` 代码不会被覆盖,但是 `BaseUser` 会被重新覆盖,因此,请不要在 `BaseUser` 手写任何代码。 +- 多次执行代码生成器,`User` 代码不会被覆盖,但是 `BaseUser` 会被重新覆盖,因此,请不要在 `BaseUser` 手写任何代码。 一般情况下,在正式的项目里,代码分层还需要 `Service` 层来对业务逻辑进行处理。 @@ -123,18 +188,6 @@ CREATE TABLE `user` ( `email` varchar(64) DEFAULT NULL COMMENT '邮件', `mobile` varchar(32) DEFAULT NULL COMMENT '手机电话', `gender` varchar(16) DEFAULT NULL COMMENT '性别', - `signature` varchar(2048) DEFAULT NULL COMMENT '签名', - `birthday` datetime DEFAULT NULL COMMENT '生日', - `company` varchar(256) DEFAULT NULL COMMENT '公司', - `occupation` varchar(256) DEFAULT NULL COMMENT '职位、职业', - `address` varchar(256) DEFAULT NULL COMMENT '地址', - `zipcode` varchar(128) DEFAULT NULL COMMENT '邮政编码', - `site` varchar(256) DEFAULT NULL COMMENT '个人网址', - `graduateschool` varchar(256) DEFAULT NULL COMMENT '毕业学校', - `education` varchar(256) DEFAULT NULL COMMENT '学历', - `avatar` varchar(256) DEFAULT NULL COMMENT '头像', - `idcardtype` varchar(128) DEFAULT NULL COMMENT '证件类型:身份证 护照 军官证等', - `idcard` varchar(128) DEFAULT NULL COMMENT '证件号码', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表,保存用户信息。'; ``` @@ -168,6 +221,584 @@ public List findListBy(int userAge,String articleTitle){ } ``` + + +## 一对一、一对多、多对一、多对多 + +在 Jboot 中,提供了 Join 系列方法,我们在 Service 层可以直接使用 Join 进行 一对一、一对多、多对一、多对多 的查询操作。 + +假设存在这样的关系:一篇文章只有一个作者,一个作者可以写多篇文章,一篇文章可以归属多个文章分类、一个文章分类有可以包含多篇文章。 + + + +那么,表结构设计如下: + + + +文章表 article : + +```sql +CREATE TABLE `article` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `author_id` int(11) unsigned DEFAULT NULL COMMENT '文章作者ID', + `title` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文章标题', + `content` text COLLATE utf8mb4_unicode_ci COMMENT '文章内容', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + + + +文章作者表 author : + +```sql +CREATE TABLE `author` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `nickname` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '作者昵称', + `email` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '作者邮件', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + + + +文章分类表 category : + +```sql +CREATE TABLE `category` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `title` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '分类标题', + `description` text COLLATE utf8mb4_unicode_ci COMMENT '分类描述', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + + + +文章分类和分类的 多对多关系表: article_category: + +```sql +CREATE TABLE `article_category` ( + `article_id` int(11) unsigned NOT NULL COMMENT '文章ID', + `category_id` int(11) unsigned DEFAULT NULL COMMENT '分类ID' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + + + +数据内容如下: + + + +```sql +INSERT INTO `article` (`id`, `author_id`, `title`, `content`) +VALUES + (1,1,'文章1','内容111'), + (2,1,'文章2','内容2222'), + (3,2,'文章3','内容333'), + (4,2,'文章4','内容444'); +``` +有 4 篇文章。 + + +```sql +INSERT INTO `author` (`id`, `nickname`, `email`) +VALUES + (1,'孙悟空','swk@gmail.com'), + (2,'猪八戒','zbj@gmail.com'); +``` +有 2 个作者。 + +```sql +INSERT INTO `category` (`id`, `title`, `description`) +VALUES + (1,'文章分类1','文章分类描述111'), + (2,'文章分类2','文章分类描述222'); +``` +有 2 个文章分类。 + + +```sql +INSERT INTO `article_category` (`article_id`, `category_id`) +VALUES + (1,1), + (1,2), + (2,2), + (3,1), + (3,2), + (4,1); +``` +文章 1 有两个分类;文章 2 有一个分类; 文章 3 有两个分类; 文章 4 有一个分类。 + + + +这样,在我们的代码里应该存在 3 个Service,分别是: + +- ArticleService 用于查询文章相关的服务 +- CategoryService 用于查询文章分类相关的服务 +- AuthorService 用于查询作者的服务 + + + +ArticleService 代码如下: + +```java +public class ArticleService extends JbootServiceBase
{ + + @Inject + private AuthorService authorService; + + @Inject + private CategoryService categoryService; + + /** + * 查询出文章列表,并自动查询其归属的多个分类,已经文章的作者 + * @return + */ + public List
findListWithAuthorAndCategories(){ + List
articles = DAO.findAll(); + + // 查询出每篇文章的作者,关系字段的字段名称 + authorService.join(articles,"author_id"); + + // 查询文章的所属分类,三个参数分别是:中间表名称,article表对应的字段,分类表对应的字段 + categoryService.joinManyByTable(articles,"article_category","article_id","category_id"); + + return articles; + } +} +``` + +ArticleService 输出的 Json 内容如下: + +```json +[ + { + "id":1, + "authorId":1, + "title":"文章1", + "content":"内容111", + "author":{ + "nickname":"孙悟空", + "id":1, + "email":"swk@gmail.com" + }, + "categoryList":[ + { + "description":"文章分类描述111", + "id":1, + "title":"文章分类1" + }, + { + "description":"文章分类描述222", + "id":2, + "title":"文章分类2" + } + ] + }, + { + "id":2, + "authorId":1, + "title":"文章2", + "content":"内容2222", + "author":{ + "nickname":"孙悟空", + "id":1, + "email":"swk@gmail.com" + }, + "categoryList":[ + { + "description":"文章分类描述222", + "id":2, + "title":"文章分类2" + } + ] + }, + { + "id":3, + "authorId":2, + "title":"文章3", + "content":"内容333", + "author":{ + "nickname":"猪八戒", + "id":2, + "email":"zbj@gmail.com" + }, + "categoryList":[ + { + "description":"文章分类描述111", + "id":1, + "title":"文章分类1" + }, + { + "description":"文章分类描述222", + "id":2, + "title":"文章分类2" + } + ] + }, + { + "id":4, + "authorId":2, + "title":"文章4", + "content":"内容444", + "author":{ + "nickname":"猪八戒", + "id":2, + "email":"zbj@gmail.com" + }, + "categoryList":[ + { + "description":"文章分类描述111", + "id":1, + "title":"文章分类1" + } + ] + } +] +``` + + + + +AuthorService 代码如下: + +```java +public class AuthorService extends JbootServiceBase { + + @Inject + private ArticleService articleService; + + public List findListWithArticles(){ + List authors = DAO.findAll(); + + //查询每个作者的所有文章,第二个参数是关系字段的名称 + articleService.joinMany(authors,"author_id"); + + return authors; + } +} +``` + +AuthorService 输出的 Json 内容如下: + +```json +[ + { + "id":1, + "nickname":"孙悟空", + "email":"swk@gmail.com", + "articleList":[ + { + "id":1, + "authorId":1, + "title":"文章1", + "content":"内容111" + }, + { + "id":2, + "authorId":1, + "title":"文章2", + "content":"内容2222" + } + ] + }, + { + "id":2, + "nickname":"猪八戒", + "email":"zbj@gmail.com", + "articleList":[ + { + "id":3, + "authorId":2, + "title":"文章3", + "content":"内容333" + }, + { + "id":4, + "authorId":2, + "title":"文章4", + "content":"内容444" + } + ] + } +] +``` + + + + +CategoryService 代码如下: + +```java +public class CategoryService extends JbootServiceBase { + + @Inject + private ArticleService articleService; + + public List findListWithArticles(){ + List categories = DAO.findAll(); + + //查询每个分类的所有文章 + articleService.joinManyByTable(categories,"article_category","category_id","article_id"); + + return categories; + } +} +``` + +CategoryService 输出的 json 内容如下: + +```json +[ + { + "articleList":[ + { + "id":1, + "authorId":1, + "title":"文章1", + "content":"内容111" + }, + { + "id":3, + "authorId":2, + "title":"文章3", + "content":"内容333" + }, + { + "id":4, + "authorId":2, + "title":"文章4", + "content":"内容444" + } + ], + "description":"文章分类描述111", + "id":1, + "title":"文章分类1" + }, + { + "articleList":[ + { + "id":1, + "authorId":1, + "title":"文章1", + "content":"内容111" + }, + { + "id":2, + "authorId":1, + "title":"文章2", + "content":"内容2222" + }, + { + "id":3, + "authorId":2, + "title":"文章3", + "content":"内容333" + } + ], + "description":"文章分类描述222", + "id":2, + "title":"文章分类2" + } +] +``` + + + +具体代码参考:[这个链接](https://gitee.com/JbootProjects/jboot/tree/master/src/test/java/io/jboot/test/join) + + + + + +## 多数据源 + +默认单数据源的情况下,我们需要在 `jboot.properties` 添加如下配置: + +``` +jboot.datasource.type=mysql +jboot.datasource.url=jdbc:mysql://127.0.0.1:3306/jbootdemo +jboot.datasource.user=root +jboot.datasource.password=your_password +``` + +如果是多个数据源,我们可以在 `jboot.datasource.` 后面添加数据源的名称,例如: + +``` +jboot.datasource.a1.type=mysql +jboot.datasource.a1.url=jdbc:mysql://127.0.0.1:3306/jboot1 +jboot.datasource.a1.user=root +jboot.datasource.a1.password=your_password + +jboot.datasource.a2.type=mysql +jboot.datasource.a2.url=jdbc:mysql://127.0.0.1:3306/jboot2 +jboot.datasource.a2.user=root +jboot.datasource.a2.password=your_password +``` + +这表示,我们又增加了数据源 `a1` 和数据源 `a2`,在使用的时候,我们只需要做以下使用: + +```java +Company company = new Company(); +company.setId(1); +company.setName("name"); + +company.use("a1").save(); +``` + +`company.use("a1").save();` 表示使用数据源 `a1` 进行保存。 + + +**需要注意的是:** + +在多数据源应用中,很多时候,我们的一个 Model 只有对应一个数据源,而不是一个 Model 对应多个数据源。假设 `company` 只有在 `a1` 数据源中存在,在其他数据源并不存在,我们需要把 `a1` 数据源的配置修改如下: + +``` +jboot.datasource.a1.type=mysql +jboot.datasource.a1.url=jdbc:mysql://127.0.0.1:3306/jboot1 +jboot.datasource.a1.user=root +jboot.datasource.a1.password=your_password +jboot.datasource.a1.table=company + +jboot.datasource.a2.type=mysql +jboot.datasource.a2.url=jdbc:mysql://127.0.0.1:3306/jboot2 +jboot.datasource.a2.user=root +jboot.datasource.a2.password=your_password +jboot.datasource.a2.table=user,xxx (其他非company表) +``` + +这样,`company` 在 `a1` 数据源中存在,Jboot在初始化的时候,并不会去检查 `company` 在其他数据源中是否存在,同时,代码操作 `company` 的时候,不再需要 `use()` ,代码如下: + + +```java +Company company = new Company(); +company.setCid("1"); +company.setName("name"); + +//company.use("a1").save(); +company.save(); +``` + +代码中不再需要 `use("a1")` 指定数据源,因为 `company` 表只有一个数据源。 + +## 事务以及 @Transactional 注解 + +在 JFinal 中提供了一种使用 Db.tx(...) 的操作方式,具体参考:https://jfinal.com/doc/5-7 ,同时,Jboot 也提供了一个用于进行事务管理的注解 +@Transactional 。具体用法如下: + +实例1:出现异常时回滚 +```java +@Transactional +public void test1() { + //your code + throw new RuntimeException("..."); +} +``` + +实例2:返回值为 false 时回滚 +```java +@Transactional(rollbackForFalse=true) +public boolean test2() { + //your code + return false; +} +``` + +实例3:返回值为 Ret.fail() 时回滚 +```java +@Transactional(rollbackForRetFail=true) +public Ret test3() { + //your code + return Ret.fail("message..."); +} +``` + +实例4:返回值为 null 时回滚 +```java +@Transactional(rollbackForNull=true) +public Ret test3() { + //your code + return null; +} +``` + +@Transactional 的更多配置: +```java +public @interface Transactional { + + /** + * 使用哪个数据源 + * + * @return + */ + String config() default ""; + + /** + * 事务隔离级别 + * + * @return + */ + int transactionLevel() default -1; + + /** + * return false 的时候,是否进行回滚 + * + * @return + */ + boolean rollbackForFalse() default false; + + /** + * return ret.fail 的时候,是否进行回滚 + * + * @return + */ + boolean rollbackForRetFail() default false; + + + /** + * 返回 null 的时候,是否进行回滚 + * + * @return + */ + boolean rollbackForNull() default false; + + + /** + * 配置允许哪些异常不回滚 + * + * @return + */ + Class[] noRollbackFor() default {}; + + /** + * 是否在新的线程里执行,在 Controller 的 Action 方法下配置无效 + * 在 Controller 里,不能以新的线程在运行 + * + * @return + */ + boolean inNewThread() default false; + + /** + * 使用哪个线程池来运行线程,需要在启动的时候,通过 TransactionalManager 来配置线程池及其名称 + * + * @return + */ + String threadPoolName() default ""; + + /** + * 是否以阻塞的方式运行线程,这个配置只有在返回值 void 情况下配置生效 + * 有返回值的,此配置无效,默认都是阻塞运行线程的方式运行 + * + * @return + */ + boolean threadWithBlocked() default false; + +} +``` + + + ## 读写分离 在 Jboot 应用中,读写分离建议使用两个数据源,分别是读的数据源和写的数据源,写的数据源必须支持可读可写。 @@ -178,7 +809,7 @@ public List findListBy(int userAge,String articleTitle){ Jboot 的分库分表功能使用了 Sharding-jdbc 实现的,若在 Jboot 应用在需要用到分库分表功能,需要添加 `jboot.datasource.shardingConfigYaml = xxx.yaml ` 的配置,其中 `xxx.yaml` 配置需要放在 classpath 目录下,配置内容参考:https://shardingsphere.apache.org/document/current/cn/manual/sharding-jdbc/configuration/config-yaml/ -**注意:** 当在 `jboot.properties` 文件配置 `jboot.datasource.shardingConfigYaml = xxx.yaml`之后,不再需要在 `jboot.properties` 配置 `jboot.datasource.url` 、 `jboot.datasource.user` 和 `jboot.datasource.password` 等,这些配置都转义到 `xxx.yaml` 里进行配置了。 +**注意:** 当在 `jboot.properties` 文件配置 `jboot.datasource.shardingConfigYaml = xxx.yaml`之后,不再需要在 `jboot.properties` 配置 `jboot.datasource.url` 、 `jboot.datasource.user` 和 `jboot.datasource.password` 等,这些配置都转移到 `xxx.yaml` 里进行配置了。 ## 分布式事务 @@ -429,4 +1060,4 @@ support { > 1、jboot.seata.txServiceGroup 配置的值要注意和 file.conf 里的 vgroup_mapping.xxx 保持一致 > 2、jboot.rpc.filter=seata ##seata在Dubbo中的事务传播过滤器 -以上配置完毕后如何使用呢?点击 [这里](../../src/test/java/io/jboot/test/seata) 查看代码实例。 \ No newline at end of file +以上配置完毕后如何使用呢?点击 [这个链接](https://gitee.com/JbootProjects/jboot/tree/master/src/test/java/io/jboot/test/seata) 查看代码实例。 \ No newline at end of file diff --git a/doc/docs/deploy.md b/doc/docs/deploy.md index 6486679fcdfb59d1f45040b72a0ac510c9fd8daf..d91d14be8c66973560e0969164e4a98d953f6237 100644 --- a/doc/docs/deploy.md +++ b/doc/docs/deploy.md @@ -2,15 +2,101 @@ ## 目录 - 描述 +- 通过 脚本 运行 - 通过 Jar 运行 - 通过 Tomcat 运行 ## 描述 +本文档提供了 3 种部署方式,对应 Jboot 里的 3 种[打包方式](./build.md)。 + +## 通过 脚本 运行 + +在 [打包方式](./build.md) 文档中,我们可以把项目打包成一个 .zip 的压缩包项目,里面带有 jboot.sh (和 jboot.bat) 执行脚本, +只需要我们解压 .zip 压缩文件,通过如下命令就可以对 jboot 项目进行启动和停止。 + +```shell +# 启动 +./jboot.sh start + +# 停止 +./jboot.sh stop + +# 重启 +./jboot.sh restart +``` + +在 Windows 系统中,通过如下命令执行 + +```shell +# 启动 +jboot.bat start + +# 停止 +jboot.bat stop + +# 重启 +jboot.bat restart +``` ## 通过 Jar 运行 -Jboot 通过依赖 `JFinal-Undertow` 内置了 Undertow 服务器,可以直接通过 Jar 的方式进行运行,这部分直接参考文档:https://www.jfinal.com/doc/1-3 即可。 +在 [打包方式](./build.md) 文档中,我们可以把所有的资源文件(html、css、js、配置文件 等)以及项目的所有依赖打包到一个 jar 包 +里去,打包成功后,可以通过如下命令运行。 + +启动(前台启动,命令窗口不能关闭) +```shell +java -jar xxx.jar +``` +> 当前ssh窗口(命令窗口)被锁定,可按 `CTRL + C` 打断程序运行,或直接关闭窗口,程序退出。 + +启动(后台启动,命令窗口不能关闭) +```shell +java -jar xxx.jar & +``` +> & 代表在后台运行。当前ssh窗口不被锁定,但是当窗口关闭时,程序中止运行。 + + +启动(后台启动) +```shell +nohup java -jar xxx.jar & +``` +>nohup 意思是不挂断运行命令,当账户退出或终端关闭时,程序仍然运行,当用 nohup 命令执行作业时,缺省情况下该作业的所有输出被重定向到nohup.out的文件中,除非另外指定了输出文件。 + +启动(后台启动) +```shell +nohup java -jar xxx.jar>temp.txt & +``` +>command >out.file 是将 command 的输出重定向到 out.file 文件,即输出内容不打印到屏幕上,而是输出到 out.file 文件中。 +>以上命令,就是把 java 启动的输出,输入到 temp.txt 文件里,而不是在屏幕上。 + + +另外:可以通过如下命令实时查看日志: + +```shell +tail -f temp.text +``` + +可通过jobs命令查看后台运行任务 + +```shell +jobs +``` + +jobs 命令就会列出所有后台执行的作业,并且每个作业前面都有个编号。如果想将某个作业调回前台控制,只需要 fg + 编号即可。 + +```shell +fg 33 +``` + +查看程序端口的进程的pid + +```shell +netstat -nlp | grep:8080 +``` + + + ## 通过 Tomcat 运行 diff --git a/doc/docs/docker.md b/doc/docs/docker.md index 0482cbc30de187aff3c166eb2ab269bfc7f8b05e..f1a1b601ccd8ba638fbb08034b2f11e1f7be029e 100644 --- a/doc/docs/docker.md +++ b/doc/docs/docker.md @@ -6,9 +6,9 @@ 例如,我们需要给应用配置数据库信息: -``` +```shell docker run --e JBOOT_DATASOURCE_URL="jdbc:mysql://127.0.0.1:3306/jpress3" +-e JBOOT_DATASOURCE_URL=jdbc:mysql://127.0.0.1:3306/jpress3 -e JBOOT_DATASOURCE_USER=root -e JBOOT_DATASOURCE_PASSWORD=123456 jpress:v2.0.8 @@ -16,7 +16,7 @@ jpress:v2.0.8 这个启动命令,等同于在 jboot.properties 添加如下的配置 -``` +```properties jboot.datasource.url=jdbc:mysql://127.0.0.1:3306/jpress3 jboot.datasource.user=root jboot.datasource.password=123456 diff --git a/doc/docs/dubbo3-upgrade.md b/doc/docs/dubbo3-upgrade.md new file mode 100644 index 0000000000000000000000000000000000000000..1c0c698b40ceb47161e2aba845142bf41867f9d6 --- /dev/null +++ b/doc/docs/dubbo3-upgrade.md @@ -0,0 +1,45 @@ +# Dubbo2 升级 Dubbo3 文档 + +dubbo2 升级 dubbo3 ,请先阅读 dubbo3 官网的升级文档:https://dubbo.apache.org/zh/docs/migration/migration-service-discovery/ + + +总体上来说,3.x 是完全兼容 2.x 版本的,因此,理论上只需要添加两个配置即可: + +- 1、生产端增加配置: + jboot.rpc.dubbo.registry.registerMode=instance + +- 2、消费端配置: + jboot.rpc.application.service-discovery.migration=FORCE_APPLICATION + + +**以下是升级流程:** + +- 1、 修改工程 pom.xml 中的 dubbo 依赖到最新版本就可以完成升级,其它代码不用动,升级完成后,建议以下参数进行定制调整(2、3步) + +- 2、Provider 端 `jboot.properties` 中 + `jboot.rpc.dubbo.registry.registerMode` 的可选值 interface、instance、all,默认是 all,即接口级地址、应用级地址都注册。 + + - instance: 应用级注册; + - interface:接口级注册; + - all:双注册(默认值) 即接口和应用都同时注册到注册中心中,双注册不可避免的会带来额外的注册中心存储压力, + 建议修改默认值为 instance 模式,这是 dubbo3 推荐方式。 + +- 3、Consumer 端 `jboot.properties` 中 `jboot.rpc.application.service-discovery.migration` +的可选值有: + - FORCE_INTERFACE:只消费接口级地址,如无地址则报错,单订阅 2.x 地址 + - APPLICATION_FIRST:智能决策接口级/应用级地址,双订阅 + - FORCE_APPLICATION:只消费应用级地址,如无地址则报错,单订阅 3.x 地址 +jboot.rpc.dubbo.application.service-discovery.migration=APPLICATION_FIRST + +> 注意事项: +> +> 在 Dubbo3 中,服务端(Provider端)如果采用应用级注册 (instance),注册中心中只会注册应用实例信息,不会注册接口服务信息。接口服务信息存储在元数据中心,在元数据中心存储了实例和接口的映射关系。消费端通过元数据映射关系获取所需要的服务信息。 +> +> 这套流程是 dubbo3 为了兼容 dubbo2 自动来完成的,官方名称为:服务自省。详情: +https://dubbo.apache.org/zh/docs/examples/service-discovery/#%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%8D%E5%8A%A1%E8%87%AA%E7%9C%81 + + +## 参考代码 + +https://gitee.com/JbootProjects/jboot/tree/master/src/test/java/io/jboot/test/rpc/dubbo3 + diff --git a/doc/docs/event.md b/doc/docs/event.md index 4cfb3693a343c7622e81b8615f3f86f8095eba8b..10f800ca445386806a4acdd74c755c786ba271b3 100644 --- a/doc/docs/event.md +++ b/doc/docs/event.md @@ -60,7 +60,8 @@ public class MyEventListener implements JbootEventListener { 此时,当有 `eventName` , `event1` , `event2` 任何一个事件发送的时候,以上的 `MyEventListener.onEvent` 都会被触发执行。 **备注** -事件机制和 MQ(消息队列)很相似,主要区别是在于事件机制只是在 Jvm 应用内执行,解决的是业务直接的耦合问题。 + +事件机制和 MQ(消息队列)很相似,主要区别是在于事件机制只是在 Jvm 应用内执行,解决的业务之间的耦合问题。 MQ(消息队列)解决的是分布式下,多个系统的事件(或者消息)发送和监听,MQ 需要第三方的 **中间件** ,例如:Redis、rabbitMQ、activeMQ...等。 diff --git a/doc/docs/gateway.md b/doc/docs/gateway.md new file mode 100644 index 0000000000000000000000000000000000000000..c1928b712bfc97925bfa832594bb09ee264c79e7 --- /dev/null +++ b/doc/docs/gateway.md @@ -0,0 +1,409 @@ +# 网关 + +## 目录 + +- 概述 +- path路由 +- host路由 +- query路由 +- 多个 Gateway 配置 +- 服务发现 +- 注意事项 + + +## 概述 + +Jboot 已经内置基础的网关,网关功能目前暂时只能通过在 jboot.properties 文件进行配置。 + +如下是一个正常的 gateway 配置。 + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + + +jboot.gateway.uriHealthCheckEnable = true +jboot.gateway.uriHealthCheckPath = /your-health-check-path + +jboot.gateway.sentinelEnable = false +jboot.gateway.sentinelBlockPage = /block +jboot.gateway.sentinelBlockJsonMap = message:xxxx;code:200 + +jboot.gateway.proxyReadTimeout = 10000 +jboot.gateway.proxyConnectTimeout = 5000 +jboot.gateway.proxyContentType = text/html;charset=utf-8 + +jboot.gateway.interceptors = com.xxx.Interceptor1,com.xxx.Interceptor2 +jboot.gateway.loadBalanceStrategy = com.xxx.loadBalanceStrategy1 + +jboot.gateway.pathEquals = /path +jboot.gateway.pathContains = /path +jboot.gateway.pathStartsWith = /path +jboot.gateway.pathEndswith = /path + +jboot.gateway.hostEquals = xxx.com +jboot.gateway.hostContains = xxx.com +jboot.gateway.hostStartsWith = xxx.com +jboot.gateway.hostEndswith = xxx.com + +jboot.gateway.queryEquals = aa:bb,cc:dd +jboot.gateway.queryContains = aa,bb +``` + + + +- name 设置路由的名称 +- uri 设置路由目标网址,可以配置多个 uri,多个 uri 用英文逗号(,) 隔开,当有多个 uri 的时候,系统会 **随机** 使用其中一个去访问 +- enable 是否启用该路由 +- uriHealthCheckEnable 是否启用健康检查功能 +- uriHealthCheckPath URI 健康检查路径,当配置 uriHealthCheckPath 后,健康检查的 url 地址为 uri + uriHealthCheckPath,当健康检查目标网址的 http code 为 200 时,表示健康状态,否则为非健康状态。 +- sentinelEnable 是否启用 sentinel 限流功能 +- sentinelBlockPage 若该路由被限流后,网页自动跳转到哪个网址 +- sentinelBlockJsonMap 若该路由被限流后,自动渲染的 jsonMap,若 sentinelBlockPage 已经配置,则 sentinelBlockJsonMap 配置无效 +- proxyReadTimeout 发生路由后,默认的请求超时时间,默认为 10 秒 +- proxyConnectTimeout 发生路由后,默认的连接超时时间,默认为 5 秒 +- proxyContentType 发生路由后,返回给浏览器的 http-content-type,默认为:text/html;charset=utf-8 +- interceptors 网关拦截器,一般用于进行鉴权等功能,配置类名,多个拦截器用英文逗号隔开,拦截器必须实现 GatewayInterceptor 接口 +- loadBalanceStrategy 负载均衡策略,当配置了多个 uri 的时候,可以通过此策略对 uri 进行获取 + +> 注意:开启健康检查后,当所有的目标地址都不健康的时候,会渲染 "none health url in gateway" 的错误信息。 +> 我们可以通过 `JbootGatewayManager.me().setNoneHealthUrlErrorRender()` 来自定义渲染功能。 + +## Path 路由 + +Path 路由一般是最常用的路由之一,是根据域名之后的路径进行路由的,Jboot 对 Path 路由提供了 4 中方式: + +**1、pathEquals** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.pathEquals = /user +``` + +当用户访问 www.xxx.com/user 的时候,自动路由到 `http://youdomain:8080/user`,但是当用户请求 `www.xxx.com/user/other` 的时候不会进行路由。 + +**2、pathContains** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.pathContains = /user +``` + +当 path 中,只要存在 `/user` 就会匹配到该路由,比如 `www.xxx.com/user/other` 或者 `www.xxx.com/other/user/xxx` 都会匹配到。 + + +**3、pathStartsWith** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.pathStartsWith = /user +``` + +当 path 中,只要以 `/user` 开头就会匹配到该路由,比如 `www.xxx.com/user/other` ,但是 `www.xxx.com/other/user/xxx` 不会匹配到,因为它是以 `/other` 开头的。 + +**4、pathEndsWith** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.pathEndsWith = /user +``` + +当 path 中,只要以 `/user` 结束就会匹配到该路由,比如 `www.xxx.com/other/user` ,但是 `www.xxx.com/user/other` 不会匹配到,因为它是以 `/other` 结束的。 + + +## Host 路由 + +Host 路由是根据域名进行路由的,Jboot 对 Host 路由提供了 4 中方式: + +**1、hostEquals** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.hostEquals = xxx.xxx.com +``` + +当用户访问 xxx.xxx.com/user/xx 的时候,自动路由到 `http://youdomain:8080/user/xx`,但是当用户请求 `www.xxx.com/user/xxx` 的时候不会进行路由。 + +**2、hostContains** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.hostContains = xxx.xxx.com +``` + +在 Host 中,只要存在 `xxx.xxx.com` 就会匹配到该路由,比如 `aaa.bbb.xxx.xxx.com/user/other` 会自动路由到 `http://youdomain:8080/user/other`。 + + +**3、hostStartsWith** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.hostStartsWith = xxx +``` + +在 Host 中,只要以 `xxx` 开头就会匹配到该路由,比如 `xxx.xxx.com/user/other` ,但是 `www.xxx.com/other/user/xxx` 不会匹配到,因为它的域名(host)是以 `xxx` 开头的。 + +**4、hostEndsWith** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.hostEndsWith = com +``` + +在 Host 中,只要以 `com` 结束就会匹配到该路由,比如 `www.xxx.com/other/user` ,但是 `www.xxx.org/user/other` 不会匹配到,因为它的域名是以 `org` 结束的。 + +## Query 路由 + +根据 get 请求的参数进行路由,注意 post 请求参数不会路由。 + + +**1、queryEquals** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.queryEquals = aaa:bbb +``` + +以上配置中,如果用户访问 `www.xxx.com/controller?aaa=bbb` 会自动路由到 `http://youdomain:8080/controller?aaa=bbb` ,但是如果用户访问 `www.xxx.com/controller?aaa=ccc`不会路由。 + +**2、queryContains** + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.queryContains = aaa +``` + +以上配置中,如果用户访问 `www.xxx.com/controller?aaa=bbb` 会自动路由到 `http://youdomain:8080/controller?aaa=bbb` ,或者用户访问 `www.xxx.com/controller?aaa=ccc` 也会路由到 `http://youdomain:8080/controller?aaa=ccc`,因为 query 都包含了 `aaa=**` 的请求,但是如果用户访问 `www.xxx.com/controller?other=aaa`不会路由。 + + + +## 多个 Gateway 配置 + +```properties +jboot.gateway.aaa.name = name +jboot.gateway.aaa.uri = http://youdomain:8080 +jboot.gateway.aaa.enable = true +jboot.gateway.aaa.sentinelEnable = false +jboot.gateway.aaa.sentinelBlockPage = /block +jboot.gateway.aaa.proxyReadTimeout = 10000 +jboot.gateway.aaa.proxyConnectTimeout = 5000 +jboot.gateway.aaa.proxyContentType = text/html;charset=utf-8 +jboot.gateway.aaa.interceptors = com.xxx.Interceptor1,com.xxx.Interceptor2 +jboot.gateway.aaa.pathEquals = /path +jboot.gateway.aaa.pathContains = /path +jboot.gateway.aaa.pathStartsWith = /path +jboot.gateway.aaa.pathEndswith = /path +jboot.gateway.aaa.hostEquals = xxx.com +jboot.gateway.aaa.hostContains = xxx.com +jboot.gateway.aaa.hostStartsWith = xxx.com +jboot.gateway.aaa.hostEndswith = xxx.com +jboot.gateway.aaa.queryEquals = aa:bb,cc:dd +jboot.gateway.aaa.queryContains = aa,bb + + +jboot.gateway.bbb.name = name +jboot.gateway.bbb.uri = http://youdomain:8080 +jboot.gateway.bbb.enable = true +jboot.gateway.bbb.sentinelEnable = false +jboot.gateway.bbb.sentinelBlockPage = /block +jboot.gateway.bbb.proxyReadTimeout = 10000 +jboot.gateway.bbb.proxyConnectTimeout = 5000 +jboot.gateway.bbb.proxyContentType = text/html;charset=utf-8 +jboot.gateway.bbb.interceptors = com.xxx.Interceptor1,com.xxx.Interceptor2 +jboot.gateway.bbb.pathEquals = /path +jboot.gateway.bbb.pathContains = /path +jboot.gateway.bbb.pathStartsWith = /path +jboot.gateway.bbb.pathEndswith = /path +jboot.gateway.bbb.hostEquals = xxx.com +jboot.gateway.bbb.hostContains = xxx.com +jboot.gateway.bbb.hostStartsWith = xxx.com +jboot.gateway.bbb.hostEndswith = xxx.com +jboot.gateway.bbb.queryEquals = aa:bb,cc:dd +jboot.gateway.bbb.queryContains = aa,bb + + +jboot.gateway.xxx.name = name +jboot.gateway.xxx.uri = http://youdomain:8080 +jboot.gateway.xxx.enable = true +jboot.gateway.xxx.sentinelEnable = false +jboot.gateway.xxx.sentinelBlockPage = /block +jboot.gateway.xxx.proxyReadTimeout = 10000 +jboot.gateway.xxx.proxyConnectTimeout = 5000 +jboot.gateway.xxx.proxyContentType = text/html;charset=utf-8 +jboot.gateway.xxx.interceptors = com.xxx.Interceptor1,com.xxx.Interceptor2 +jboot.gateway.xxx.pathEquals = /path +jboot.gateway.xxx.pathContains = /path +jboot.gateway.xxx.pathStartsWith = /path +jboot.gateway.xxx.pathEndswith = /path +jboot.gateway.xxx.hostEquals = xxx.com +jboot.gateway.xxx.hostContains = xxx.com +jboot.gateway.xxx.hostStartsWith = xxx.com +jboot.gateway.xxx.hostEndswith = xxx.com +jboot.gateway.xxx.queryEquals = aa:bb,cc:dd +jboot.gateway.xxx.queryContains = aa,bb +``` + +## 服务发现 + +Jboot Gateway 功能通过 Nacos(可以通过 SPI 进行扩展其他事项方式) 实现了自动发现服务。 + +相关代码示例可以参考: + +https://gitee.com/JbootProjects/jboot/tree/master/simples/gateway + + +使用方法如下: + +1、新增 nacos 依赖(Gateway 端和服务端都需要) + +```xml + + com.alibaba.nacos + nacos-client + 版本号 + +``` + +2、启动 Nacos + +参考文档: + +https://nacos.io/zh-cn/docs/quick-start.html + + +3、在 Gateway 网关端添加如下配置: + +```properties +jboot.gateway.name = myName +jboot.gateway.enable = true +jboot.gateway.pathStartsWith = / + + +jboot.gateway.discovery.enable = true + +#若配置其他,则自行通过 SPI 进行扩展 +jboot.gateway.discovery.type = nacos + +#默认值为:DEFAULT_GROUP +jboot.gateway.discovery.group = + +jboot.gateway.discovery.nacos.serverAddr = 127.0.0.1:8848 +``` + +更多的 nacos 配置如下: +```properties +jboot.gateway.discovery.nacos.isUseCloudNamespaceParsing = xxx +jboot.gateway.discovery.nacos.isUseEndpointParsingRule = xxx +jboot.gateway.discovery.nacos.endpoint = xxx +jboot.gateway.discovery.nacos.endpointPort = xxx +jboot.gateway.discovery.nacos.namespace = xxx +jboot.gateway.discovery.nacos.username = xxx +jboot.gateway.discovery.nacos.password = xxx +jboot.gateway.discovery.nacos.accessKey = xxx +jboot.gateway.discovery.nacos.secretKey = xxx +jboot.gateway.discovery.nacos.ramRoleName = xxx +jboot.gateway.discovery.nacos.contextPath = xxx +jboot.gateway.discovery.nacos.clusterName = xxx +jboot.gateway.discovery.nacos.encode = xxx +jboot.gateway.discovery.nacos.configLongPollTimeout = xxx +jboot.gateway.discovery.nacos.configRetryTime = xxx +jboot.gateway.discovery.nacos.maxRetry = xxx +jboot.gateway.discovery.nacos.enableRemoteSyncConfig = xxx +``` + +4、在 服务端 添加如下配置 + +```properties +jboot.gateway.discovery.enable = true + +#若配置其他,则自行通过 SPI 进行扩展 +jboot.gateway.discovery.type = nacos + +#默认值为:DEFAULT_GROUP,这个值必须和 gateway 配置的一致 +jboot.gateway.discovery.group = + +## 注意:这个配置的 myName 必须和 Gateway 里的 'jboot.gateway.name = myName' 中的 myName 一样 +jboot.gateway.instance.name = myName + +#不配置默认为 http,可以配置为 https +jboot.gateway.instance.uriScheme = http + +#不配置默认为当前服务器的IP地址,可以获取错误 +jboot.gateway.instance.uriHost = + +#默认为 undertow.port 的配置 +jboot.gateway.instance.uriPort = http + +jboot.gateway.instance.uriPath = /user/aaa + +jboot.gateway.discovery.nacos.serverAddr = 127.0.0.1:8848 +``` + +更多的 nacos 配置如下: +```properties +jboot.gateway.discovery.nacos.isUseCloudNamespaceParsing = xxx +jboot.gateway.discovery.nacos.isUseEndpointParsingRule = xxx +jboot.gateway.discovery.nacos.endpoint = xxx +jboot.gateway.discovery.nacos.endpointPort = xxx +jboot.gateway.discovery.nacos.namespace = xxx +jboot.gateway.discovery.nacos.username = xxx +jboot.gateway.discovery.nacos.password = xxx +jboot.gateway.discovery.nacos.accessKey = xxx +jboot.gateway.discovery.nacos.secretKey = xxx +jboot.gateway.discovery.nacos.ramRoleName = xxx +jboot.gateway.discovery.nacos.contextPath = xxx +jboot.gateway.discovery.nacos.clusterName = xxx +jboot.gateway.discovery.nacos.encode = xxx +jboot.gateway.discovery.nacos.configLongPollTimeout = xxx +jboot.gateway.discovery.nacos.configRetryTime = xxx +jboot.gateway.discovery.nacos.maxRetry = xxx +jboot.gateway.discovery.nacos.enableRemoteSyncConfig = xxx +``` + +## 注意事项 +当配置中,如果一个内容存在多个值的时候,需要用英文逗号(,)隔开。 + +比如: + +```properties +jboot.gateway.name = name +jboot.gateway.uri = http://youdomain:8080 +jboot.gateway.enable = true + +jboot.gateway.pathContains = /user,/article +``` + +当 path 中,只要存在 `/user` 或者 存在 `/article` 都会匹配到该路由,比如 `www.xxx.com/user/xxx` 或者 `www.xxx.com/article/xxx` 都会匹配到。 + +其他同理。 \ No newline at end of file diff --git a/doc/docs/hotload.md b/doc/docs/hotload.md index adea77222641c7145cbd675c4141f21415d6e4d5..3a3777342b2959921684f06e686943c0e7ac1cdf 100644 --- a/doc/docs/hotload.md +++ b/doc/docs/hotload.md @@ -5,15 +5,15 @@ ## 目录 - 描述 -- Maven 多模块( module )下资源文件热加载 +- Maven 多模块( module )下资源文件热加载 - 常见错误 ## 描述 -在 Jboot 开发模式下,默认开启热加载功能,若要关闭热加载功能,可以添加配置 `undertow.devMode = false` , 或者通过 `jboot.app.mode = product` 配置修改当前应用为生产环境。 +在 Jboot 开发模式下,可以添加配置 `undertow.devMode = true` 来开启热加载功能。 -## Maven 多模块( module )下资源文件热加载 +## Maven 多模块( module )下资源文件热加载 在 Jboot 中,内置了一个类叫 `JbootResourceLoader`,只有在 dev 模式下才生效。 @@ -21,7 +21,7 @@ 倘若你的资源文件不在 `src/main/webapp` 目录,则需要配置 `jboot.app.resourcePathName = myResourcePath` ,那么,当 Jboot 启动的时候则会启动去监控各个模块的 `src/main/myResourcePath` 目录,并自动同步到 `classpath:myResourcePath` 下。 -默认是 `src/main/webapp`,若你的资源文件在 `src/main/webapp` 目录下,不需要任何配置即可以实现资源文件的热加载功能。 +默认是 `src/main/webapp`,若你的资源文件在 `src/main/webapp` 目录下不需要任何配置即可以实现资源文件的热加载功能。 ## 常见错误 @@ -36,4 +36,4 @@ undertow.hotSwapClassPrefix = xxx1.com, xxx2.com - **错误2:没有热加载** 在 idea 开发工具中,可能会出现未正确进行热加载的情况,一般是没有配置 idea 的自动编译功能。 -![](./imgs/idea-auto-build.jpg) \ No newline at end of file +![](./static/images/idea-auto-build.jpg) \ No newline at end of file diff --git a/doc/docs/imgs/jboot-wechat-group.png b/doc/docs/imgs/jboot-wechat-group.png deleted file mode 100644 index 3d46050d5d6276b97877067ed4e33d62ab57c764..0000000000000000000000000000000000000000 Binary files a/doc/docs/imgs/jboot-wechat-group.png and /dev/null differ diff --git a/doc/docs/install.md b/doc/docs/install.md index eab83655a318aa39ef9896361fa65e3ce90f73f8..fa52c91def1aecd869cd111856f02da459ef79a1 100644 --- a/doc/docs/install.md +++ b/doc/docs/install.md @@ -9,7 +9,7 @@ io.jboot jboot - 3.0.1 + 4.1.0 ``` diff --git a/doc/docs/jfinalConfig.md b/doc/docs/jfinalConfig.md index ff66625118facff2f8c5a31edb59e756931ca3e1..eb5ff5d0c362a6d2df6765ba41e99ec8c4d48745 100644 --- a/doc/docs/jfinalConfig.md +++ b/doc/docs/jfinalConfig.md @@ -1,6 +1,8 @@ -# 如何在Jboot中添加自己的 JFianlConfig +# 如何在Jboot中添加自己的 JFinalConfig -凡是开发过 JFinal 的同学,都知道 JFinalConfig 是 JFinal 的核心配置,详情: https://www.jfinal.com/doc/2-1 ,其内容如下: +[[toc]] + +JFinalConfig 是 JFinal 的核心配置,详情: https://www.jfinal.com/doc/2-1 ,其内容如下: ```java public class DemoConfig extends JFinalConfig { @@ -14,7 +16,7 @@ public class DemoConfig extends JFinalConfig { ``` 默认情况下,我们不需要对 JFinal 进行任何的配置,因为 Jboot 已经对 JFinal 进行了默认的配置,同时,Controller 等的配置完全是通过注解 -@RequestMapping 来配置了,数据库也只是在 jboot.properties 里添加就可以。 +`@RequestMapping` 来配置了,数据库也只是在 jboot.properties 里添加就可以。 但是可能在某些特殊情况下,我们对 JFinal 进行自己特殊的配置,如何来做呢? @@ -59,12 +61,6 @@ public class JbootAppListenerBase implements JbootAppListener { //对应 JFinalConfig 的 configInterceptor } - @Override - public void onFixedInterceptorConfig(FixedInterceptors fixedInterceptors) { - //FixedInterceptor 类似 Interceptor, - // 但是 FixedInterceptor 不会被注解 @Clear 清除 - } - @Override public void onHandlerConfig(JfinalHandlers handlers) { //对应 JFinalConfig 的 configHandler diff --git a/doc/docs/json.md b/doc/docs/json.md new file mode 100644 index 0000000000000000000000000000000000000000..c037ecd477233e04be15e7377dcb1c27d75980d6 --- /dev/null +++ b/doc/docs/json.md @@ -0,0 +1,554 @@ +# Json 配置 + +## 接收 Json + +### @JsonBody +在 Controller 中,我们可以通过给参数添加 @JsonBody 注解来把客户端传入的 Json 数据,转换为我们需要的参数。 + +#### @JsonBody 接收 Bean + +我们定义的 Bean 如下: + +```java +public class MyBean { + private String id; + private int age; + private BigInteger amount; + + //getter setter +} +``` + +示例1:假设前端传入的 Json 数据内容如下: + + +```json +{ + "id":"abc", + "age":17, + "amount":123 + } +``` + +在 Controller 中,我们只需要编写如下内容就可以正常接收: + +```java +public void bean(@JsonBody() MyBean bean) { + System.out.println("bean--->" + bean); + renderText("ok"); +} +``` + + +示例2:假设我们接收的 Bean 在 Json 实体里,例如: +```json +{ "aaa":{ + "bbb":{ + "id":"abc", + "age":17, + "amount":123 + } + } + } +``` + + 在 Controller 中,我们只需要编写如下内容就可以正常接收: + +```java +public void bean(@JsonBody("aaa.bbb") MyBean bean) { + System.out.println("bean--->" + bean); + renderText("ok"); +} +``` + +#### @JsonBody 接收 Map + + 接收 Map 和 Bean 是一样的,只是参数不同。 + +示例1:假设前端传入的 Json 数据内容如下: + + +```json +{ + "id":"abc", + "age":17, + "amount":123 + } +``` + +在 Controller 中,我们只需要编写如下内容就可以正常接收: + +```java +public void bean(@JsonBody() Map bean) { + System.out.println("bean--->" + bean); + renderText("ok"); +} +``` + + +示例2:假设我们接收的 Map 在 Json 实体里,例如: +```json +{ "aaa":{ + "bbb":{ + "id":"abc", + "age":17, + "amount":123 + } + } + } +``` + Controller 代码如下: +```java +public void bean(@JsonBody("aaa.bbb") Map bean) { + System.out.println("bean--->" + bean); + renderText("ok"); +} +``` + +#### @JsonBody 接收 集合(List、Set、Queue、Vector、Stack、Deque 和 数组) + +示例1:前端传入的内容如下 + +```json +[1,2,3] +``` + +如下的方法,都可以正常接收数据: + +```java +//通过 int[] 数组来接收 +public void method1(@JsonBody() int[] beans) { + System.out.println("beans--->" + beans); + renderText("ok"); +} + + //通过 String[] 数组来接收 +public void method2(@JsonBody() String[] beans) { + ystem.out.println("beans--->" + beans); + enderText("ok"); +} + +//通过 List 来接收 +public void method3(@JsonBody() List beans) { + System.out.println("beans--->" + beans); + renderText("ok"); +} + +//通过 Set 来接收 +public void method4(@JsonBody() Set beans) { + System.out.println("beans--->" + beans); + renderText("ok"); +} + +//通过 List 指定泛型 Integer 来接收 +public void method5(@JsonBody() List beans) { + System.out.println("beans--->" + beans); + renderText("ok"); +} + +//通过 Set 指定泛型 Integer 来接收 +public void method6(@JsonBody() Set beans) { + System.out.println("beans--->" + beans); + renderText("ok"); +} + + //通过 List 指定泛型 String 来接收 +public void method7(@JsonBody() List beans) { + System.out.println("beans--->" + beans); + renderText("ok"); +} + + //通过 Set 指定泛型 String 来接收 +public void method8(@JsonBody() Set beans) { + System.out.println("beans--->" + beans); + renderText("ok"); +} +``` + +当然,我们可以把 List 或者 Set 修改为 Queue、Vector、Stack、Deque 等其他数据类型。 + +如果我们要接收的数组包括在 Json 实体里,例如: + +```json +{ +"aaa":{ + "bbb":[1,2,3] + } +} +``` + +只需要在 @JsonBody 添加对应的前缀即可,比如: + +```java +public void method1(@JsonBody("aaa.bbb") int[] beans) { + System.out.println("beans--->" + beans); + renderText("ok"); +} +``` + + 示例2,接收 Bean 数组: + +比如前端传入的是: + +```json +{ +"aaa":{ + "bbb":[ + { + "id":"abc", + "age":17, + "amount":123 + }, + { + "id":"abc", + "age":17, + "amount":123 + } + ] + } +} + +``` + +Controller 的代码如下: + +```java +public void list(@JsonBody("aaa.bbb") List list) { + System.out.println("list--->" + list); + renderText("ok"); + } +``` + +或者 + +```java +public void set(@JsonBody("aaa.bbb") Set" + beans); + renderText("ok"); +} +``` +或者 + +```java +public void array(@JsonBody("aaa.bbb") MyBean[] beans) { + System.out.println("array--->" + beans); + renderText("ok"); +} +``` + +### 通过 @JsonBody 接收基本数据 + +假设客户端(前端)传入的数据内容如下,我们想获取 `id` 的值。 + +```json +{ "aaa":{ + "bbb":{ + "id":"abc", + "age":17, + "amount":123 + } + } + } +``` + +Controller 代码如下: + +```java + public void id(@JsonBody("aaa.bbb.id") String id) { + System.out.println("id--->" + id); + renderText("ok"); + } +``` + +或者我们接收 `age` 参数,内容 Controller 代码如下: + +```java +public void age(@JsonBody("aaa.bbb.age") int age) { + System.out.println("age--->" + age); + renderText("ok"); + } +``` + 此处要注意,因为 `age` 在方法中定义的是 `int` 类型,当前端没有传入任何值得时候,`age` 得到的值为: `0`。所以,如果我们想接收基本数据类型的值,建议定义为其封装类型,比如 int 定义为 Integer,long 定位为 Long 等。 + + +### @JsonBody 高级特性:自动拆装 + +示例1:假设前端传入的内容如下: + +```json +{ +"aaa":{ + "bbb":[ + { + "id":"abc", + "age":17, + "amount":123 + }, + { + "id":"abc", + "age":17, + "amount":123 + } + ] + } +} +``` + +我们想接收到所有 `id` 值,并自动转换为一个 `String[]` 数组、或者 `List `等类型,接收代码如下: + +```java +public void array(@JsonBody("aaa.bbb[id]") String[] ids) { + System.out.println("array--->" + ids); + renderText("ok"); +} +``` + +或者 +```java +public void array(@JsonBody("aaa.bbb[id]") List ids) { + System.out.println("array--->" + ids); + renderText("ok"); +} +``` + +示例2,如果我们想获取数组里的某个值,比如前端传入的数据内容如下: + +```json +{ +"aaa":{ + "bbb":[ + { + "id":"abc", + "age":17, + "amount":123 + }, + { + "id":"abc", + "age":17, + "amount":123 + } + ] + } +} +``` + +我们想获取 第一个 `age` 的值,接收数据内容如下: + +```java +public void age(@JsonBody("aaa.bbb[0].age") long age) { + renderText("intInArray--->" + age); +} +``` + +示例3: +假设前度传入的 Json 内容如下: + +```json +{ + "aaa":{ + "bbb":[ + { + "attr1":"abc", + "beans":[ + { + "id":"abc", + "age":17, + "amount":123 + }, + { + "id":"abc", + "age":17, + "amount":123 + } + ] + }, + { + "attr2":"abc" + } + ] + } +} +``` +我们想获取 beans 的值,Controller 内容如下: + +```java +public void array(@JsonBody("aaa.bbb[0].beans") MyBean[] beans) { + System.out.println("array--->" + JsonKit.toJson(beans)); + renderText("ok"); +} +``` + +如果我们想获取 `beans` 下的所有 `id` 值,Controller 内容如下: + +```java +public void array(@JsonBody("aaa.bbb[0].beans[id]") String[] ids) { + System.out.println("array--->" + JsonKit.toJson(ids)); + renderText("ok"); +} +``` + +### 直接通过 Controller 接收 Json + +在 Controller 中,我们如果不通过 `@JsonBody` 接收 Json 数据,也是没问题的。 + +例如:前端传入的 Json 内容如下: + +```json +{ + "aaa":{ + "bbb":[ + { + "attr1":"abc", + "beans":[ + { + "id":"abc", + "age":17, + "amount":123 + }, + { + "id":"abc", + "age":17, + "amount":123 + } + ] + }, + { + "attr2":"abc" + } + ] + } +} +``` + +我们想获取 beans 的值,Controller 内容如下: + +```java +public void array() { + MyBean[] beans = getRawObject(MyBean[].class,"aaa.bbb[0].beans"); + System.out.println("array--->" + JsonKit.toJson(beans)); + renderText("ok"); +} +``` + +此处要注意,如果我们想获取一个泛型 `List`,需要使用如下方法: + +```java +public void array() { + List beans = getRawObject(new TypeDef>(){},"aaa.bbb[0].beans"); + System.out.println("array--->" + JsonKit.toJson(beans)); + renderText("ok"); +} +``` + +或者 + +```java +public void array() { + Set beans = getRawObject(new TypeDef>(){},"aaa.bbb[0].beans"); + System.out.println("array--->" + JsonKit.toJson(beans)); + renderText("ok"); +} +``` + +如果我们想获取 `beans` 下的所有 `id` 值,Controller 内容如下: + + +```java +public void array(@JsonBody() { + String[] ids = getRawObject(String[].class,"aaa.bbb[0].beans[id]"); + System.out.println("array--->" + JsonKit.toJson(ids)); + renderText("ok"); +} +``` + +或者 +```java +public void array() { + List ids = getRawObject(new TypeDef>(){},"aaa.bbb[0].beans[id]"); + System.out.println("array--->" + JsonKit.toJson(ids)); + renderText("ok"); +} +``` + +或者 + +```java +public void array() { + Set ids = getRawObject(new TypeDef>(){},"aaa.bbb[0].beans[id]"); + System.out.println("array--->" + JsonKit.toJson(ids)); + renderText("ok"); +} +``` + +## 输出 Json + + +### jboot v3.5.1(包括 3.5.1) 之后的配置 + +``` +#是否启用大写转换,比如 user_age 自动转为为 UserAge,只对 Model 生效,默认值为 true +jboot.json.camelCaseJsonStyleEnable = true + +#是否转换任何地方,比如 map.put("my_field") 默认输出为 myField,默认值为 false +jboot.json.camelCaseToLowerCaseAnyway = false + +#是否跳过 null 值输出,map.put("key",null),则 key 值不输,默认值为 true +jboot.json.skipNullValueField = true + +#配置输出的时间格式,默认值为 yyyy-MM-dd HH:mm:ss +jboot.json.timestampPattern = "yyyy-MM-dd HH:mm:ss" + +# 是否跳过 model 的 attr,只用 bean 的 getter 来渲染 +jboot.json.skipModelAttrs = false + +# 是否值跳过 bean 的 getter,只用 model 的 attr 渲染 +jboot.json.skipBeanGetters =false +``` + +### jboot v3.5.1 之前的配置 + +``` +#是否启用大写转换,比如 user_age 自动转为为 UserAge +jboot.web.camelCaseJsonStyleEnable = true + +#是否转换任何地方,比如 map.put("my_field") 默认输出为 myField +jboot.web.camelCaseToLowerCaseAnyway = false + +#配置输出的时间格式 +jboot.web.jsonTimestampPattern +``` + +### 通过 Java 代码配置 + +更多的配置可以通过 JFinalJsonKit 工具类来进行配置。 + +### 其他 + +- 1、通过 @JsonIgnore 注解可以忽略某个字段,比如 + +```java +public class User{ + + @JsonIgnore + public String getPassword(){ + return Supper.getPassword(); + } +} +``` + +以上代码可以忽略 password 的输出。 + +- 2、支持 FastJson 的 JsonField 的配置 + +```java +public class User { + + @JSONField(name = "sex") + public String getSexString(){ + return "男"; + } +} +``` + +输出字段又 `sexString` 重新修改为 `sex`。 + diff --git a/doc/docs/junit.md b/doc/docs/junit.md new file mode 100644 index 0000000000000000000000000000000000000000..fe345a9ecc5fcade739b2ecbd519eb99c3885e5f --- /dev/null +++ b/doc/docs/junit.md @@ -0,0 +1,254 @@ +# Junit 单元测试 + +## 目录 +- Junit4 单元测试 +- Junit5 单元测试 +- @TestConfig +- @MockMethod +- @MockClass + +## Junit 单元测试简介 + +单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。 + +JUnit 是一个 Java 编程语言的单元测试框架。JUnit 在测试驱动的开发方面有很重要的发展,是起源于 JUnit 的一个统称为 xUnit 的单元测试框架之一。 + + +目前市面上主要是使用 Junit4 和 Junit5 对 Java 程序进行单元测试。 + + +## Junit4 单元测试 + +1、第一步,添加 junit4 的 maven 依赖 + +```xml + + junit + junit + 4.13.2 + test + +``` + +2、第二步,编写单元测试代码 + +```java +@RunWith(JbootRunner.class) +public class MyAppTester { + + private static MockMvc mvc = new MockMvc(); + + @Inject + private MyService myService; + + @Test + public void test_url_aaa() { + MockMvcResult mvcResult = mvc.get("/aaa"); + + mvcResult.printResult() + .assertThat(result -> Assert.assertNotNull(result.getContent())) + .assertTrue(result -> result.getStatus() == 200); + } + + @Test + public void test_url_bbb() { + MockMvcResult mvcResult = mvc.get("/bbb"); + + mvcResult.printResult() + .assertThat(result -> Assert.assertNotNull(result.getContent())) + .assertTrue(result -> result.getStatus() == 200); + } + + @Test + public void test_my_service() { + Ret ret = myService.doSomeThing(); + Assert.assertNotNull(ret); + //..... + } +} +``` +> 注意:Junit4 测试类必须添加 `@RunWith(JbootRunner.class)` 配置 + + +## Junit5 单元测试 + +1、第一步,添加 junit4 的 maven 依赖 + +```xml + + org.junit.jupiter + junit-jupiter-engine + 5.7.2 + test + +``` + +2、第二步,编写单元测试代码 + +```java +@ExtendWith(JbootExtension.class) +public class MyAppTester { + + private static MockMvc mvc = new MockMvc(); + + @Inject + private MyService myService; + + @Test + public void test_url_aaa() { + MockMvcResult mvcResult = mvc.get("/aaa"); + + mvcResult.printResult() + .assertThat(result -> Assertions.assertNotNull(result.getContent())) + .assertTrue(result -> result.getStatus() == 200); + } + + @Test + public void test_url_bbb() { + MockMvcResult mvcResult = mvc.get("/bbb"); + + mvcResult.printResult() + .assertThat(result -> Assertions.assertNotNull(result.getContent())) + .assertTrue(result -> result.getStatus() == 200); + } + + @Test + public void test_my_service() { + Ret ret = myService.doSomeThing(); + Assertions.assertNotNull(ret); + //..... + } +} +``` +> 注意:Junit5 测试类必须添加 `@ExtendWith(JbootExtension.class)` 配置 + + +## @TestConfig + +在测试的过程中,Jboot 默认的 webRootPath 是 `target/classes/webapp` 目录,而 classPath 的目录是 `target/classes` +目录。 + +如果我们需要修改此目录,则需要在测试类中添加 @TestConfig 注解,对 webRootPath 或 classPath 进行配置。 + +例如: + +```java +@RunWith(JbootRunner.class) +@TestConfig(webRootPath = "your-path",classPath = "your-path") +public class MyAppTester { + + private static MockMvc mvc = new MockMvc(); + + + @Test + public void test_url_aaa() { + MockMvcResult mvcResult = mvc.get("/aaa"); + + //your code ... + } + + +} +``` + +`@TestConfig(webRootPath = "your-path",classPath = "your-path")` 里的配置路径,可以是绝对路径或相对路径, +若是相对路径,则是相对 `target/test-classes` 目录的路径。 + +## @MockMethod + +Jboot 提供了 `@MockMethod` 注解,方便对 AOP 管理的类里的方法(method)进行 Mock 操作。 + +例如: +```java +@RunWith(JbootRunner.class) +@TestConfig(autoMockInterface = true) +public class OptionApiControllerTest { + + private static final MockMvc mvc = new MockMvc(); + + @Test + public void query() { + mvc.get("/api/option/query?key=myKey").printResult(); + } + + + @MockMethod(targetClass = JPressCoreInitializer.class) + public void onHandlerConfig(JfinalHandlers handlers) { + handlers.add(new JPressHandler()); + } + + + @MockMethod(targetClass = UtmService.class) + public void doRecord(Utm utm){ + System.out.println(">>>>>>>>>doRecord: " + utm); + } + + + @MockMethod(targetClass = WebInitializer.class,targetMethod = "onEngineConfig") + public void mock_on_engine_config(Engine engine){ + System.out.println(">>>>>>>>>onEngineConfig: " + engine); + } +} +``` + +在以上的代码中,有几个关键的地方 +- `@TestConfig(autoMockInterface = true)`,表示当我们测试 `query()` 方法的时候,可能会遇到一些注入进来的接口,但是可能没有实现类(或者说实现类在别的 Maven Module,并没有依赖进来),但是保证不出错。 +当调用接口方法时,等同于什么都不做,若有返回值,则返回 `null`。 +- `@MockMethod(targetClass = JPressCoreInitializer.class)` 表示复写 `JPressCoreInitializer` 类的 `onHandlerConfig` 方法。 +- `@MockMethod(targetClass = UtmService.class)` 表示复写 `UtmService` 类的 `doRecord` 方法。 +- `@MockMethod(targetClass = WebInitializer.class,targetMethod = "onEngineConfig")` 表示复写 `WebInitializer` 类的 `onEngineConfig` 方法。 + +> 注意: +> +> 1、在以上代码中,`mock_on_engine_config` 方法的参数必须和 `WebInitializer.onEngineConfig` 里的参数一样, +> 或者 `mock_on_engine_config` 可以多出一个 `targetClass` 的对象参数,例如:mock_on_engine_config(WebInitializer webInitializer,Engine engine) +> +> 2、`@MockMethod` 的优先级高于 `@TestConfig(autoMockInterface = true)` 的配置。 + + +## @MockClass + +Jboot 提供了 `@MockClass` 注解,方便对 AOP 管理的类(Class)进行 Mock 操作。 + +例如,如下的代码是请求了 `/api/article/detail` 这个 API 接口。 + +```java +public class ArticleApiControllerTest{ + + static final MockMvc mvc = new MockMvc(); + + @Test + public void detail() { + mvc.get("/api/article/detail?id=1").printResult(); + } +} +``` + +假设这个 API 接口里的 `Controller` 通过 `ArticleService` 进行进步一查询数据。除了我们可以通过 `@MockMethod` 对 `ArticleService` 的方法进行 Mock, +也编写一个类,实现 `ArticleService` 接口,并通过 `@MockClass` 添加在实现的类上。 + +```java +@MockClass +public class ArticleServiceMock implements ArticleService { + + @Override + public Article findById(Object id) { + Article article = new Article(); + article.setId((Long) id); + article.setStatus(Article.STATUS_NORMAL); + return article; + } + +} +``` + +此时,我们在测试的时候,当 `Controller` 调用了 `ArticleService.findById(id)` 就会自动调用了 `ArticleServiceMock` 里的 `findById` 方法。 + +相对 `@MockMethod` 而言,`@MockClass` 有几个好处: + +- 1、`@MockClass` 注解的类是去实现某个接口的,ide 等工具会自动帮我们生成方法,不易出错。 +- 2、当 Mock 很多个方法的时候,可以写到这个类里,使得测试代码更加简洁 +- 3、在有多个测试类的时候,多个测试类可以共用相同的 Mock 代码,减少代码量 + + +> 注意:如果 `@MockClass` 和 `@MockMethod` 同时对一个方法进行 Mock,`@MockMethod` 的优先级高于 `@MockClass`。 \ No newline at end of file diff --git a/doc/docs/jwt.md b/doc/docs/jwt.md index aca24a393012538a80bc45a75ecbb4ec15c3e7ce..0be927a2fb9d28def9c0ff4c1640974ab2d14310 100644 --- a/doc/docs/jwt.md +++ b/doc/docs/jwt.md @@ -12,27 +12,44 @@ JWT 是 Json web token 的简称, 是为了在网络应用环境间传递声明 ## JWT 配置 -- jboot.web.jwt.httpHeaderName:配置JWT的http头的key,默认为 `Jwt` -- jboot.web.jwt.secret:配置JWT的密钥 -- jboot.web.jwt.validityPeriod:配置JWT的过期时间,默认不过期 +在使用 JWT 之前,我们需要对 JWT 进行一些必要的配置。 + +- jboot.web.jwt.httpHeaderName:配置 JWT 的 http 头的 key,默认为 `Jwt`,可以不配置。 +- jboot.web.jwt.secret:配置 JWT 的密钥,必须配置,否则使用 jwt 会抛出异常或给出警告。 +- jboot.web.jwt.validityPeriod:配置 JWT 的过期时间,默认永不不过期。 ## JWT 使用 -在 `JbootController` 中,新增了如下几个用于操作 JWT 的方法: +在 `JbootController` 中,新增了如下几个用于操作 JWT 的方法,在使用 Jwt 之前,需要在使用 Jwt 的 Controller +里添加注解 `@EnableJwt` ,才能够正常的生成和刷新 Jwt 。当有很多个 Controller 都使用 Jwt 的话,可以直接 创建 +一个 BaseController,然后在 BaseController 里添加注解 `@EnableJwt`。 + - setJwtAttr():设置 jwt 的 key 和 value - setJwtMap():把整个 map的key和value 设置到 jwt - getJwtAttr():获取 已经设置进去的 jwt 信息 - getJwtAttrs():获取 所有已经设置进去的 jwt 信息 -- getJwtPara():获取客户端传进来的 jwt 信息,若 jwt 超时或者不被信任,那么获取到的内容为null +- getJwtPara():获取客户端传进来的 jwt 信息,若 jwt 超时或者不被信任,那么获取到的内容为 null +- getJwtParaToString() +- getJwtParaToInt() +- getJwtParaToLong() +- getJwtParaToBigInteger() +- getJwtParas() ## 注意事项 -在服务端通过 `setJwtAttr()` 方法设置 JWT 后,Http 的响应头会添加一个名称为 `Jwt` 的属性(可以通过 `jboot.web.jwt.httpHeaderName` 进行配置)。 +在服务端通过 `setJwtAttr()` 方法设置 JWT 后,Http 的响应头会添一个名称为 `Jwt` 的属性 +(可以通过 `jboot.web.jwt.httpHeaderName` 进行配置)。 + + +此时,客户端(浏览器、小程序、APP等)发现 Http 头有该属性后,需要客户端主动把该值存储起来。 +APP存储到数据库、浏览和小程序可以存储到 `localStorage` 等。 +当客户端进行 Http 请求的时候,需要在 Http 头添加下属性为 `Jwt`、值为之前存储数据 的请求头。 + -此时,客户端(浏览器、小程序、APP等)发现 Http 头有该属性后,需要客户端主动把该值存储起来。APP存储到数据库、浏览和小程序可以存储到localStorage等,当客户端进行 Http 请求的时候,需要在 Http 头添加下属性为 `Jwt`、值为之前存储数据 的请求头。 +当客户端正确添加 `Jwt` 的 Http 请求头的时候,服务端可以通过 `getJwtPara()` +方法获取到客户端传入的内容。 -当客户端正确添加 `Jwt` 的 Http 请求头的时候,服务端可以通过 `getJwtPara()` 方法获取到客户端传入的内容。 **注意:** 接收客户端传入的Jwt值是通过`getJwtPara()`方法,而不是 `getJwtAttr()`。 \ No newline at end of file diff --git a/doc/docs/limit.md b/doc/docs/limit.md index 3898f20aa095a5ad545e7cb83d0029a2b4afc4b7..546cc05fc81442ec45dd124d335eb93ab964857c 100644 --- a/doc/docs/limit.md +++ b/doc/docs/limit.md @@ -1,5 +1,7 @@ # 限流 +关于 Sentinel 限流功能,请点 [这里](./sentinel.md) 查看。 + ## 目录 - 限流的场景 @@ -31,25 +33,30 @@ Jboot 提供了两种方案: 在 `jboot.properties` 文件中定义如下: ``` jboot.limit.enable = true -jboot.limit.rule = /user*:tb:1,io.jboot.aop*.get*(*):tb:1 +jboot.limit.rule = /user*:tb:1,/other*:iptb:1,io.jboot.aop*.get*(*):tb:1 +jboot.limit.ipWhitelist = 127.0.0.1 ``` - jboot.limit.enable : 限流功能的开关 - jboot.limit.rule : 限流规则 +- jboot.limit.ipWhitelist : IP 白名单,多个 IP 用英文逗号隔开,此 IP 下的访问,将不受限流配置的影响 + > 规则说明: > - 1、可以配置多个规则,每个规则用英文逗号隔开 > - 2、规则分为三个部分,用冒号(:)隔开,分别是:资源、限流类型、限流参数值 -> - 3、限流的类型有2种、分别是:tb 和 cc。tb:TOKEN BUCKET(令牌桶),cc:CONCURRENCY(并发量) +> - 3、限流的类型有4种、分别是:tb、cc、iptb 和 ipcc。tb:TOKEN BUCKET(令牌桶,每秒允许访问的数量),cc:CONCURRENCY(并发量),iptb:单个ip每秒允许访问的数量,ipcc:单个ip允许同时的并发量。 > - 4、星号(*)匹配任意字符,也可以是空字符。 -在以上配置中,配置了两个规则,分别是: +在以上配置中,配置了三个规则,分别是: - `/user*:tb:1` +- `/other*:iptb:1` - `io.jboot.aop*.get*(*):cc:1` -第一个规则:匹配 `/user` 开头的所有url地址,每个 url 地址,1秒钟之内只允许访问1次。 -第二个规则:匹配 `io.jboot.aop` 开头的所有包名,并且 `get` 开头的所有任意参数的方法。并发量为 1。 +第一个规则:匹配 `/user` 开头的所有url地址,每个 url 地址,1 秒钟之内只允许访问 1 次。 +第二个规则:匹配 `/other` 开头的所有url地址,每个 ip 地址在 1 秒钟之内只允许访问 1 次。 +第三个规则:匹配 `io.jboot.aop` 开头的所有包名,并且 `get` 开头的所有任意参数的方法。并发量为 1。 **使用方案2:通过注解 `@EnableLimit`** @@ -81,8 +88,9 @@ public class IndexController extends JbootController { ## 限流的实现 上文中提到,Jboot 提供了两种限流类型,他们分别是: -- TOKEN BUCKET : 令牌桶 -- CONCURRENCY : 并发量 + +- TOKEN BUCKET : 令牌桶,每秒允许访问的次数。 +- CONCURRENCY : 并发量,和时间无关。 TOKEN BUCKET 令牌桶,是通过 Google Guava 的 RateLimiter 来实现的。 diff --git a/doc/docs/metrics.md b/doc/docs/metrics.md index 5ef5a117bdd1fb785afcf5027b3bffa696bbf242..cb0fe750de427a91acff29ded7dfcb7de0b5adfd 100644 --- a/doc/docs/metrics.md +++ b/doc/docs/metrics.md @@ -8,17 +8,17 @@ Jboot 内置了一套监控机制,可以用来监控 Controller、Service 等 - @EnableMetricMeter - @EnableMetricTimer -这些监控的数据,我们可以输出到 slf4j 日志,可以输入到网页的json,也可以通过配置直接把数据输出到 grafana,使用 grafana 面板来进行可视化的数据监控,如下图。 +这些监控的数据,我们可以输出到 slf4j 日志,可以输入到网页的 json,也可以通过配置直接把数据输出到 prometheus,然后使用 grafana 面板来进行可视化的数据监控,如下图。 -![grafana](./imgs/grafana.png) +![grafana](./static/images/grafana.png) ## Metrics 输出到日志 这是最简单的一种方法,我们只需要在 jboot.properties 添加如下配置: -``` -jboot.metric.url=-/metrics_admin +```properties +jboot.metric.enable=true jboot.metric.reporter=slf4j ``` @@ -38,54 +38,100 @@ public class MetricsController extends JbootController { 此时,启动 jboot 应用后,当访问 `http://127.0.0.1:8080/` ,控制台(日志) 会定时输出 `http://127.0.0.1:8080/` 的并发量和访问次数。(默认情况下是1分钟输出一次日志)。 -同时,由于我们配置了 `jboot.metric.url=-/metrics_admin` ,我们可以通过 `http://127.0.0.1:8888/metrics_admin` 来查看 `index()` 这个方法的访问次数和并发量。 +如果我们需要通过网页来查看监控的 json 数据,可以添加配置 +```properties +boot.metric.adminServletMapping = metrics.admin +``` -## Metrics 输出到 grafana +此时,我们可以通过 `http://127.0.0.1:8888/metrics.admin` 来查看 `index()` 这个方法的访问次数和并发量。 -grafana 并没有接收数据的能力,因此,jboot 的方案是先数据输出到 influxdb,在配置 grafana 来读取 influxdb 的数据。 -因此,在 grafana 正常显示 jboot 数据之前,先把 grafana 和 influxdb 启动起来。 +## Metrics 输出到 Grafana -**启动 influxdb :** +Grafana 是一个开源的度量分析与可视化套件。经常被用作基础设施的时间序列数据和应用程序分析的可视化,它在其他领域也被广泛的使用包括工业传感器、家庭自动化、天气和过程控制等。 -``` -docker run -d -p 8086:8086 -p 8083:8083 \ - -e INFLUXDB_ADMIN_ENABLED=true \ - -e INFLUXDB_DB=metricsDb \ - -e INFLUXDB_ADMIN_USER=admin \ - -e INFLUXDB_ADMIN_PASSWORD=123456 \ - -e INFLUXDB_USER=fuhai \ - -e INFLUXDB_USER_PASSWORD=123456 \ - influxdb -``` +Grafana 支持许多不同的数据源,比如: Graphite,InfluxDB,OpenTSDB,Prometheus,Elasticsearch,CloudWatch 和 KairosDB 等。每个数据源都有一个特定的查询编辑器,该编辑器定制的特性和功能是公开的特定数据来源。 -**启动 grafana :** +需要注意的是:Grafana 并没有接收数据的能力,因此,Jboot 的方案是先把数据输出到 Prometheus (或者 influxdb),再配置 Grafana 来读取 Prometheus (或者 influxdb) 的数据。 + +因此,在 Grafana 正常显示 Jboot 数据之前,先把 Grafana 和 Prometheus 启动起来。 + +**启动 Prometheus :** +- 1、下载 Prometheus 到本地,下载地址:https://prometheus.io/download/ +- 2、进入到 Prometheus 的解压目录,修改 prometheus.yml 文件,内容如下: + +```yml +# my global config +global: + scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + # scrape_timeout is set to the global default (10s). + +# Alertmanager configuration +alerting: + alertmanagers: + - static_configs: + - targets: + # - alertmanager:9093 + +# Load rules once and periodically evaluate them according to the global 'evaluation_interval'. +rule_files: + # - "first_rules.yml" + # - "second_rules.yml" + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + + # metrics_path defaults to '/metrics' + # scheme defaults to 'http'. + + static_configs: + - targets: ['localhost:9090'] + + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'jboot' + static_configs: + - targets: ['localhost:1234'] -``` -docker run -d -p 3000:3000 grafana/grafana ``` -同时,需要在 jboot 应用添加如下依赖: +- 3、通过 ./prometheus --config.file=prometheus.yml 启动 Prometheus +- 4、启动成功后,我们可以通过 `http://127.0.0.1:9090` 访问到 Prometheus 的查询页面,在 `http://127.0.0.1:9090/targets` 可以看到如下图所示: + +![prometheus_targets](./static/images/prometheus_targets.png) + +其中,jboot 应用是红色的,state 处于 down 的状态,原因是 Jboot 应用还未启动。 + + + + +最后,需要在 jboot 应用添加如下依赖: ```xml - com.github.davidb - metrics-influxdb - 1.1.0 + io.prometheus + simpleclient_dropwizard + 0.11.0 + provided + + + + io.prometheus + simpleclient_httpserver + 0.11.0 + provided ``` 和 在 jboot.properties 添加如下配置: -``` -jboot.metric.url=/metrics_admin -jboot.metric.reporter=influxdb -jboot.metric.reporter.influxdb.host=127.0.0.1 -jboot.metric.reporter.influxdb.port=8086 -jboot.metric.reporter.influxdb.user=admin -jboot.metric.reporter.influxdb.password=123456 -jboot.metric.reporter.influxdb.dbName=metricsDb +```properties +jboot.metric.enable=true +jboot.metric.reporter=prometheus ``` 当然,要监控某个方法的相关输入,还需要通过注解来进行配置 @@ -105,25 +151,53 @@ public class MetricsController extends JbootController { } ``` -启动 jboot,当访问 `http://127.0.0.1:8080/` 之后, jboot 就会把Metrics的数据输出到 influxdb,此时我们就可以配置 grafana 读取 influxdb 的数据了。 +启动 jboot,当访问 `http://127.0.0.1:8080/` 之后, jboot 就会把 Metrics 的数据输出到 prometheus ,此时我们就可以配置 grafana 读取 prometheus 的数据了。 -更多关于 grafana 读取 influxdb 的文档请参考 https://grafana.com/docs/features/datasources/influxdb/ 。 +**配置 Grafana 读取 Prometheus 的数据** +- 启动 Grafana -## Metrics 输出到 graphite +```shell +docker run -d -p 3000:3000 grafana/grafana +``` -在开始之前,需要添加如下的 Maven 依赖。 +- 进入 Grafana 的后台 + + 通过网址 `http://127.0.0.1:3000` 可以访问到 Grafana,首次访问需要登录,默认账号和密码都是 admin。 -```xml - - io.dropwizard.metrics - metrics-graphite - 4.1.0 - -``` +- 导入 Jboot JVM 的 Grafana 大盘配置 -**启动 graphite** +![grafana_import](./static/images/grafana_import.png) -``` +![grafana_import](./static/images/grafana_import_json.png) + +- 为 Grafana 添加 Prometheus 的数据源 + +![grafana_import](./static/images/grafana_datasource_new.png) + + +在 `Import via panel json` 中输入 `https://gitee.com/JbootProjects/jboot/raw/master/doc/jboot_jvm_grafana.json` 中的内容,然后点击 load,就可以见到如下的 JVM 大图了。 + +![grafana_import](./static/images/grafana_jboot_jvm.png) + + +## Metrics 输出到 Graphite + +Graphite 是一个开源实时的、显示时间序列度量数据的图形系统。Graphite 并不收集度量数据本身,而是像一个数据库,通过其后端接收度量数据,然后以实时方式查询、转换、组合这些度量数据。Graphite支持内建的Web界面,它允许用户浏览度量数据和图。 + +Graphite 有三个主要组件组成: + +- 1)Graphite-Web +这是一个基于Django的Web应用,可以呈现图形和仪表板 + +- 2)Carbon +这是一个度量处理守护进程 + +- 3)Whisper +这是一个基于时序数据库的库 + +在开始之前,我们需要启动 Graphite + +```shell docker run -d\ --name graphite\ --restart=always\ @@ -135,10 +209,21 @@ docker run -d\ graphiteapp/graphite-statsd ``` -在 jboot.properties 添加如下配置: +然后,在我们自己的项目添加如下的 Maven 依赖。 + +```xml + + io.dropwizard.metrics + metrics-graphite + 4.1.0 + ``` -jboot.metric.url=/metrics_admin + +最后在 jboot.properties 添加如下配置: + +```properties +jboot.metric.enable=true jboot.metric.reporter=graphite jboot.metric.reporter.graphite.host=127.0.0.1 jboot.metric.reporter.graphite.port=2003 diff --git a/doc/docs/mq.md b/doc/docs/mq.md index 339a75f28fe7cab8d9015c550ede5197a9908782..b2c8b376c7fa34cab1e4790e65914fcc54339e4d 100644 --- a/doc/docs/mq.md +++ b/doc/docs/mq.md @@ -1,10 +1,12 @@ # MQ 消息队列 +## 基本使用 + Jboot 内置了对MQ消息队列的功能支持,使用MQ需要以下几步步骤。 **第一步:配置jboot.properties文件,内容如下:** -``` +```properties # 默认为redis (支持: redis,activemq,rabbitmq,hornetq,aliyunmq等 ) jboot.mq.type = redis jboot.mq.channel = channel1,channel2,channel3 @@ -32,11 +34,117 @@ Jboot.getMq().startListening(); 配置完毕后,我们在其他服务器,就可以通过如下代码发送消息: -``` +```java Jboot.getMq().publish(yourObject, "channel1"); ``` 需要注意的是,两个服务器的 mq 类型、和服务器信息一定是一致的。消息的接收端的 `jboot.mq.channel` 配置必须包含 "channel1" 才能正常接收数据 。 -其他更多关于 rabbitmq、aliyunmq、qpidmq 等的配置,需要查看 [这里](./config.md)。 \ No newline at end of file +其他更多关于 rabbitmq、aliyunmq、qpidmq 等的配置,需要查看 [这里](./config.md)。 + +## Rabbitmq + +```properties +#设置 mq 的相关信息 +jboot.mq.type=rabbitmq +jboot.mq.channel=channel1 + + +#以下可以不用配置,是默认信息 +jboot.mq.rabbitmq.username=guest +jboot.mq.rabbitmq.password=guest +jboot.mq.rabbitmq.host=127.0.0.1 +jboot.mq.rabbitmq.port=5672 +jboot.mq.rabbitmq.virtualHost= + +#非常重要,多个应用如果同时接受同一个 channel 的广播,必须配置此项,而且必须不能相同,否则广播的时候只有一个应用能够接受到 +jboot.mq.rabbitmq.broadcastChannelPrefix=app1 +``` + +## Redis-mq + +```properties +jboot.mq.type=redis +jboot.mq.channel=channel1,channel2,myChannel +jboot.mq.redis.host=127.0.0.1 +jboot.mq.redis.port=6379 +jboot.mq.redis.timeout=2000 +jboot.mq.redis.password +jboot.mq.redis.database +jboot.mq.redis.clientName +jboot.mq.redis.testOnCreate +jboot.mq.redis.testOnBorrow +jboot.mq.redis.testOnReturn +jboot.mq.redis.testWhileIdle +jboot.mq.redis.minEvictableIdleTimeMillis +jboot.mq.redis.timeBetweenEvictionRunsMillis +jboot.mq.redis.numTestsPerEvictionRun +jboot.mq.redis.maxAttempts +jboot.mq.redis.maxTotal +jboot.mq.redis.maxIdle +jboot.mq.redis.minIdle +jboot.mq.redis.maxWaitMillis +``` + +## Rocketmq + +```properties +jboot.mq.type=rocketmq +jboot.mq.channel=channel1,channel2,myChannel +jboot.mq.rocket.namesrvAddr=127.0.0.1:9876 +jboot.mq.rocket.namespace +jboot.mq.rocket.consumerGroup = "jboot_default_consumer_group"; +jboot.mq.rocket.consumeMessageBatchMaxSize +jboot.mq.rocket.broadcastChannelPrefix "broadcast-"; +jboot.mq.rocket.producerGroup "jboot_default_producer_group"; +``` + +## 多 MQ 实例 + +若在一个应用在,里面有多个 MQ 实例,假设有两个 redis 实例和两个 Rabbitmq 实例,配置如下: + +```properties +# 默认 MQ 及其类型 +jboot.mq.type=redis +jboot.mq.channel=channel1,channel2,myChannel + +jboot.mq.redis.host=127.0.0.1 +jboot.mq.redis.port=6379 +jboot.mq.redis.timeout=2000 + + +# 其他另一个 mq1 的类型 +jboot.mq.other1.type=redis +jboot.mq.other1.typeName=redis1 +jboot.mq.other1.channel=channel1,channel2.... + + +jboot.mq.redis.redis1.host=127.0.0.1 +jboot.mq.redis.redis1.port=6379 +jboot.mq.redis.redis1.timeout=2000 + + +# 其他另一个 mq2 的类型 +jboot.mq.other2.type=rabbitmq +jboot.mq.other2.typeName=rabbitmqaaa +jboot.mq.other2.channel=channel1,channel2.... + +jboot.mq.rabbitmq.rabbitmqaaa.username=guest +jboot.mq.rabbitmq.rabbitmqaaa.password=guest +jboot.mq.rabbitmq.rabbitmqaaa.host=127.0.0.1 +jboot.mq.rabbitmq.rabbitmqaaa.port=5672 + +# 其他另一个 mq3 的类型 +jboot.mq.other3.type=rabbitmq +jboot.mq.other3.typeName=rabbitmqbbb +jboot.mq.other3.channel=channel1,channel2.... + +jboot.mq.rabbitmq.rabbitmqbbb.username=guest +jboot.mq.rabbitmq.rabbitmqbbb.password=guest +jboot.mq.rabbitmq.rabbitmqbbb.host=127.0.0.1 +jboot.mq.rabbitmq.rabbitmqbbb.port=5672 + + +``` + diff --git a/doc/docs/mvc.md b/doc/docs/mvc.md index 43fcf2ad1d378680423ba4ef43a03e2064f66889..904c728364c91e8443bf52dc9771197bb7f7a7d6 100644 --- a/doc/docs/mvc.md +++ b/doc/docs/mvc.md @@ -7,14 +7,11 @@ - Controller : 控制器 - Action :请求的基本单位 - Interceptor : 拦截器 -- FixedInterceptor :永久拦截器 - Handler : 处理器 - Render :渲染器 - Session - Cookie -- Jwt : Json Web Token -- Validate : 验证器 -- 安全 + ## 描述 @@ -27,9 +24,141 @@ JFinal 的相关文档: [https://www.jfinal.com/doc/3-1](https://www.jfinal.co Controller 是 JFinal 的核心类之一,是 MVC 设计模式中的控制器。基于 Jboot 开发的控制器需要继承 Controller。Controller 也是定义 Action 方法的地点,一个 Controller 可以包含多个 Action 。 - 另外,JbootController 扩展了 JFinal 的 Controller 类,增加了 Jwt、FlashMessage 和 其他一些实用的方法。 +## Jboot 增强的 Controller 功能 + +#### 增强功能1:缓存功能 + +例如: +```java +@Cacheable(name = "aaa", liveSeconds = 10) +public void json() { + + System.out.println("json() invoked!!!!!!!!!"); + + Map data = new HashMap<>(); + data.put("age", 1); + data.put("name", "张三"); + data.put("sex", 1); + + renderJson(data); +} +``` +以上的代码中,在 10 秒钟内多次访问的时候,只有第一次访问到了 json() 方法,其余的都直接使用第一次的结果输出给浏览器。其中,缓存的名称为 +"aaa",key 为 `class.method()` 格式。我们可以通过 `@Cacheable(name = "aaa", key="xxx")` 来指定缓存的key值。 + +其中 key 里的内容还可以使用 `#(方法参数名称)` 和 `@para('http参数名称')` 的方式来定义 key。 + +例如: +```java +@Cacheable(name = "aaa", key="#para('mykey')", liveSeconds = 10) +public void json() { + + System.out.println("json() invoked!!!!!!!!!"); + + Map data = new HashMap<>(); + data.put("age", 1); + data.put("name", "张三"); + data.put("sex", 1); + + renderJson(data); +} +``` +当访问 `http://127.0.0.1:8080/json?mykey=thekey` 的时候,该缓存的 key 的内容为:"thekey"。 + +但是在某些极端场景下,我们可能是不需要缓存的,配置如下: + +```java + @Cacheable(name = "aaa", liveSeconds = 10, unless = "para('type')==1") + public void json2() { + System.out.println("json2() invoked!!!!!!!!!"); + Map data = new HashMap<>(); + data.put("age", 1); + data.put("name", "张三"); + data.put("sex", 1); + renderJson(data); + } +``` + +当访问 `http://127.0.0.1:8080/json2?type=1` 的时候,永远不会命中缓存。 + +#### 增强功能2:返回值自动渲染功能 + +例如: + +```java +@RequestMapping("/") +public MyController extends Controller{ + + //自动使用 html 来渲染 + public String test1(){ + return "test1.html"; + } + + //自动使用文本来渲染 + public String test2(){ + return "test2..."; + } + + //渲染 404 错误 + public String test3(){ + return "error: 404"; + } + + //渲染 500 错误 + public String test4(){ + return "error: 500"; + } + + //自动 redirect 跳转 + public String test5(){ + return "redirect: /to/your/path"; + } + + //自动 forward action 重定向 + public String test6(){ + return "forward: /to/your/path"; + } + + //自动进行文件下载 + public File test7(){ + return new File('/file/path'); + } + + //渲染 json 内容 + public Object test8(){ + Map map = new HashMap<>(); + map.put("key","value...."); + return map; + } + + //使用自动的 render 渲染 + public Object test9(){ + Render render = new TextRender("some text..."); + return render; + } + +} +``` + +#### 增强功能3:XSS 功能防护功能 + +在 jboot 中,只需要我们在 jboot.properties 添加如下配置,并能开启全局 xss 防护功能: + +```properties +jboot.web.escapeParasEnable = true +``` +所以的 http 请求都会自动进行 escape 编码,防止 xss 攻击。若想在某些极端场景下获取原始内容,只需要我们在 Controller 里通过 +getOriginalPara('key'); 既可以获得原始的内容,此时,需要自己进行 XSS 内容过滤或者对内容进行安全编码。 + +#### 增强功能4:更加强大的验证器功能 - 建议基于 Jboot 开发的应用,都继承至 JbootController。(备注:只是建议、而非必须。) +详情:http://www.jboot.com.cn/docs/validator.html + +#### 更多的增强功能 + +Jboot 还对 web 模块做了许多其他的增强,比如 1、分布式 session 的支持; 2、json 增强,前端传入 json 内容可以直接注入到 model 或者 bean; +3、更多的模板指令,比如前端的分页指令等。4、@EnableCORS 对跨域的支持。 5、更加方便的枚举类在 模板 里的使用。6、提供 @GetRequest、@PostRequest +对 Controller 方法的限制 等等等等。 ## Action @@ -40,9 +169,6 @@ Action 的相关文档请参考: [https://www.jfinal.com/doc/3-2](https://www. Interceptor 拦截器的相关文档请参考 [https://www.jfinal.com/doc/4-2](https://www.jfinal.com/doc/4-2) -## FixedInterceptor :永久拦截器 - -FixedInterceptor 的用法和 Interceptor 一样,但是 `FixedInterceptor` 不会被 `@Clear` 清除,常用于给 Controller 或者 Service 的注解进行增强。 ## Handler : 处理器 @@ -54,80 +180,69 @@ Render 请参加 JFinal 的文档 [https://www.jfinal.com/doc/3-7](https://www.j ## Session -Jboot 增强了 JFinal 的 Session 功能,同时 Session 默认使用了 Jboot 自带的缓存实现,当 Jboot 的缓存使用分布式缓存之后(比如 redis )。Session 就会自动有了分布式 Session 的功能。 +Jboot 增强了 JFinal 的 Session 功能,同时 Session 默认使用了 Jboot 自带的缓存实现,当 Jboot 开启分布式缓存之后(比如 redis )。Session 就会自动有了分布式 Session 的功能。 -当然,也可以通过如下来配置 Session 特殊功能: +开启分布式缓存,值需要添加如下配置: -``` -jboot.web.session.cookieName #cookie 的名称 -jboot.web.session.cookieDomain #cookie 的域名 -jboot.web.session.cookieContextPath #cookie 的路径 -jboot.web.session.maxInactiveInterval #cookie 的刷新时间 -jboot.web.session.cookieMaxAge #cookie 的有效时间 -jboot.web.session.cacheName #Session的缓存名称 -jboot.web.session.cacheType #Session的缓存类型(不配置的情况使用jboot的默认缓存) -``` +```properties +jboot.cache.type = redis +jboot.cache.redis.host = 127.0.0.1 +jboot.cache.redis.port = 3306 +jboot.cache.redis.password +jboot.cache.redis.database +jboot.cache.redis.timeout +``` -# Jwt +> 更多关于缓存的配置请参考【缓存】章节 - Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。 -**JWT的方法:** +添加以上配置后,我们在 Controller 中就可以使用如下代码操作 Session 了。 -|方法调用 | 描述 | -| ------------- | -----| -| setJwtAttr()| 设置 jwt 的 key 和 value | -| setJwtMap()| 把整个 map的key和value 设置到 jwt | -| getJwtAttr()| 获取 已经设置进去的 jwt 信息 | -| getJwtAttrs()| 获取 所有已经设置进去的 jwt 信息| -| getJwtPara()| 获取客户端传进来的 jwt 信息,若 jwt 超时或者不被信任,那么获取到的内容为null | +```java +@RequestMapping("/") +public class MyController extends JbootController { + public void index() { + + //设置 session 内容 + setSessionAttr("attr", "your session value"); -**JWT的相关配置** + renderText("hello world"); + } +} +``` -|配置属性 | 描述 | -| ------------- | -----| -| jboot.web.jwt.httpHeaderName| 配置JWT的http头的key,默认为JWT | -| jboot.web.jwt.secret | 配置JWT的密钥 | -| jboot.web.jwt.validityPeriod | 配置JWT的过期时间,默认不过期 | +当然,也可以通过如下来对 Session 进行更多的配置: +``` +jboot.web.session.cookieName #cookie 的名称 +jboot.web.session.cookieDomain #cookie 的域名 +jboot.web.session.cookieContextPath #cookie 的路径 +jboot.web.session.maxInactiveInterval #cookie 的刷新时间 +jboot.web.session.cookieMaxAge #cookie 的有效时间 +jboot.web.session.cacheName #Session的缓存名称 +jboot.web.session.cacheType #Session的缓存类型(不配置的情况使用jboot的默认缓存) +``` -## Validate : 验证器 +## Cookie -Jboot 提供了一些列的 validate 注解,方便用户对 Controller 进行数据验证。 +Jboot 增强了 JFinal 的 Cookie 功能,同时提供了 CookieUtil 工具类,用于对 Cookie 进行加密安全保护 Cookie 信息安全。 -- CaptchaValidate 对验证码进行验证 -- EmptyValidate 对空内容进行验证 -- UrlParaValidate 对URl参数内容进行验证 +```java -使用方法: +//设置 Cookie 数据 +CookieUtil.put(controller,"key","value"); -```java -@RequestMapping("/validate") -public class ValidateController extends Controller { +//读取 Cookie 数据 +CookieUtil.get(controller,"key") +``` - public void index(){ - renderText("index"); - } +```properties +jboot.web.cookieEncryptKey = cookie安全秘钥 +jboot.web.cookieMaxAge = 60 * 60 * 24 * 2 +``` - // 访问 /validate/test1 不通过,必须是 /validate/test1/data 才会通过 - @UrlParaValidate - public void test1(){ - renderText("test1"); - } - // 访问 /validate/test2 不通过,浏览器会显示内容 :test2 was verification failed - @UrlParaValidate(renderType = ValidateRenderType.TEXT,message = "test2 was verification failed") - public void test2(){ - renderText("test2"); - } - // 访问 /validate/test3 不通过,必须传入 form 数据 - @EmptyValidate(value = @Form(name = "form"),renderType = ValidateRenderType.JSON) - public void test3(){ - renderText("test3"); - } -} -``` \ No newline at end of file diff --git a/doc/docs/quickstart.md b/doc/docs/quickstart.md index 935e4d74c1b5df8a1cd1c6f195863f4a8f029971..a72bf0ab4fe373e41079806c35b2ca43fc679875 100644 --- a/doc/docs/quickstart.md +++ b/doc/docs/quickstart.md @@ -28,7 +28,7 @@ io.jboot jboot - 3.0.1 + 4.1.0 ``` diff --git a/doc/docs/readme.md b/doc/docs/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..90522646b26f9a7fbae25e90920976776874c032 --- /dev/null +++ b/doc/docs/readme.md @@ -0,0 +1,67 @@ +# Jboot 简介 + +[[toc]] + +## 简介 + +Jboot 是一个基于 JFinal、JFinal-Undertow、Dubbo、Seata、Sentinel、ShardingSphere、Nacos 等开发的微服务框架, +帮助开发者降低微服务开发门槛。同时完美支持在 idea、eclipse 下多 maven 模块,对 java 代码、html、css、js 等资源文件进行热加载。爽爽开发,快乐生活。 + + + +## 特点 + +目前已经开源超过了 3 年的时间,迭代了 100+ 个版本,已经被超过 1000+ 公司在使用。 + +Jboot 主要有以下特征: + +- 1、基于 JFinal 的 MVC + ORM 快速开发。 +- 2、基于 ShardingSphere + Seata 分布式事务 和 分库分表。 +- 3、基于 Dubbo 或 Motan 的 RPC 实现 +- 4、基于 Sentinel 的分布式限流和降级 +- 5、基于 Apollo 和 Nacos 的分布式配置中心 +- 6、基于 EhCache 和 Redis 的分布式二级缓存 + +## 微信群 + +![](./static/images/jboot-wechat-group.png) + +## QQ群 + +群1: 601440615(已满) +群2: 719614554 + +## 文档目录 + +- [安装](install.md) +- [2分钟快速开始](quickstart.md) +- [热加载](hotload.md) +- [Undertow](undertow.md) +- [配置](config.md) +- [JFinalConfig](jfinalConfig.md) +- [WebSocket](websocket.md) +- [MVC](mvc.md) +- [AOP](aop.md) +- [数据库操作](db.md) +- [缓存](cache.md) +- [RPC远程调用](rpc.md) +- [MQ消息队列](mq.md) +- [Gateway 网关](gateway.md) +- [任务调度](schedule.md) +- [限流](limit.md) +- [监控](metrics.md) +- [序列化](serialize.md) +- [事件机制](event.md) +- [SPI扩展机制](spi.md) +- [代码生成器](codegen.md) +- [项目构建](build.md) +- [项目部署](deploy.md) +- [Jboot与Docker](docker.md) +- [1.x 升级到 2.x 教程](upgrade.md) +- [交流社区、QQ群和微信群](communication.md) +- 第三方组件的支持 + - [sentinel 限流降级](sentinel.md) + - [redis](redis.md) + - [shiro](shiro.md) + - [jwt](jwt.md) + - [swagger](swagger.md) diff --git a/doc/docs/redis.md b/doc/docs/redis.md index e4c1d67de4077c13b22237846454ada5440c3f30..68a1c7ecf11347ea8e87c1c5cdcd93a24f40e6e2 100644 --- a/doc/docs/redis.md +++ b/doc/docs/redis.md @@ -29,14 +29,14 @@ Redis 优势: ## Redis的配置 在使用 Redis 之前,先进行 Redis 配置,配置内容如下: -``` +```properties jboot.redis.host=127.0.0.1 jboot.redis.password=xxxx ``` Redis 还支持如下的更多功能的配置: -``` +```properties jboot.redis.port = 6379 jboot.redis.timeout = 2000 jboot.redis.database @@ -62,7 +62,7 @@ jboot.redis.serializer 配置后,就可以通过如下代码获取 JbootRedis 对redis进行操作: -``` +```java JbootRedis redis = Jboot.getRedis(); redis.set("key1","value1"); @@ -176,17 +176,17 @@ JbootRedis 是通过 `jedis` 或者 `JedisCluster` 进行操作的,如果想 JbootRedis redis = Jboot.me().getReids(); //单机模式下 -JbootRedisImpl redisImpl = (JbootRedisImpl)redis; +JbootJedisImpl redisImpl = (JbootJedisImpl)redis; Jedis jedis = redisImpl.getJedis(); //集群模式下 -JbootClusterRedisImpl redisImpl = (JbootClusterRedisImpl)redis; +JbootJedisClusterImpl redisImpl = (JbootJedisClusterImpl)redis; JedisCluster jedis = redisImpl.getJedisCluster(); ``` ## Redis集群 在单机模式下,配置文件如下: -``` +```properties jboot.redis.host=127.0.0.1 jboot.redis.password=xxxx ``` @@ -194,7 +194,7 @@ jboot.redis.password=xxxx 在集群模式下,只需要在 jboot.redis.host 配置为多个主机即可,例如: -``` +```properties ## 多个IP用英文逗号隔开,端口号用英文冒号(:)。 Jboot.redis.host=192.168.1.33,192.168.1.34:3307,192.168.1.35 jboot.redis.password=xxxx diff --git a/doc/docs/rpc.md b/doc/docs/rpc.md index 30116bc32ed3e49055405ec2851d1c6f50406705..349e61365a08d91518152dde3f826139f154c4b4 100644 --- a/doc/docs/rpc.md +++ b/doc/docs/rpc.md @@ -1,29 +1,136 @@ # RPC 远程调用 +## 说明 + +此文档只适用于 jboot v3.1.0 以上,之前的版本请参考 [这里](./rpc2.x.md) 。 + ## 目录 +- 添加依赖 - 配置 - 开始使用 +- restful 暴露 - 高级功能 +## 添加依赖 + +Jboot 支持 dubbo 和 motan,假设我们需要使用 dubbo 作为底层的 RPC 框架,需要添加如下依赖: + +```xml + + org.apache.dubbo + dubbo + ${dubbo.version} + + + org.springframework + spring-context + + + com.alibaba.spring + spring-context-support + + + + + + org.apache.dubbo + dubbo-dependencies-zookeeper + ${dubbo.version} + pom + +``` + +如果使用 motan,需要添加如下依赖: + +```xml + + com.weibo + motan-core + ${motan.version} + + + + com.weibo + motan-transport-netty4 + ${motan.version} + + + + + com.weibo + motan-registry-consul + ${motan.version} + + + + com.weibo + motan-registry-zookeeper + ${motan.version} + +``` + ## 配置 -在 Jboot 中,默认实现了对 Dubbo、motan 的 RPC 调用支持。在使用 RPC 远程调用之前,需要做一些基本的配置。 +在 Jboot 中默认实现了对 Dubbo、motan 的 RPC 调用支持。在使用 RPC 远程调用之前,需要做一些基本的配置。 例如 : -``` + +```properties jboot.rpc.type = dubbo -jboot.rpc.callMode = direct -jboot.rpc.directUrl = 127.0.0.1:8000 +jboot.rpc.urls = com.yourdomain.AAAService:127.0.0.1:8080,com.yourdomain.XXXService:127.0.0.1:8080 +jboot.rpc.providers = com.yourdomain.AAAService:providerName,com.yourdomain.XXXService:providerName +jboot.rpc.consumers = com.yourdomain.AAAService:consumerName,com.yourdomain.XXXService:consumerName +jboot.rpc.defaultVersion = 1.0.0 +jboot.rpc.versions = com.yourdomain.AAAService:1.0.0,com.yourdomain.XXXService:1.0.1 +jboot.rpc.defaultGroup = +jboot.rpc.groups = com.yourdomain.AAAService:group1,com.yourdomain.XXXService:group2 +jboot.rpc.autoExportEnable = true +``` + +- jboot.rpc.type : RPC 的类型,目前只支持 dubbo 和 motan +- jboot.rpc.urls : 一般不用配置,只有直连模式下才会去配置,此处是配置 Service接口和URL地址的映射关系。 +- jboot.rpc.providers : 配置 Service 和 Provider 的映射关系( Motan下配置的是 Service 和 BasicService 的映射关系)。 +- jboot.rpc.consumers : 配置 Reference 和 consumer 的映射关系( Motan下配置的是 Referer 和 BaseReferer 的映射关系)。 +- jboot.rpc.defaultVersion : 当service不配置版本时,默认的版本号,默认值为 1.0.0 +- jboot.rpc.versions : 每个服务对应的版本号 +- jboot.rpc.defaultGroup : 当服务不配置 group 时,默认的 gourp +- jboot.rpc.groups : 每个服务对应的 group +- jboot.rpc.autoExportEnable : 当 Jboot 启动的时候,是否自动暴露 @RPCBean 注解的接口。 + +在 以上 示例中,`jboot.rpc.providers` 配置中,可以对每个 Service 进行配置,但是,在绝大多数的情况下,我们可能只需要一个配置,这个配置应用于所有的 Service 服务,此时我们需要做如下配置: + +```properties +# 名称为 default 的 provider 配置(当不配置其名称的时候,名称默认为 default) +jboot.rpc.dubbo.provider.timeout = xx.xx.xx +jboot.rpc.dubbo.provider.loadbalance = xx.xx.xx +jboot.rpc.dubbo.provider.group = xx.xx.xx +jboot.rpc.dubbo.provider.host = xx.xx.xx +jboot.rpc.dubbo.provider.default = true #设置当前 provider 为默认配置,既所有未指定 provider 的 service 服务都使用此配置。 + +# 名称为 name1 的 provider 配置 +jboot.rpc.dubbo.provider.name1.timeout = xx.xx.xx +jboot.rpc.dubbo.provider.name1.loadbalance = xx.xx.xx +jboot.rpc.dubbo.provider.name1.group = xx.xx.xx +jboot.rpc.dubbo.provider.name1.host = xx.xx.xx + +# 名称为 name2 的 provider 配置 +jboot.rpc.dubbo.provider.name2.timeout = xx.xx.xx +jboot.rpc.dubbo.provider.name2.loadbalance = xx.xx.xx +jboot.rpc.dubbo.provider.name2.group = xx.xx.xx +jboot.rpc.dubbo.provider.name2.host = xx.xx.xx + + +# 配置 com.yourdomain.AAAService 使用的 provider 配置为 name1 +# 配置 com.yourdomain.AAAService 使用的 provider 配置为 name2 +# 其他所有服务的 provider 配置使为 default,原因是名称为 default 的 provider 其属性 default 为 true 了 +# 此处要注意,如果我们给 name2 的 provider 添加配置 jboot.rpc.dubbo.provider.name2.default = true, +# 那么所有的未配置 providers 的服务都使用 name2 作为其默认位置。 +jboot.rpc.providers = com.yourdomain.AAAService:name1,com.yourdomain.XXXService:name2 ``` -- jboot.rpc.type : RPC 的类型,不配置默认为 Dubbo -- jboot.rpc.callMode : RPC 的调用方式 - - direct : 直联模式 - - registry : 注册中心模式(服务注册和服务发现) -- jboot.rpc.directUrl : 当 callMode 为 direct 直联模式时,需要配置直联模式的服务器 IP 地址和端口号。 +provider 的更多配置情况参考:https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-provider/ -> 更多的配置请查看 [config.md](./config.md) ## 开始使用 @@ -31,14 +138,13 @@ jboot.rpc.directUrl = 127.0.0.1:8000 - 1、定义接口 - 2、编写实现类 -- 3、启动 Server 暴露服务 +- 3、启动 Server(Provider) 暴露服务 - 4、启动客户端、通过 RPC 调用 Server 提供的服务 **定义接口** ```java - public interface BlogService { public String findById(); @@ -48,9 +154,8 @@ public interface BlogService { **编写实现类** -```java - +```java @RPCBean public class BlogServiceProvider implements BlogService { @@ -66,13 +171,33 @@ public class BlogServiceProvider implements BlogService { } ``` +### Dubbo **启动 Server 暴露服务** -```java +```java public class DubboServer { public static void main(String[] args) { + + JbootApplication.setBootArg("undertow.port", "9998"); + + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + + //dubbo 的通信协议配置 + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name", "dubbo"); + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port", "28080"); + + + // dubbo 注册中心的配置, + // 当不配置注册中心的时候,默认此服务只提供了直联模式的请求 + // JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "zookeeper"); + // JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:2181"); + JbootApplication.run(args); System.out.println("DubboServer started..."); @@ -81,17 +206,27 @@ public class DubboServer { } ``` +>备注:以上的 JbootApplication.setBootArg() 里设置的内容,都可以配置到 jboot.properties 里。例如: `JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true);` 在 jboot.properties +>里对应的配置是 `jboot.rpc.autoExportEnable = true` 。 -**启动客户端、通过 RPC 调用 Server 提供的服务** -```java + +**启动 dubbo 客户端、通过 RPC 调用 Server 提供的服务** + +```java @RequestMapping("/dubbo") public class DubboClient extends JbootController{ public static void main(String[] args) { - //Undertow端口号配置 - JbootApplication.setBootArg("undertow.port", "8888"); + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "9999"); + + //RPC配置 + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + //设置直连模式,方便调试,默认为注册中心 + JbootApplication.setBootArg("jboot.rpc.urls", "io.jboot.test.rpc.commons.BlogService:127.0.0.1:28080"); JbootApplication.run(args); } @@ -104,53 +239,426 @@ public class DubboClient extends JbootController{ System.out.println(blogService); renderText("blogId : " + blogService.findById()); } - } ``` +### Motan -## 高级功能 +**Motan 服务端** -在以上的示例中,使用到了两个注解: -- @RPCBean,标示当前服务于RPC服务 -- @RPCInject,RPC注入赋值 +```java +public class MotanServer { -虽然以上示例中,`@RPCBean` 和 `@RPCInject` 没有添加任何的参数,但实际是他们提供了非常丰富的配置。 + public static void main(String[] args) throws InterruptedException { -以下分别是 `@RPCBean` 和 `@RPCInject` 的定义: -RPCBean : -```java + JbootApplication.setBootArg("jboot.rpc.type", "motan"); + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + + // motan 与 dubbo 不一样,motan 需要配置 export, + // export 配置内容为 协议ID:端口号,默认的协议 id 为 default + JbootApplication.setBootArg("jboot.rpc.motan.defaultExport", "default:28080"); -public @interface RPCBean { + // motan 的注册中心的协议 + // JbootApplication.setBootArg("jboot.rpc.motan.registry.regProtocol", "zookeeper"); + //注册中心地址,即zookeeper的地址 + // JbootApplication.setBootArg("jboot.rpc.motan.registry.address", "127.0.0.1:2181"); - int port() default 0; - int timeout() default -1; - int actives() default -1; - String group() default ""; - String version() default ""; - //当一个Service类实现对个接口的时候, - //可以通过这个排除不暴露某个实现接口 - Class[] exclude() default Void.class; + + + JbootSimpleApplication.run(args); + + System.out.println("MotanServer started..."); + + } } + ``` -RPCInject : +**Motan 客户端** ```java +@RequestMapping("/motan") +public class MotanClient extends JbootController { + + public static void main(String[] args) { + + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "9999"); + + //RPC配置 + JbootApplication.setBootArg("jboot.rpc.type", "motan"); + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", false); + + //设置直连模式,方便调试,默认为注册中心 + JbootApplication.setBootArg("jboot.rpc.urls", "io.jboot.test.rpc.commons.BlogService:127.0.0.1:28080"); + + + // motan 的注册中心的协议 + // JbootApplication.setBootArg("jboot.rpc.motan.registry.regProtocol", "zookeeper"); + //注册中心地址,即zookeeper的地址 + // JbootApplication.setBootArg("jboot.rpc.motan.registry.address", "127.0.0.1:2181"); + + JbootApplication.run(args); + } + + + @RPCInject + private BlogService blogService; + +// @Before(MotanInterceptor.class) + public void index() { + + System.out.println("blogService:" + blogService); + + renderText("blogId : " + blogService.findById()); + } + + +} +``` + + +## restful 暴露 + +在某些情况下,我们希望 rpc service 通过 restful 协议暴露给其他客户端(或者其他编程语言)去使用,我们需要添加如下的依赖。 + +PS:目前只有 dubbo 支持了 restful 协议,其他 rpc 框架暂时不支持。 + + +```xml + + + io.netty + netty-all + ${io.netty.version} + + + + org.jboss.resteasy + resteasy-jaxrs + ${org.jboss.resteasy.version} + + + + org.jboss.resteasy + resteasy-client + ${org.jboss.resteasy.version} + + + + org.jboss.resteasy + resteasy-netty4 + ${org.jboss.resteasy.version} + + + + javax.validation + validation-api + 1.1.0.Final + + + + org.jboss.resteasy + resteasy-jackson-provider + ${org.jboss.resteasy.version} + + + + org.jboss.resteasy + resteasy-jaxb-provider + ${org.jboss.resteasy.version} + + +``` + +其中版本号对应为 + +```xml +4.1.9.Final +3.9.0.Final +``` + +第二步需要在 接口添加 相关注解 -public @interface RPCInject { - - int port() default 0; - int timeout() default -1; - int retries() default -1; - int actives() default -1; - String group() default ""; - String version() default ""; - String loadbalance() default ""; - String async() default ""; - String check() default ""; +```java +@Path("users") // #1 +@Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_XML}) // #2 +@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML}) +public interface UserService { + @GET // #3 + @Path("{id: \\d+}") + User getUser(@PathParam("id") Long id); + + @POST // #4 + @Path("register") + Long registerUser(User user); } -``` \ No newline at end of file +``` + +这部分的内容,可以添加在接口里,也可以添加在实现类里,具体参考:http://dubbo.apache.org/zh-cn/blog/dubbo-rest.html + + +第三步,在 jboot.properties 添加 restful 协议: + +```properties +jboot.rpc.dubbo.protocol.name = dubbo +jboot.rpc.dubbo.protocol.host = 127.0.0.1 +jboot.rpc.dubbo.protocol.port = 28080 + +jboot.rpc.dubbo.protocol.rest.name = rest +jboot.rpc.dubbo.protocol.rest.host = 127.0.0.1 +jboot.rpc.dubbo.protocol.rest.port = 8080 +jboot.rpc.dubbo.protocol.rest.server = netty +``` + +第四步:给 Service 配置暴露协议 + + +```properties +jboot.rpc.dubbo.provider.protocal = default,rest //使用 dubbo 和 rest 两种协议同时暴露 +jboot.rpc.dubbo.provider.default = true // 给应用配置默认的 provider +``` + + + +## 高级功能 + +### 更多的 dubbo 配置 + +目前,jboot 不支持通过 jboot.properties 直接对 service 和 reference 配置,但是可以对 provider 和 consumer 的配置, +在通过 @RPCInject 来再次复制给 service 或者 reference,结果也等同于对 service 和 reference 配置进行配置了。 + + +#### application + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-application/ + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.application` + +例如: + +```properties +jboot.rpc.dubbo.application.name = xx.xx.xx +jboot.rpc.dubbo.application.version = xx.xx.xx +``` + + +#### registry + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-registry/ + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.registry` + +例如: + +```properties +jboot.rpc.dubbo.registry.address = xx.xx.xx +``` + +> 例如使用 ncaos 注册中心时,配置内容如下: +> +> `jboot.rpc.dubbo.registry.address=nacos://127.0.0.1:8848?namespace=test` + + + + +更多的注册中心配置内容如下: + +```properties +jboot.rpc.dubbo.registry.address = xx.xx.xx +jboot.rpc.dubbo.registry.port = xx.xx.xx +jboot.rpc.dubbo.registry.username = xx.xx.xx +jboot.rpc.dubbo.registry.password = xx.xx.xx +``` + +**多个注册中心** + +多注册中心可以配置如下(多 protocol 、多 consumer、多provider 都是同理): + + +```properties +jboot.rpc.dubbo.registry.address = xx.xx.xx +jboot.rpc.dubbo.registry.port = xx.xx.xx +jboot.rpc.dubbo.registry.username = xx.xx.xx +jboot.rpc.dubbo.registry.password = xx.xx.xx + + +jboot.rpc.dubbo.registry.other1.address = xx.xx.xx +jboot.rpc.dubbo.registry.other1.port = xx.xx.xx +jboot.rpc.dubbo.registry.other1.username = xx.xx.xx +jboot.rpc.dubbo.registry.other1.password = xx.xx.xx + + +jboot.rpc.dubbo.registry.other2.address = xx.xx.xx +jboot.rpc.dubbo.registry.other2.port = xx.xx.xx +jboot.rpc.dubbo.registry.other2.username = xx.xx.xx +jboot.rpc.dubbo.registry.other2.password = xx.xx.xx +``` + +这样,在系统中就存在了多个注册中心,第一个配置的名称(name)分别为 default,第二个和第三个为 +other1、other2,这样,当一个服务(或者接口)需要在多个注册中心暴露的时候,只需要在其 registry 配置相应的 name 即可。 + +例如: + +```properties +jboot.rpc.dubbo.provider.address = default,other1 +``` + +若当服务没有指定注册中心,注册中心默认为 default。 + + +#### protocol + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-protocol/ + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.protocol` + +例如: + +```properties +jboot.rpc.dubbo.protocol.host = 127.0.0.1 +jboot.rpc.dubbo.protocol.port = 28080 +``` + + + +#### provider + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-provider/ + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.provider` + +例如: + +``` +jboot.rpc.dubbo.provider.host = 127.0.0.1 +``` + + + +#### consumer + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-consumer/ + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.consumer` + +例如: + +``` +jboot.rpc.dubbo.consumer.timeout = 127.0.0.1 +``` + + + +#### monitor + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-monitor/ + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.monitor` + +例如: + +``` +jboot.rpc.dubbo.monitor.protocol = xxx +``` + + +#### metrics + + +对应的配置类: org.apache.dubbo.config.MetricsConfig + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.metrics` + +例如: + +``` +jboot.rpc.dubbo.metrics.protocol = xxx +``` + + +#### module + + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-module/ + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.module` + +例如: + +``` +jboot.rpc.dubbo.module.name = xxx +``` + + +#### MetadataReport + + +对应的配置类: org.apache.dubbo.config.MetadataReportConfig + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.metadata-report` + +例如: + +``` +jboot.rpc.dubbo.metadata-report.group = xxx +``` + + +#### ConfigCenter + + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-config-center/ + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.config-center` + +例如: + +``` +jboot.rpc.dubbo.config-center.group = xxx +``` + +#### method + + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-method/ + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.method` + +例如: + +``` +jboot.rpc.dubbo.method.name = xxx +``` + +#### argument + + +https://dubbo.apache.org/zh/docs/v2.7/user/references/xml/dubbo-argument/ + + +对应的 jboot 的配置前缀为: `jboot.rpc.dubbo.argument` + +例如: + +``` +jboot.rpc.dubbo.argument.name = xxx +``` + diff --git a/doc/docs/rpc2.x.md b/doc/docs/rpc2.x.md new file mode 100644 index 0000000000000000000000000000000000000000..30116bc32ed3e49055405ec2851d1c6f50406705 --- /dev/null +++ b/doc/docs/rpc2.x.md @@ -0,0 +1,156 @@ +# RPC 远程调用 + +## 目录 + +- 配置 +- 开始使用 +- 高级功能 + +## 配置 +在 Jboot 中,默认实现了对 Dubbo、motan 的 RPC 调用支持。在使用 RPC 远程调用之前,需要做一些基本的配置。 + + +例如 : +``` +jboot.rpc.type = dubbo +jboot.rpc.callMode = direct +jboot.rpc.directUrl = 127.0.0.1:8000 +``` + +- jboot.rpc.type : RPC 的类型,不配置默认为 Dubbo +- jboot.rpc.callMode : RPC 的调用方式 + - direct : 直联模式 + - registry : 注册中心模式(服务注册和服务发现) +- jboot.rpc.directUrl : 当 callMode 为 direct 直联模式时,需要配置直联模式的服务器 IP 地址和端口号。 + +> 更多的配置请查看 [config.md](./config.md) + +## 开始使用 + +一般情况下,RPC 调用需要以下几个步骤: + +- 1、定义接口 +- 2、编写实现类 +- 3、启动 Server 暴露服务 +- 4、启动客户端、通过 RPC 调用 Server 提供的服务 + + +**定义接口** + +```java + +public interface BlogService { + + public String findById(); + public List findAll(); +} +``` + +**编写实现类** + +```java + + +@RPCBean +public class BlogServiceProvider implements BlogService { + + @Override + public String findById() { + return "id from provider"; + } + + @Override + public List findAll() { + return Lists.newArrayList("item1","item2"); + } +} +``` + +**启动 Server 暴露服务** + +```java + +public class DubboServer { + + public static void main(String[] args) { + + JbootApplication.run(args); + System.out.println("DubboServer started..."); + + } +} +``` + + +**启动客户端、通过 RPC 调用 Server 提供的服务** +```java + +@RequestMapping("/dubbo") +public class DubboClient extends JbootController{ + + public static void main(String[] args) { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8888"); + + JbootApplication.run(args); + } + + + @RPCInject + private BlogService blogService; + + public void index() { + System.out.println(blogService); + renderText("blogId : " + blogService.findById()); + } + +} +``` + + +## 高级功能 + +在以上的示例中,使用到了两个注解: +- @RPCBean,标示当前服务于RPC服务 +- @RPCInject,RPC注入赋值 + +虽然以上示例中,`@RPCBean` 和 `@RPCInject` 没有添加任何的参数,但实际是他们提供了非常丰富的配置。 + +以下分别是 `@RPCBean` 和 `@RPCInject` 的定义: + +RPCBean : +```java + +public @interface RPCBean { + + int port() default 0; + int timeout() default -1; + int actives() default -1; + String group() default ""; + String version() default ""; + + //当一个Service类实现对个接口的时候, + //可以通过这个排除不暴露某个实现接口 + Class[] exclude() default Void.class; +} +``` + + +RPCInject : + +```java + +public @interface RPCInject { + + int port() default 0; + int timeout() default -1; + int retries() default -1; + int actives() default -1; + String group() default ""; + String version() default ""; + String loadbalance() default ""; + String async() default ""; + String check() default ""; +} +``` \ No newline at end of file diff --git a/doc/docs/schedule.md b/doc/docs/schedule.md index 6ff2bada49dff8d0bc35b7ea3ac7e72e45640ad8..5c09c61a07f0f0a4e396d8d50715731d070b6fac 100644 --- a/doc/docs/schedule.md +++ b/doc/docs/schedule.md @@ -53,6 +53,8 @@ public class MyTask implements Runnable { } } ``` +注意:由于 jdk `ScheduledThreadPoolExecutor` 自身实现的问题,任务的 run 方法如果抛出异常,会造成线程池停止调度, +请务必在任务的 run 方法中使用 try catch 自行捕捉异常。 **方案3:** 使用 JFinal 自带的任务调度方案,参考文档:https://www.jfinal.com/doc/9-1 diff --git a/doc/docs/sentinel.md b/doc/docs/sentinel.md new file mode 100644 index 0000000000000000000000000000000000000000..193edac687c39d5da84a1fb0b2356263b789f237 --- /dev/null +++ b/doc/docs/sentinel.md @@ -0,0 +1,171 @@ +# Sentinel 限流 + +## 目录 + +- 概述 +- sentinel 的使用 + +## 概述 +随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 + +Sentinel 具有以下特征: + +- **丰富的应用场景**:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。 +- **完备的实时监控**:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。 +- **广泛的开源生态**:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。 +- **完善的 SPI 扩展点**:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。 + + + +Sentinel 分为两个部分: + +核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。 +控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。 + + +更多的文档请参考: + +https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D + + +## sentinel 的使用 + +**第一步,启动 Sentinel dashboard:** + +下载 Sentinel 的 jar 到本地,并通过如下方式启动 启动 sentinel dashboard + +```shell +java -jar sentinel-dashboard-1.8.0.jar +``` + + jar 的下载地址:https://github.com/alibaba/Sentinel/releases + +启动时,默认端口号为:8080,可以通过 -Dserver.port=8888 用于指定 Sentinel 控制台端口为 8888。 + +例如: + +```shell +java -Dserver.port=8888 -jar sentinel-dashboard-1.8.0.jar +``` + +从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel。 + +可以通过如下配置来修改掉默认的账号和密码: +- -Dsentinel.dashboard.auth.username=sentinel 用于指定控制台的登录用户名为 sentinel; +- -Dsentinel.dashboard.auth.password=123456 用于指定控制台的登录密码为 123456; + +关于控制台的更多配置,请参考: +https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0#%E6%8E%A7%E5%88%B6%E5%8F%B0%E9%85%8D%E7%BD%AE%E9%A1%B9 + +**第二步:配置项目的 jboot.properties 、 sentinel.properties 和 Maven 依赖** + +在 jboot.properties 添加如下配置 + +```properties +jboot.sentinel.enable = true; + +// 是否对 http 请求启用限流,默认值为 true,启用后还需要去 sentinel 后台配置 +jboot.sentinel.reqeustEnable = true; + +// 配置 sentinel 的前缀,多个用英文逗号隔开,若不配置则所有的都会被拦截 +jboot.sentinel.reqeustTargetPrefix = "/my"; + +// 如果 http 被限流后跳转的页面 +jboot.sentinel.requestBlockPage; + + // 如果 http 被限流后渲染的 json 数据,requestBlockPage 配置优先于此项 +jboot.sentinel.requestBlockJsonMap; +``` + +在项目的 resource 目录下创建 sentinel.properties 文本,并配置相关信息如下: + +``` +csp.sentinel.dashboard.server=localhost:8080 +``` +这个配置指的是 sentinel 配置服务器的地址 + +关于更多 sentinel.properties 的配置请参考: + +https://github.com/alibaba/Sentinel/wiki/%E5%90%AF%E5%8A%A8%E9%85%8D%E7%BD%AE%E9%A1%B9#%E5%9F%BA%E7%A1%80%E9%85%8D%E7%BD%AE%E9%A1%B9 + + +添加 maven 依赖: + + +```xml + + com.alibaba.csp + sentinel-core + ${sentinel.version} + + + + com.alibaba.csp + sentinel-cluster-client-default + ${sentinel.version} + + + + com.alibaba.csp + sentinel-transport-simple-http + ${sentinel.version} + +``` + +如果使用 阿里云 AHAS 替代 sentinel dashboard,需要添加如下依赖(以上依赖不再需要): + + +```xml + + com.alibaba.csp + ahas-sentinel-client + 1.8.0 + +``` + +阿里云 AHAS 截图: + +![](./static/images/ahas.png) + +更多关于 阿里云 AHAS 的文档请参考:https://github.com/alibaba/Sentinel/wiki/AHAS-Sentinel-%E6%8E%A7%E5%88%B6%E5%8F%B0 + + + +**第三步:配置限流资源** + +在项目的任意目录下,使用注解 `@SentinelResource` 给方法进行配置,代码如下: + +```java +@RequestMapping("/sentinel") +public class SentinelController extends JbootController { + + @SentinelResource + public void index(){ + renderText("sentinel index..."); + } +} +``` + +或者在 Service 中 + +```java +public class UserService{ + + // 原本的业务方法. + @SentinelResource(blockHandler = "blockHandlerForGetUser") + public User getUserById(String id) { + throw new RuntimeException("getUserById command failed"); + } + + // blockHandler 函数,原方法调用被限流/降级/系统保护的时候调用 + public User blockHandlerForGetUser(String id, BlockException ex) { + return new User("admin"); + } +} +``` + +然后通过浏览器进入 sentinel dashboard 中心,对 `SentinelController.index()` 和 `UserService.getUserById()` 的资源进行具体的限流参数配置。 + +关注注解 `@SentinelResource` 更多的配置,请参考文档: https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81#sentinelresource-%E6%B3%A8%E8%A7%A3 + +更多的 sentinel dashboard 配置内容请参考:https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0 \ No newline at end of file diff --git a/doc/docs/serialize.md b/doc/docs/serialize.md index 8f33c7dde99a50a574d5d011c1f258c8e3b2fd60..75cebf694946b23b8d593866d18f78add020f0fc 100644 --- a/doc/docs/serialize.md +++ b/doc/docs/serialize.md @@ -1,14 +1,22 @@ # 序列化 -在jboot中,很多分布式的调用,需要把 javaBean 序列化后才能进行传输 或者 进行缓存。比如 rpc、mq、cache 等都需要序列化组件。 +序列化 (Serialization) 是将 Java 对象转换为可以存储或传输的状态信息。在很多的场景下,比如缓存、分布式调用 RPC,MQ 等都需要到了序列化。才能把 Java 对象传输到另一个其他系统。 -例如,在 redis 中,我们可以为 redis 组件自定义自己的序列化方式,只需要进入如下配置即可: +在 Jboot 中,已经内置了多种序列化解决方案。 -``` -jboot.redis.serializer = xxxx +- fst +- kryo +- fastjson + +默认已经使用了 FST,当没有特殊需求的时候,使用默认的 fst 就可以了,但是在某些情况下,比如 redis 缓存已经使用了其他序列化方案进行存储数据了,我们要正确读取其数据,需要设置我们的序列化方案为 redis 已经使用的方案。 + +此时,我们可以通过如下的配置,来修改掉 redis 的序列化: + +```properties +jboot.redis.serializer = xxx ``` -自定义序列化是通过Jboot的SPI机制来实现的,我们只需要安装 SPI 规范来实现自己的序列化组件,然后通过以上配置即可。 +其中,xxx 是序列化的名称,倘若 Jboot 中不存在此序列化方案,需要用户自行通过 Jboot SPI 进行扩展, -更多关于 SPI 的查看 [这里](.spi.md) 。 +更多关于 SPI 的查看 [这里](./spi.md) 。 diff --git a/doc/docs/shiro.md b/doc/docs/shiro.md index b06fa86257453b889be2a7736c6b939e2f6c9bb0..45e5462a20f7748560275142b63b99a2916bde21 100644 --- a/doc/docs/shiro.md +++ b/doc/docs/shiro.md @@ -19,25 +19,24 @@ Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、 Jboot 默认情况下并没有依赖 shiro,因此,在在使用 Jboot 的 Shiro 模块之前,需要你添加下 Shiro 的 Maven 依赖。 ```xml - - org.apache.shiro - shiro-core - 1.3.2 + + org.apache.shiro + shiro-core + 1.7.1 - org.apache.shiro - shiro-web - 1.3.2 + org.apache.shiro + shiro-web + 1.7.1 ``` -注意:目前暂时不支持 Shiro 1.4.x 版本,晚点会添加支持。 同时,需要在 resources 目录下配置上您的 `shiro.ini` 配置文件,并在 `jboot.porperties` 添加上 `jboot.shiro.ini = shiro.ini` 配置。在 `shiro.ini` 文件里,需要在自行扩展 `realm` 等信息。 ## Shiro的使用 -Jboot 的 Shiro 模块为您提供了以下12个模板指令,同时支持 Shiro 的5个 Requires 注解功能。方便您使用 Shiro。 +Jboot 的 Shiro 模块为您提供了以下12个模板指令,同时支持 Shiro 的 5 个 Requires 注解功能。方便您使用 Shiro。 **12个Html模板指令** - shiroAuthenticated:用户已经身份验证通过,Subject.login登录成功 @@ -196,7 +195,7 @@ public class MyController extends JbootController{ } ``` -RequiresUser、RequiresGuest、RequiresAuthentication的使用 +RequiresUser、RequiresGuest、RequiresAuthentication 的使用 ```java public class MyController extends JbootController{ @@ -233,12 +232,12 @@ public class MyshiroListener implements JbootShiroInvokeListener { @Override - public void onInvokeBefore(FixedInvocation inv) { + public AuthorizeResult onInvokeBefore(Invocation inv) { //do nothing } @Override - public void onInvokeAfter(FixedInvocation inv, AuthorizeResult result) { + public void onInvokeAfter(Invocation inv, AuthorizeResult result) { //说明该用户授权成功, //可以允许访问 @@ -280,4 +279,12 @@ jboot.shiro.invokeListener=com.xxx.MyshiroListener ### Shiro 与 Jwt 整合 -### Shiro 与 SSO 整合 \ No newline at end of file + 在 `JbootShiroInvokeListener.onInvokeBefore()` 中,接收 JWT 数据,并 JWT 处理的相关认证,然后返回 + `AuthorizeResult`,返回的 `AuthorizeResult` 直接交给 `onInvokeAfter` 处理,不再交给 +Shiro 内部处理。 + +若 `JbootShiroInvokeListener.onInvokeBefore()` 返回 null,则交给 Shiro 内部处理 + +### Shiro 与 SSO 整合 + +同 Jwt 处理方案。 \ No newline at end of file diff --git a/doc/docs/spi.md b/doc/docs/spi.md index 9f3e3bf1f8e7212d54421e054ad86158c71298a5..9cac715a170242458e3b992a3a097390815110d7 100644 --- a/doc/docs/spi.md +++ b/doc/docs/spi.md @@ -15,7 +15,7 @@ SPI 的全名为 : Service Provider Interface。 当服务的提供者,提供了服务接口的一种实现之后,在 jar 包的`META-INF/services/` 目录里同时创建一个以 **服务接口** 命名的文件。该文件里就是实现该服务接口的具体实现类。而 Jboot 装配这个模块的时候,就能通过该 jar 包 `META-INF/services/` 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 ## Jboot SPI 模块 -在jboot中,一下模块已经实现了SPI机制。 +在jboot中,以下模块已经实现了SPI机制。 - Jbootrpc - JbootHttp @@ -34,4 +34,4 @@ SPI 的全名为 : Service Provider Interface。 - 3:通过在jboot.properties文件中配置上类型为 mycache,配置代码如下:`jboot.cache.type = mycache` -通过以上三步,我们就可以完成了对 JbootCache 模块的扩展,其他模块类似。 \ No newline at end of file +通过以上三步,我们就可以完成了对 JbootCache 模块的扩展,其他模块类似。 diff --git a/doc/docs/start.md b/doc/docs/start.md new file mode 100644 index 0000000000000000000000000000000000000000..1aa9efb9b96ee5322c9e8d0913766a84dcf41a07 --- /dev/null +++ b/doc/docs/start.md @@ -0,0 +1,845 @@ +--- +sidebar: false +--- + +# 开始使用Jboot + +Jboot 使用到了如下的技术,了解 Jboot 之前,请先保证您已经了掌握如下技术: + + * 熟悉 Java 编程语言 + * 熟悉 maven 的基本原理 + * 熟悉 IntelliJ IDEA 或者 Eclipse 等编辑器的使用 + +交流 QQ 群: +* 群1:601440615 (已满) +* 群2:719614554 (开放中) + +## 创建项目 + +### 通过 IntelliJ IDEA 创建项目 + +IntelliJ IDEA 下载地址:https://www.jetbrains.com/idea/ ,下载完成后完成后开始安装,安装过程略。 + +第一步:打开 IntelliJ IDEA 创建 maven 项目,如下图: +![](./static/images/idea_001.jpg "") + +第二步:填写 maven 项目的 GroupId、ArtifactId 和 Version + +* GroupId 一般是包名,用来做项目的唯一标识 +* ArtifactId 一般是项目名 +* Version 是项目的版本 +![](./static/images/idea_002.png "") + +第三步:填写 项目存储路径 + +![](./static/images/idea_003.png "") + +创建完毕后,我们会看到如下图所示,注意点击 Enable Auto-Import. + +![](./static/images/idea_004.png "") + +### 通过 Eclipse 创建项目 +略,和 通过 IntelliJ IDEA 创建项目 基本相同。 + +## Maven 依赖 + +通过 以上步骤建立项目后,我们会在项目目录下找到 pom.xml 文件,这个文件是 maven 的核心文件,maven 是通过 pom.xml 对项目进行依赖配置和管理的。 + +我们需要在 pom.xml 里添加对 Jboot 的依赖配置,如下代码: + +```xml + + io.jboot + jboot + 4.1.0 + +``` + +如下图所示: + +![](./static/images/idea_005.png "") + +## Hello World + +一般情况下,对一个新项目的了解是从 Hello World 开始的,因此,我们需要通过 Jboot 来写一个 Hello World 程序。 + +这个 Hello World 的需求是: + +> **通过编写代码,我们在浏览器访问后输出 “Hello World Jboot” 的文字内容。** + +通过以上步骤,我们创建好了项目、添加好了 jboot 的maven依赖,接下来我们需要来创建一个叫 IndexController 的java文件 + +![](./static/images/idea_006.png "") + +![](./static/images/idea_007.png "") + +IndexController 的代码如下: + +```java +import io.jboot.app.JbootApplication; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/") +public class IndexController extends JbootController { + + public void index() { + renderText("Hello World Jboot"); + } + + + public static void main(String[] args) { + JbootApplication.run(args); + } +} +``` + +以上代码需要注意以下几点: +1. IndexController 需要继承 JbootController 或者 Controller +1. 需要添加 @RequestMapping("/") 请求映射配置 +1. 通过编写 `index()` 方法来接收请求,并向浏览器输出 "Hello World Jboot" , 此处注意:必须叫 `index()` 不能修改名字,后续会讲到其原理。 + +通过运行 IndexController 的 main() 方法,我们可以看到如下的日志输出: + +``` + + ____ ____ ___ ___ ______ + | || \ / \ / \ | | + |__ || o )| || || | + __| || || O || O ||_| |_| +/ | || O || || | | | +\ ` || || || | | | + \____||_____| \___/ \___/ |__| + + + +JbootApplication { mode='dev', version='4.1.0', jfinalConfig='io.jboot.core.JbootCoreConfig' } +Classpath : /Users/michael/git/jboot/target/test-classes/ +Starting JFinal 4.2 -> http://127.0.0.1:8080 +Info: jfinal-undertow 1.6, undertow 2.0.19.Final, jvm 1.8.0_201 +ClassScanner scan classpath : /Users/michael/git/jboot/target/test-classes +ClassScanner scan classpath : /Users/michael/git/jboot/target/classes +Starting Complete in 1.3 seconds. Welcome To The JFinal World (^_^) + +JbootResourceLoader started, Watched resource path name : webapp +``` + +我们看到最后一行日志的内容是: + +``` +server started success , url : http://127.0.0.1:8080/ +``` +此时我们通过浏览器访问:http://127.0.0.1:8080 , 就可以看到如下内容: + +![](./static/images/0008.png "") + + + +## 链接数据库 + +在 Java Web 开发中,几乎 99% 以上的项目都需要和数据库打交道,因此,了解 Jboot 如何连接数据成为了必须。 + +通过 Jboot 连接数据库只需要做两步: +1. 创建 jboot.properties 配置文件 +1. 在 jboot.properties 添加数据库连接信息 + +第一步:在项目的resource目录下创建 jboot.properties 文件,此时,项目的目录结构应该如下: + +``` +├── pom.xml +├── src +│   ├── main +│   │   ├── java +│   │   │   └── IndexController.java +│   │   └── resources +│   │   └── jboot.properties #注意文件目录不要错了 +│   └── test +│   └── java +``` + +第二步:在 jboot.properties 文件添加如下数据库信息: + +``` +jboot.datasource.type = mysql +jboot.datasource.url = jdbc:mysql://127.0.0.1:3306/jbootdemo +jboot.datasource.user = root +jboot.datasource.password = +``` + +* jboot.datasource.type:配置的是数据库的类型,目前 Jboot 支持的数据库类型有:Mysql 、Oracle 、SqlServer 、Postgresql 、Sqlite 和 其他标准的数据库。 +* jboot.datasource.url: 配置的是数据库的连接信息 +* jboot.datasource.user: 配置的是数据库的连接账号 +* jboot.datasource.password: 配置的是数据库的连接密码,没有密码可以留空 + +通过配置完毕后,Jboot就已经有了访问数据库的能力,我们可以在 IndexController 写一个 `dbtest()` 方法,来测试下 Jboot 的数据库访问能力,代码如下: + +```java +@RequestMapping("/") +public class IndexController extends JbootController { + + public void index() { + renderText("Hello World Jboot"); + } + + public void dbtest(){ + List records = Db.find("select * from user"); + renderText(Arrays.toString(records.toArray())); + } + + + public static void main(String[] args) { + JbootApplication.run(args); + } +} +``` +注意:以上代码能够正式运行的前提是: +1. 你本地安装好mysql数据库,并创建好库 `jbootdemo` ,因为数据库的连接url是:`jdbc:mysql://127.0.0.1:3306/jbootdemo` +2. jbootdemo 下要有数据表 user,因为 sql 查询内容是:`select * from user` + +例如:作者本地数据库的内容如下: + +![](./static/images/0010.png "") + +运行 IndexController 的 `main()` 方法,并访问 `http://127.0.0.1:8080/dbtest`,会看到如下内容所示: + +![](./static/images/0009.png "") + +此时,证明 Jboot 已经能够准确访问数据库。 + + + +## 使用代码生成器 +Jboot 内置了一个简易的代码生成器,通过代码生成器运行,Jboot帮开发者生成每个数据库表对应 java 的 model 实体类,同时可以生成带有增、删、改、查基本数据库操作能力的 service 层代码。 + +在使用 Jboot 代码生成器之前,我们需要在 jboot.properties 配置好数据库的连接信息(以上内容已经提到)。并编写任意名字带有`main()`方法的执行类,例如我们叫 CodeGenerator, 代码如下: + +```java +public class CodeGenerator { + + public static void main(String args[]){ + + // 配置数据库的数据源 + JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://127.0.0.1:3306/jbootdemo"); + JbootApplication.setBootArg("jboot.datasource.user", "root"); + JbootApplication.setBootArg("jboot.datasource.password", "123456"); + + String modelPackage = "io.jboot.test.codegen.model"; + String baseModelPackage = modelPackage + ".base"; + + String modelDir = CodeGenHelpler.getUserDir() + "/src/main/java/" + modelPackage.replace(".", "/"); + String baseModelDir = CodeGenHelpler.getUserDir() + "/src/main/java/" + baseModelPackage.replace(".", "/"); + + System.out.println("start generate..."); + System.out.println("generate dir:" + modelDir); + + // 生成 Model + new JbootBaseModelGenerator(baseModelPackage, baseModelDir).setGenerateRemarks(true).generate(); + new JbootModelGenerator(modelPackage, baseModelPackage, modelDir).generate(); + + + String servicePackage = "io.jboot.test.codegen.service"; + String serviceImplPackage = "io.jboot.test.codegen.service.impl"; + + String serviceOutputDir = CodeGenHelpler.getUserDir() + "/src/main/java/" + servicePackage.replace(".", "/"); + String serviceImplOutputDir = CodeGenHelpler.getUserDir() + "/src/main/java/" + serviceImplPackage.replace(".", "/"); + + // 生成 Service 接口 及其 实现类 + new JbootServiceInterfaceGenerator(servicePackage, serviceOutputDir, modelPackage).generate(); + new JbootServiceImplGenerator(servicePackage, serviceImplPackage, serviceImplOutputDir, modelPackage).setImplName("impl").generate(); + + + } +} +``` + +运行 CodeGenerator 的 `main()` 方法之后,我们能看到 Jboot 已经帮我们创建好对应的包名和类名,此时,项目的目录如下: + +``` +├── pom.xml +├── src +│   ├── main +│   │   ├── java +│   │   │   ├── CodeGenerator.java +│   │   │   ├── IndexController.java +│   │   │   └── com +│   │   │   └── xxx +│   │   │   ├── model +│   │   │   │   ├── Article.java +│   │   │   │   ├── User.java +│   │   │   │   └── base +│   │   │   │   ├── BaseArticle.java +│   │   │   │   └── BaseUser.java +│   │   │   └── service +│   │   │   ├── ArticleService.java +│   │   │   ├── UserService.java +│   │   │   └── impl +│   │   │   ├── ArticleServiceImpl.java +│   │   │   └── UserServiceImpl.java +│   │   └── resources +│   │   └── jboot.properties +│   └── test +│   └── java +``` + +通过 Jboot 代码生成器的运行,项目对应的 model 类和 service 会自动生成,同时 Service 层的代码以及带有了对数据库增、删、改、查的基本能力. + +需要注意的是: +**再次运行该代码生成器的时候,BaseUser、BaseArticle会被重新覆盖,其他代码不会被覆盖。** 若需要重新生成 service 层 和 User、Article 等代码,需要手动删除后,再次运行代码生成器 CodeGenerator 。 + +## 自动注入 +Jboot 通过 Google Guice 提供了强健稳定的代码注入功能,使用注入功能只需要了解以下三个注解: +1. @Bean : 声明此类可以被自动注入 +2. @Inject : 对属性进行赋值注入 + +通过代码生成器生成的Service层代码就已经默认添加上了 @Bean 和 @Singleton 这两个配置,生成的代码如下: + +```java +package com.xxx.service.impl; + +import io.jboot.aop.annotation.Bean; +import com.xxx.service.UserService; +import com.xxx.model.User; +import io.jboot.service.JbootServiceBase; + +import javax.inject.Singleton; + +@Bean +public class UserServiceImpl extends JbootServiceBase implements UserService { + +} +``` + +我们使用到 UserService 接口的时候,只需要添加 @Inject 注解即可,例如:在 IndexController 需要用到 UserService,代码如下: + +```java +@RequestMapping("/") +public class IndexController extends JbootController { + + @Inject + private UserService userService; + + public void index() { + renderText("Hello World Jboot"); + } + + public void dbtest() { + List records = Db.find("select * from user"); + renderText(Arrays.toString(records.toArray())); + } + + + public void users() { + // 这里用到了 userService 的查询方法 + List users = userService.findAll(); + renderText(Arrays.toString(users.toArray())); + } + + + public static void main(String[] args) { + Jboot.run(args); + } +} +``` + +运行 `main()` 方法后,我们通过浏览访问 `http://127.0.0.1:8080/users` ,此时,页面显示的内容和 访问 `http://127.0.0.1:8080/dbtest` 的效果是一样的: + +![](./static/images/0009.png "") + + + +## 数据库的增删改查 +在本章节,我们要完成一个小型的项目,这个项目是一个用户管理系统,他具有以下功能: + +* 显示用户列表,带有分页的功能 +* 可以对单个用户删除 +* 可以对用户进行修改 +* 可以添加新的用户 + +### 分页查询 + +我们可以继续来改造 IndexController,通过修改代码生成器的生成的 UserService,来达到上述要求的功能。 + +在上述的章节里,我们知道,通过如下的代码可以获得所有的用户信息: + +```java + public void users() { + List users = userService.findAll(); + renderText(Arrays.toString(users.toArray())); +} +``` + +如果要分页,我们需要在UserService添加一个分页的方法,并在 UserServiceImpl 来实现这个分页的方法,代码如下: + +UserService.java + +```java +public interface UserService { + + // 代码生成器生成的其他方法略 + //... + + public Page paginate(int page, int size); +} +``` + +UserServiceImpl.java + +```java +@Bean +public class UserServiceImpl extends JbootServiceBase implements UserService { + + public Page paginate(int page, int size) { + return DAO.paginate(page, size); + } +} +``` + +为了代码更加简洁直观,我们新建一个 UserController 来做用户相关的增删改查功能,代码如下: + +```java +@RequestMapping("/user") +public class UserController extends JbootController { + + @Inject + private UserService userService; + + public void index() { + int page = getParaToInt("page", 1); + Page userPage = userService.paginate(page, 10); + setAttr("pageData", userPage); + render("/user.html"); + } + +} +``` + +* `getParaToInt()` 可以获得request提交过来的page数据,例如:http://127.0.0.1:8080/user?page=100 ,此时,代码里 page 的值是 100,当不传值得时候,默认值是 1 。 +* 通过 userService.paginate 查询数据库,返回一个 Page 对象,里面包含了 当前页码、总页码 和 数据列表等信息。 +* 通过 `setAttr()` 把数据结果传递到页面 + +把数据传递到 user.html 后,需要 user.html 把具体的数据和分页相关在网页上列出来。 + +第一步:完善数据的显示,user.html内容如下: + +```html + + + + + user index + + + + + + + + + #for(user : pageData.list ) + + + + + + #end +
ID登录名密码
#(user.id)#(user.login_name)#(user.password)
+ + +``` +此时,运行 `main()` 方法,访问 `http://127.0.0.1:8080/user` ,页面显示内容如下: + +![](./static/images/0011.png "") + +第二步:完善分页功能。 + +Jboot应用的分页功能需要自定义一个分页标签,自定义分页标签非常简单,代码内容如下: + +```java +@JFinalDirective("myPaginate") +public class MyPaginateDirective extends JbootPaginateDirective { + +} +``` +然后再修改 user.html 内容如下: + +```html + + + + + user index + + + + + + + + + #for(user : pageData.list ) + + + + + + #end +
ID登录名密码
#(user.id)#(user.login_name)#(user.password)
+#myPaginate() + #for(page : pages) + #(page.text??) + #end +#end + + +``` + +此时,运行 `main()` 方法,访问 `http://127.0.0.1:8080/user` ,页面显示内容如下: + +![](./static/images/0012.png "") + +由于数据量太小,同时在我们的代码里,要求每页显示10条数据,所以页面才显示了第一页,当我们在数据库添加数据量超过10条的时候,页面显示内容如下: + +![](./static/images/0013.png "") + +同时,上一页、下一页等功能正常使用,如下图: + +![](./static/images/0014.png "") + +实际上,#myPaginate() 自定义分页标签还可以做更多的配置,包括功能和样式等,但是这不是本章节要讨论的内容了。 + +### 新增功能 +为了实现新增功能,我们需要写一个叫 add.html 的页面,并写对应的 Controller,保证可以访问。 + +add.html 的代码如下: + +```html + + + + + user add + + +
+ 登录名:
+ 密码:
+ +
+ + +``` + +通过 add.html 内容我们能看到,当用户点击 `提交数据` 按钮的时候,页面会把数据提交到 `/user/doSave` 这个路径上去,所以,需要我们在 UserController 编写一个叫做 `doSave()` 的方法来接收数据,并保存到数据库。 + +doSave() 方法内容如下: + +```java +public void doSave() { + String loginName = getPara("login_name"); + String password = getPara("password"); + + User user = new User(); + user.setLoginName(loginName); + user.setPassword(password); + + user.save(); + + redirect("/user"); +} +``` + +`doSave()` 方法的主要作用是接收数据、把数据保存到数据库、然后跳转到 `/user` 这个页面去。 + +### 修改功能 + +为了减少代码量,我们直接把 add.html 改造成为可用做新增,也可以用作修改的功能(通常在商业项目中也会这么做),因此,我们需要简单修改下 add.html 代码和 add() 这个方法的代码。 + +add.html + +```html + + + + + user add + + +
+ + 登录名:
+ 密码:
+ +
+ + +``` + +和新增功能的html对比,增加了 `` 这行代码。 + +add() 方法内容如下: + +```java +public void add() { + int id = getParaToInt("id", 0); + if (id > 0) { //有id ,说明有数据提交过来,用来做修改的标识。 + setAttr("id", id); + } + render("/add.html"); +} +``` + +同时, doSave()方法也需要修改下,用来区分是新增还是修改,代码如下: + +```java +public void doSave() { + String loginName = getPara("login_name"); + String password = getPara("password"); + + long id = getParaToLong("id",0l); + + + User user = new User(); + user.setLoginName(loginName); + user.setPassword(password); + + if (id > 0){ //说明是更新 + user.setId(id); + user.update(); + }else { //说明是新增 + user.save(); + } + + redirect("/user"); +} +``` + +最后,我们在改造下 user.html ,在表格的后面添加一个 `修改` 的连接, user.html 代码如下: + +```html + + + + + user index + + + + + + + + + + #for(user : pageData.list ) + + + + + + + #end +
ID登录名密码操作
#(user.id)#(user.login_name)#(user.password)修改
+#myPaginate() + #for(page : pages) + #(page.text??) + #end +#end + + +``` + +此时,页面内容如下,修改功能正常使用。 + +![](./static/images/0015.png "") + +### 删除功能 + +删除功能更加简单,只需要在Controller接收ID,然后调用 userService.delete() 方法就可以了,改造 user.html 代码如下: + +```html + + + + + user index + + + + + + + + + + #for(user : pageData.list ) + + + + + + + #end +
ID登录名密码操作
#(user.id)#(user.login_name)#(user.password) + 修改 + 删除 +
+#myPaginate() + #for(page : pages) + #(page.text??) + #end +#end + + +``` + +页面显示如下: + +![](./static/images/0016.png "") + +我们只需要在 UserController 编写一个 del() 方法,接收id、删除数据库数据,并跳转回 /user 即可完成任务,代码如下: + +```java + public void del() { + long id = getParaToLong("id",0l); + userService.deleteById(id); + redirect("/user"); +} +``` + +到目前为止,增删改查所有功能完成。 + + +## 使用缓存提高性能 +通过以上内容,我们可以使用Jboot开发一个具有增、删、改、查基本功能的Java Web 应用,但是,在互联网的应用里,高并发的要求可以说是必不可少的,缓存在提高应用性能和并发上有绝对的话语权。 + +在 Jboot 里,我们如何来使用缓存呢? + +Jboot 提供了两种方案: + +1. 注解 +1. 手写代码 + +在注解中,Jboot提供了4个注解,方便的对缓存进行操作,他们分别是: + +* @Cacheable +* @CachePut +* @CacheEvict +* @CachesEvict + +如何来使用呢? + +在以上的章节里,我们知道,如下的代码是一个分页查询的功能: + +```java +public class UserServiceImpl extends JbootServiceBase implements UserService { + + public Page paginate(int page, int size) { + return DAO.paginate(page, size); + } +} +``` + +如何来让 `paginate(int page, int size)` 方法具有缓存的功能呢? + +非常简单: + +```java +public class UserServiceImpl extends JbootServiceBase implements UserService { + + @Cacheable(name = "myCache",key = "page:#(page)-#(size)") + public Page paginate(int page, int size) { + return DAO.paginate(page, size); + } +} +``` +只需要添加 `@Cacheable(name = "myCache",key = "page:#(page)-#(size)")` 这个注解。 + +在Jboot中,默认的缓存为 `EhCache` , 这个注解的含义是: + +* 在EhCache中创建一个缓存为myCache的缓存区 +* 当查询第 1 页的时候,缓存的key为:`page:1-10`,因为 `paginate(int page, int size)` 方法在执行的时候,传递过来的值分别是:page=1,size=10 +* 当查询第 2 页的时候,缓存的key为:`page:2-10`,原因同上。 + +当 `paginate(int page, int size)` 方法使用 @Cacheable 缓存之后,只有第一次访问的时候去查询数据库,之后的访问会直接从缓存中获取数据,大大提高了性能。 + +但是... + +使用缓存也会带来一些问题,因为 `paginate(int page, int size)` 方法不再访问数据库,从而导致我们在数据库对数据进行增、删、改,这个页面数据都不会再发生变化。 + +要让 `paginate(int page, int size)` 方法与数据库同步,怎么办呢? + +需要我们在对数据库进行 增、删、改 的时候,清除这个方法里的缓存数据。 + +代码如下: + +```java +public class UserServiceImpl extends JbootServiceBase implements UserService { + + @Cacheable(name = "myCache",key = "page:#(page)-#(size)") + public Page paginate(int page, int size) { + return DAO.paginate(page, size); + } + + @Override + @CacheEvict(name = "myCache",key = "*") + public boolean save(User model) { + return super.save(model); + } + + @Override + @CacheEvict(name = "myCache",key = "*") + public boolean update(User model) { + return super.update(model); + } + + @Override + @CacheEvict(name = "myCache",key = "*") + public boolean delete(User model) { + return super.delete(model); + } +} +``` +重写父类 `JbootServiceBase` 的增、删、改的方法,在这些方法添加 `@CacheEvict(name = "myCache",key = "*")` 注解。 + +**被添加@CacheEvict的方法,在执行之后,会清除 name 为 myCache 的所有key。** 也就是清除 `paginate(int page, int size)` 方法所有的 key 。 + +这样,就做到了 `paginate(int page, int size)` 方法与数据库同步的功能了。是不是非常简单呢 ?! + +不过 ... + +以上,只是 Jboot 缓存功能的冰山一角,Jboot 的缓存非常强大,比如: + + * @CachePut 和 @CachesEvict 又如何使用?什么场景下使用? + * 如何使用 Redis 或者其他的缓存方案代替默认的 EhCache ,甚至是公司自己内部的缓存方案 ? + * 分布式缓存如何做 ? + * 如何设置缓存的失效时间 ? + * 如何做到整个网页缓存,类似页面静态化? + * 等等等等 + +Jboot 都给与类非常完美的支持。 + +## 探索 Jboot 的更多功能... + +恭喜你,到目前为止,你已经掌握了使用 Jboot 来开发一个 java web 的基本技能,包含了 + +* MVC +* ORM +* AOP +* 代码生成器 +* 使用缓存提高性能 +* 等等 + +这是非常重要的一步。 + +但是,Jboot 的功能远远不止这些,以上只是 Jboot 的冰山一角。 + +Jboot 真正的核心是做微服务的开发,微服务的底层代码是分布式调用 RPC,RPC 的框架和实现的方案非常繁杂,不过 Jboot 已经支持了主流 RPC 的实现,其中包含了 Dubbo 、motan、Zbus 等, 未来还会添加 gRPC 等更多的支持。 + +* 在 RPC 下,Jboot 支持了 RPC 下的 熔断、降级、监控、Opentracing 等等功能 +* 在分布式下,Jboot 支持了分布式缓存、分布式 Session、分布式锁、分布式任务、统一配置中心 +* 在数据库下,Jboot 支持分库分表、支持 Reids 等 nosql 数据库的极简调用 +* 在 MQ 下,Jboot 支持 rabbitmq、redismq、rocketmq 甚至还支持了 阿里云的商业MQ +* 另外,Jboot 还支持了 Swagger、Event 事件机制、高并发下的 Sentinel 限流方案等等更多的惊喜 + + +希望你用的顺手、开心,如果有什么问题,可以通过 顶部菜单的 提问 链接进行提问,我回在第一时间回复您。 diff --git a/doc/docs/static/images/0008.png b/doc/docs/static/images/0008.png new file mode 100644 index 0000000000000000000000000000000000000000..da156921beb4cc2082eb7449343e59d13b02fb91 Binary files /dev/null and b/doc/docs/static/images/0008.png differ diff --git a/doc/docs/static/images/0009.png b/doc/docs/static/images/0009.png new file mode 100644 index 0000000000000000000000000000000000000000..62b152099aa347dc1bd474855e0a419fd00a320f Binary files /dev/null and b/doc/docs/static/images/0009.png differ diff --git a/doc/docs/static/images/0010.png b/doc/docs/static/images/0010.png new file mode 100644 index 0000000000000000000000000000000000000000..fd48654ada1c755a25f49ff154534bac9c6a0dfd Binary files /dev/null and b/doc/docs/static/images/0010.png differ diff --git a/doc/docs/static/images/0011.png b/doc/docs/static/images/0011.png new file mode 100644 index 0000000000000000000000000000000000000000..248297e3dbe3bcdc4726ce06c8638d4b6b250ec6 Binary files /dev/null and b/doc/docs/static/images/0011.png differ diff --git a/doc/docs/static/images/0012.png b/doc/docs/static/images/0012.png new file mode 100644 index 0000000000000000000000000000000000000000..c2a983f68a9dc6eeb4426402db530a72969a99bf Binary files /dev/null and b/doc/docs/static/images/0012.png differ diff --git a/doc/docs/static/images/0013.png b/doc/docs/static/images/0013.png new file mode 100644 index 0000000000000000000000000000000000000000..05371b914eee623fa7024d4abe18ec2e88d54165 Binary files /dev/null and b/doc/docs/static/images/0013.png differ diff --git a/doc/docs/static/images/0014.png b/doc/docs/static/images/0014.png new file mode 100644 index 0000000000000000000000000000000000000000..5a3742f8de1e503c000c315910c461a11a570e6f Binary files /dev/null and b/doc/docs/static/images/0014.png differ diff --git a/doc/docs/static/images/0015.png b/doc/docs/static/images/0015.png new file mode 100644 index 0000000000000000000000000000000000000000..3d8c0b3be9c57224339edfb1d339ee79e2ecdfe4 Binary files /dev/null and b/doc/docs/static/images/0015.png differ diff --git a/doc/docs/static/images/0016.png b/doc/docs/static/images/0016.png new file mode 100644 index 0000000000000000000000000000000000000000..7b1a43edba866ffe3802238631ebebd2aaadd1cc Binary files /dev/null and b/doc/docs/static/images/0016.png differ diff --git a/doc/docs/static/images/1_eclipse.png b/doc/docs/static/images/1_eclipse.png new file mode 100755 index 0000000000000000000000000000000000000000..10b75a2810fdd99ed9b75cfc082ff10d599da530 Binary files /dev/null and b/doc/docs/static/images/1_eclipse.png differ diff --git a/doc/docs/static/images/1_ieda.png b/doc/docs/static/images/1_ieda.png new file mode 100755 index 0000000000000000000000000000000000000000..1e87b58a892477335f901308fda853df23ee6b9b Binary files /dev/null and b/doc/docs/static/images/1_ieda.png differ diff --git a/doc/docs/static/images/ahas.png b/doc/docs/static/images/ahas.png new file mode 100644 index 0000000000000000000000000000000000000000..24bb1308a563278ef3be9f1fe14f53264d3444d7 Binary files /dev/null and b/doc/docs/static/images/ahas.png differ diff --git a/doc/docs/static/images/apidoc.jpg b/doc/docs/static/images/apidoc.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b17c102e84b482bbd8009f523dfbaea9d6da541f Binary files /dev/null and b/doc/docs/static/images/apidoc.jpg differ diff --git a/doc/docs/static/images/apidoc1.jpg b/doc/docs/static/images/apidoc1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..db3350433a1c3003064c0bcc5a29ea304accf6b0 Binary files /dev/null and b/doc/docs/static/images/apidoc1.jpg differ diff --git a/doc/docs/imgs/grafana.png b/doc/docs/static/images/grafana.png similarity index 100% rename from doc/docs/imgs/grafana.png rename to doc/docs/static/images/grafana.png diff --git a/doc/docs/static/images/grafana_datasource_new.png b/doc/docs/static/images/grafana_datasource_new.png new file mode 100644 index 0000000000000000000000000000000000000000..124fbef87705b6bad41aa3f012b14a73905a6285 Binary files /dev/null and b/doc/docs/static/images/grafana_datasource_new.png differ diff --git a/doc/docs/static/images/grafana_import.png b/doc/docs/static/images/grafana_import.png new file mode 100644 index 0000000000000000000000000000000000000000..b716364f69e627dba92acb9ca2f365a05b40375d Binary files /dev/null and b/doc/docs/static/images/grafana_import.png differ diff --git a/doc/docs/static/images/grafana_import_json.png b/doc/docs/static/images/grafana_import_json.png new file mode 100644 index 0000000000000000000000000000000000000000..91391fb0acc81ce946d23dec28efebc3ace9f838 Binary files /dev/null and b/doc/docs/static/images/grafana_import_json.png differ diff --git a/doc/docs/static/images/grafana_jboot_jvm.png b/doc/docs/static/images/grafana_jboot_jvm.png new file mode 100644 index 0000000000000000000000000000000000000000..dadc1e24763317f7c5bbe8c9904221f802e2c800 Binary files /dev/null and b/doc/docs/static/images/grafana_jboot_jvm.png differ diff --git a/doc/docs/imgs/idea-auto-build.jpg b/doc/docs/static/images/idea-auto-build.jpg similarity index 100% rename from doc/docs/imgs/idea-auto-build.jpg rename to doc/docs/static/images/idea-auto-build.jpg diff --git a/doc/docs/static/images/idea_001.jpg b/doc/docs/static/images/idea_001.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2516433b99504cdd8779bdb2fca4cb4a79b6d09c Binary files /dev/null and b/doc/docs/static/images/idea_001.jpg differ diff --git a/doc/docs/static/images/idea_002.png b/doc/docs/static/images/idea_002.png new file mode 100644 index 0000000000000000000000000000000000000000..9c4ec19d6280e71cb4dd6fe90a3faf862351de03 Binary files /dev/null and b/doc/docs/static/images/idea_002.png differ diff --git a/doc/docs/static/images/idea_003.png b/doc/docs/static/images/idea_003.png new file mode 100644 index 0000000000000000000000000000000000000000..14809cd5b0274538aca622779eba1b9835225008 Binary files /dev/null and b/doc/docs/static/images/idea_003.png differ diff --git a/doc/docs/static/images/idea_004.png b/doc/docs/static/images/idea_004.png new file mode 100644 index 0000000000000000000000000000000000000000..1bb8fa47ab869d2f359554e0c95b51a790be7e74 Binary files /dev/null and b/doc/docs/static/images/idea_004.png differ diff --git a/doc/docs/static/images/idea_005.png b/doc/docs/static/images/idea_005.png new file mode 100644 index 0000000000000000000000000000000000000000..ad9ae1ee13e5535e1bf75905a3096fe7f4b108d3 Binary files /dev/null and b/doc/docs/static/images/idea_005.png differ diff --git a/doc/docs/static/images/idea_006.png b/doc/docs/static/images/idea_006.png new file mode 100644 index 0000000000000000000000000000000000000000..fe50b6c16bb42aef8be483fbb6db3db19221f067 Binary files /dev/null and b/doc/docs/static/images/idea_006.png differ diff --git a/doc/docs/static/images/idea_007.png b/doc/docs/static/images/idea_007.png new file mode 100644 index 0000000000000000000000000000000000000000..a8756ad37b9a6067ed6963544d555abc2a2b3224 Binary files /dev/null and b/doc/docs/static/images/idea_007.png differ diff --git a/doc/docs/static/images/jboot-logo.png b/doc/docs/static/images/jboot-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..67983802cd3a645e7425ef7528f420e1908c0ace Binary files /dev/null and b/doc/docs/static/images/jboot-logo.png differ diff --git a/doc/docs/static/images/jboot-wechat-group.png b/doc/docs/static/images/jboot-wechat-group.png new file mode 100644 index 0000000000000000000000000000000000000000..3f607d8a4a2c7a3697dd82cd176e53e15e7c9ce2 Binary files /dev/null and b/doc/docs/static/images/jboot-wechat-group.png differ diff --git a/doc/docs/static/images/prometheus_targets.png b/doc/docs/static/images/prometheus_targets.png new file mode 100644 index 0000000000000000000000000000000000000000..4d673227ba0f4cd79105cf65fd188b7dab948980 Binary files /dev/null and b/doc/docs/static/images/prometheus_targets.png differ diff --git a/doc/docs/undertow.md b/doc/docs/undertow.md index f5e029e96dcb4be262cbce1018e53acf4b84c375..74eef406bebd7f5f1bfce7a608fdd67e9df016ad 100644 --- a/doc/docs/undertow.md +++ b/doc/docs/undertow.md @@ -11,7 +11,7 @@ ## 其他扩展 ### 扩展1:配置位置 -在 Jboot 应用中,除了可以在 resource 目录下的 `undertow.txt` 文件进行配置以外,也可以在 `jboot.properties` 文件里配置。 +在 Jboot 应用中,除了可以在 resource 目录下的 `undertow.txt` 文件进行配置以外,也可以在 `jboot.properties` 文件里配置。 同时可以通过启动参数 和 环境变量等进行配置,Undertow 启动的时候读取配置内容的优先顺序是: diff --git a/doc/docs/upgrade.md b/doc/docs/upgrade.md index 193d7dc3b63ff3e16295f0c915decea493187490..9b8b8e1177096a0589c819b0124d068e0374034c 100644 --- a/doc/docs/upgrade.md +++ b/doc/docs/upgrade.md @@ -21,6 +21,7 @@ JbootCache cache = Jboot.me().getCache(); ``` 修改为: + ```java JbootCache cache = Jboot.getCache(); ``` diff --git a/doc/docs/validator.md b/doc/docs/validator.md new file mode 100644 index 0000000000000000000000000000000000000000..2c2c13379774aab0410d01b1de38d4785880e63f --- /dev/null +++ b/doc/docs/validator.md @@ -0,0 +1,184 @@ +# 数据验证 Validator + +Jboot 从 V3.7.5 开始,增强 Jboot 的验证方式,在 Jboot 之前的 @EmptyValidate、@RegexValidate 等基础上,进一步基于 JSR 303 – Bean Validation 简化了验证方式,相比 Spring 更加优雅简单。 + +[[toc]] + +## @NotNull + +在 Controller (或 Service 等)中,我们可以直接通过 @NotNull 注解给 Controller 添加,例如: + +```java +@RequestMapping("/") +public class MyController extends JbootController { + + public void test(@NotNull String para) { + renderText("test6"); + } +} +``` + +当我们访问 `/test` 的时候,会出现如下的错误: + +``` +para is null at method: io.jboot.test.MyController.test(java.lang.String) : /test +io.jboot.components.valid.ValidException : 不能为null + at io.jboot.components.valid.ValidUtil.throwValidException(ValidUtil.java:59) + at io.jboot.components.valid.ValidUtil.throwValidException(ValidUtil.java:50) + at io.jboot.components.valid.interceptor.NotNullInterceptor.intercept(NotNullInterceptor.java:36) +``` + +如果是 ajax (或者 content-type 为 "application/json") 访问 `/test` 的时候,会返回如下的 json 信息: + +```json +{ + "throwable": "io.jboot.components.valid.ValidException: 不能为null", + "errorMessage": "para is null at method: io.jboot.test.MyController.test(java.lang.String)", + "errorCode": 400, + "state": "fail", + "message": "不能为null" +} +``` + +如果我们访问 `/test?para=123`,则可以正常访问,不会出错,此时 `test` 方法里的 para 的值为 `123`(不为 null)。 + +## @Size 验证 + +@Size 验证,不仅仅可以验证 String 数据的长度,也可以验证 int long 等数据类型的值的大小范围。比如: + +```java +public void test(@Size(min=2,max=10) int value) { + renderText("test6"); +} +``` + +这个要求 value 的值必须在 2 ~ 10 之间。 + + +当然,我们还可以使用 @Size 来验证 `Map`、`List`、`数组` 的长度,比如配合 @JsonBody 来接收前端传入的值: + +```java +public void list(@Size(min=2,max=10) @JsonBody() List list) { + System.out.println("list--->" + list); + renderText("ok"); +} +``` + +要求前度传入的 MyBean Json 数组的数量必须是在 2~10 之间。 + +> 更多关于 @JsonBody 请参考:[这个链接](./json.md)。 + +## @NotEmpty 验证 + +@NotEmpty 不仅仅可以验证 String 类型不能为 null 和 空字符串,也可以验证 Map、List、数组等不能为空,比如: + +```java +public void list(@NotEmpty() @JsonBody() List list) { + System.out.println("list--->" + list); + renderText("ok"); +} +``` + +要求前端掺入的 MyBean Json 数组必须有值。 + +## @Valid 验证 +@Valid 是针对整个 Java Bean 验证,也可以对 JFinal 的 Model 进行验证。的 MyBean Json 数 + +比如 MyBean 定义如下: + +```java +public class MyBean { + private String id; + + @NotBlank(message = "密码不能为空") + private String password; + + @Size(min=0,max=2,message = "性别的值只能是 0 1 2") + private int sex; + + @Min(value = 18,message = "未成年禁止入内") + private Integer age; +} +``` + +在 Controller 或者 Service 中,如下代码可以直接对 MyBean 进行验证: + +```java +public void test(@Valid() MyBean bean) { + renderText("test6"); +} +``` + +如果 MyBean 是一个 JFinal 的 Model,我们只需要在 getter 方法添加注解即可。 + +## 更多的验证 +除了以上的基本示例以外,Jboot 的验证还支持了更多的验证: + +| 注解 | 说明 | +| ---- | ---- | +| @NotNull | 限制必须不为null | +| @DecimalMax(value) | 限制必须为一个不大于指定值的数字 | +| @DecimalMin(value) | 限制必须为一个不小于指定值的数字 | +| @Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction | +| @Max(value) | 限制必须为一个不大于指定值的数字 | +| @Min(value) | 限制必须为一个不小于指定值的数字 | +| @Pattern(value) | 限制必须符合指定的正则表达式 | +| @Size(max,min) | 限制字符长度必须在min到max之间 | +| @NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) | +| @NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 | +| @Email | 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 | + +## @EmptyValidate 和 @RegexValidate + +当如果我们的 `Controller` 的方法没有参数的时候,就无法使用如上的注解进行验证了。因为,以上的注解 +是给方法里的参数进行添加。在这个时候,我们可以使用 `@EmptyValidate` 和 `@RegexValidate` 进行验证。 + +比如: + +```java +@RequestMapping("/") +public class MyController extends JbootController { + + @EmptyValidate(@Form(name = "mobile", message = "手机号不能为空")) + public void action1() { + renderText("ok"); + } + + + @RegexValidate(@RegexForm(name = "mobile", regex = Regex.MOBILE, message= "您输入的不是手机号")) + public void action2() { + renderText("ok"); + } + +} +``` + +- 当我们访问 `/action1` 的时候,会出现手机号不能为空的错误提示。访问 `/action1?mobile=123` 的时候,正常访问。 +- 当我们访问 `/action2` 或者 `/action2?mobile=123` 的时候,会出现手机号不正确的错误提示。 + 访问 `/action2?mobile=18611223344` 的时候,正常访问。因为 `18611223344` 是一个正确的手机号 。 + +## 自定义渲染器 + + 我们可以写一个 `RenderFactory` 继承自 `JbootRenderFactory` ,并复写其 `getValidErrorRender()` 方法。 + + 例如: + +```java +public MyRenderFactory extends JbootRenderFactory(){ + + @Override + public ValidErrorRender getValidErrorRender(ValidException validException){ + //return 返回自己写渲染器 + } +} +``` + +然后在项目启动的时候,通过在 `onConstantConfig(Constants constants)` 里配置上自己的 `MyRenderFactory`: + +```java +@Override +public void onConstantConfig(Constants constants) { + constants.setRenderFactory(new MyRenderFactory()); +} + +``` \ No newline at end of file diff --git a/doc/jboot_jvm_grafana.json b/doc/jboot_jvm_grafana.json new file mode 100644 index 0000000000000000000000000000000000000000..f62c6e673276b85ad5b03d296c1a4db83e93ea66 --- /dev/null +++ b/doc/jboot_jvm_grafana.json @@ -0,0 +1,3628 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "limit": 100, + "name": "Annotations & Alerts", + "showIn": 0, + "type": "dashboard" + }, + { + "datasource": "Prometheus", + "enable": true, + "expr": "resets(process_uptime_seconds{application=\"$application\", instance=\"$instance\"}[1m]) > 0", + "iconColor": "rgba(255, 96, 96, 1)", + "name": "Restart Detection", + "showIn": 0, + "step": "1m", + "tagKeys": "restart-tag", + "textFormat": "uptime reset", + "titleFormat": "Restart" + } + ] + }, + "description": "Jboot 应用的 JVM 大盘", + "editable": true, + "gnetId": 4701, + "graphTooltip": 1, + "id": 4, + "iteration": 1610259558892, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 125, + "panels": [], + "repeat": null, + "title": "Quick Facts", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "decimals": 1, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "s", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 1 + }, + "height": "", + "id": 63, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "jvm_uptime{application=\"$application\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "", + "title": "Uptime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "decimals": null, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "dateTimeAsIso", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 1 + }, + "height": "", + "id": 92, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "jvm_start_time{application=\"$application\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "", + "title": "Start time", + "type": "singlestat", + "valueFontSize": "70%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 65, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(jvm_memory_heap_used{application=\"$application\"})*100/sum(jvm_memory_heap_max{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "70,90", + "title": "Heap used", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 75, + "interval": null, + "links": [], + "mappingType": 2, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + }, + { + "from": "-99999999999999999999999999999999", + "text": "N/A", + "to": "0" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(jvm_memory_non_heap_used{application=\"$application\"})*100/sum(jvm_memory_non_heap_max{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "70,90", + "title": "Non-Heap used", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + }, + { + "op": "=", + "text": "x", + "value": "" + } + ], + "valueName": "current" + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 4 + }, + "id": 126, + "panels": [], + "repeat": null, + "title": "I/O Overview", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 5 + }, + "hiddenSeries": false, + "id": 111, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(jboot_http_requests_count{application=\"$application\"}[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "HTTP", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "HTTP": "#890f02", + "HTTP - 5xx": "#bf1b00" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 5 + }, + "hiddenSeries": false, + "id": 112, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(jboot_http_responseCodes_serverError_total{application=\"$application\"}[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "HTTP - 5xx", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Errors", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "HTTP": "#890f02", + "HTTP - 5xx": "#bf1b00" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 5 + }, + "hiddenSeries": false, + "id": 135, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(jboot_http_responseCodes_notFound_total{application=\"$application\"}[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "HTTP - 5xx", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "404 Not Fund", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 5 + }, + "hiddenSeries": false, + "id": 113, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(jboot_http_requests_sum{application=\"$application\"}[1m]))/sum(rate(jboot_http_requests_count{application=\"$application\"}[1m]))", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "HTTP - AVG", + "refId": "A" + }, + { + "expr": "max(jboot_http_requests_max{application=\"$application\"})", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "HTTP - MAX", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Duration", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ns", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 127, + "panels": [], + "repeat": null, + "title": "JVM Memory", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 13 + }, + "hiddenSeries": false, + "id": 24, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_heap_used{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_heap_committed{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_heap_max{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "mbytes", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 13 + }, + "hiddenSeries": false, + "id": 25, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_non_heap_used{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_non_heap_committed{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_non_heap_max{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Non-Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "mbytes", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 13 + }, + "hiddenSeries": false, + "id": 26, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_total_used{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_total_committed{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_total_max{application=\"$application\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Total", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "mbytes", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 13 + }, + "hiddenSeries": false, + "id": 86, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "process_memory_vss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": true, + "interval": "", + "intervalFactor": 2, + "legendFormat": "vss", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": " {application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "rss", + "refId": "B" + }, + { + "expr": "process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "swap", + "refId": "C" + }, + { + "expr": "process_memory_rss_bytes{application=\"$application\", instance=\"$instance\"} + process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "total", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Process Memory", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "mbytes", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 128, + "panels": [], + "repeat": null, + "title": "JVM Misc", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 21 + }, + "hiddenSeries": false, + "id": 106, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_cpu_usage{application=\"$application\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "system", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "process_cpu_usage{application=\"$application\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "process", + "refId": "B" + }, + { + "expr": "avg_over_time(process_cpu_usage{application=\"$application\"}[1h])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "process-1h", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "CPU Usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "decimals": 1, + "format": "percentunit", + "label": "", + "logBase": 1, + "max": "1", + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 21 + }, + "hiddenSeries": false, + "id": 93, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_load_average_1m{application=\"$application\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "system-1m", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "system_cpu_count{application=\"$application\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "cpus", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Load", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "decimals": 1, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 21 + }, + "hiddenSeries": false, + "id": 32, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_threads_waiting_count{application=\"$application\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "waiting", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_threads_daemon_count{application=\"$application\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "daemon", + "metric": "", + "refId": "B", + "step": 2400 + }, + { + "expr": "jvm_threads_blocked_count{application=\"$applicstance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "blocked", + "refId": "C", + "step": 2400 + }, + { + "expr": "jvm_threads_total_started_count{application=\"$applicaion\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "total", + "refId": "D", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Threads", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "blocked": "#bf1b00", + "new": "#fce2de", + "runnable": "#7eb26d", + "terminated": "#511749", + "timed-waiting": "#c15c17", + "waiting": "#eab839" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 21 + }, + "hiddenSeries": false, + "id": 124, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_threads_count{application=\"$application\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{state}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Thread States", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 28 + }, + "hiddenSeries": false, + "id": 61, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "process_files_open{application=\"$application\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "open", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "process_files_max{application=\"$application\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "File Descriptors", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 10, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 35 + }, + "id": 129, + "panels": [], + "repeat": "persistence_counts", + "title": "JVM Memory Pools", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 9, + "x": 0, + "y": 36 + }, + "hiddenSeries": false, + "id": 3, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": "jvm_memory_pool_heap", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_heap_used{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_heap_committed{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_heap_max{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "mbytes", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 9, + "x": 9, + "y": 36 + }, + "hiddenSeries": false, + "id": 78, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": "jvm_memory_pool_nonheap", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_non_heap_used{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_non_heap_committed{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_non_heap_max{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Non Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "mbytes", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 131, + "panels": [], + "repeat": null, + "title": "Garbage Collection", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 44 + }, + "hiddenSeries": false, + "id": 98, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_pause_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{action}} ({{cause}})", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Collections", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 44 + }, + "hiddenSeries": false, + "id": 101, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_pause_sum{application=\"$application\", instance=\"$instance\"}[1m])/rate(jvm_gc_pause_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "avg {{action}} ({{cause}})", + "refId": "A" + }, + { + "expr": "jvm_gc_pause_max{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "max {{action}} ({{cause}})", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Pause Durations", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 44 + }, + "hiddenSeries": false, + "id": 99, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_memory_allocated{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "allocated", + "refId": "A" + }, + { + "expr": "rate(jvm_gc_memory_promoted{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "promoted", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Allocated/Promoted", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 132, + "panels": [], + "repeat": null, + "title": "Classloading", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 52 + }, + "hiddenSeries": false, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_classes_loaded{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "loaded", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Classes loaded", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 52 + }, + "hiddenSeries": false, + "id": 38, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "delta(jvm_classes_loaded{application=\"$application\",instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "delta-1m", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Class delta", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ops", + "short" + ], + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 59 + }, + "id": 133, + "panels": [], + "repeat": null, + "title": "Buffer Pools", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 60 + }, + "hiddenSeries": false, + "id": 33, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffers_direct_used{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_buffers_direct_capacity{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "capacity", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Direct Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 60 + }, + "hiddenSeries": false, + "id": 83, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffers_direct_count{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "count", + "metric": "", + "refId": "A", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Direct Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 60 + }, + "hiddenSeries": false, + "id": 85, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffers_mapped_used{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_buffers_mapped_capacity{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "capacity", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mapped Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 60 + }, + "hiddenSeries": false, + "id": 84, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.1", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffers_mapped_count{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "count", + "metric": "", + "refId": "A", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mapped Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "30s", + "schemaVersion": 26, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": false, + "text": "jboot", + "value": "jboot" + }, + "datasource": "Prometheus", + "definition": "", + "error": null, + "hide": 0, + "includeAll": false, + "label": "应用", + "multi": false, + "name": "application", + "options": [], + "query": "label_values(application)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "selected": false, + "text": "localhost:1234", + "value": "localhost:1234" + }, + "datasource": "Prometheus", + "definition": "label_values(instance)", + "error": null, + "hide": 0, + "includeAll": false, + "label": "实例", + "multi": false, + "multiFormat": "glob", + "name": "instance", + "options": [], + "query": "label_values(instance)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": "Prometheus", + "definition": "", + "error": null, + "hide": 0, + "includeAll": true, + "label": "JVM Memory Pools Heap", + "multi": false, + "multiFormat": "glob", + "name": "jvm_memory_pool_heap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"},id)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": "Prometheus", + "definition": "", + "error": null, + "hide": 0, + "includeAll": true, + "label": "JVM Memory Pools Non-Heap", + "multi": false, + "multiFormat": "glob", + "name": "jvm_memory_pool_nonheap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"},id)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 2, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Jboot JVM", + "uid": "F7CDMa-Mz", + "version": 1 +} \ No newline at end of file diff --git a/doc/jbootadmin/attachment.md b/doc/jbootadmin/attachment.md new file mode 100644 index 0000000000000000000000000000000000000000..d6fa94f12ce7fc4da8f3bd5467059183c6c567dd --- /dev/null +++ b/doc/jbootadmin/attachment.md @@ -0,0 +1,3 @@ +# 分布式文件同步 + +购买后获得阅读权限。 \ No newline at end of file diff --git a/doc/jbootadmin/buy.md b/doc/jbootadmin/buy.md new file mode 100644 index 0000000000000000000000000000000000000000..edc01cc2f51adc256cf84d8cb27ad50899a2ae84 --- /dev/null +++ b/doc/jbootadmin/buy.md @@ -0,0 +1,3 @@ +# 购买 JbootAdmin + +购买 JbootAdmin 请联系海哥微信:wx198819880 \ No newline at end of file diff --git a/doc/jbootadmin/cdn.md b/doc/jbootadmin/cdn.md new file mode 100644 index 0000000000000000000000000000000000000000..32f5615c7f9360739a2ac59a00cdb6b5e5f2b836 --- /dev/null +++ b/doc/jbootadmin/cdn.md @@ -0,0 +1,8 @@ +# CDN 配置 + +CDN 配置,我们只需要在 `jboot.properties` 里,添加如下代码。 + +``` +jboot.web.cdn.enable = true +jboot.web.cdn.domain = http://your-cdn-daomain.com +``` \ No newline at end of file diff --git a/doc/jbootadmin/config.md b/doc/jbootadmin/config.md new file mode 100644 index 0000000000000000000000000000000000000000..bf00d2bd20e9f146724a920dd4b348516f91c91f --- /dev/null +++ b/doc/jbootadmin/config.md @@ -0,0 +1,3 @@ +# 分布式配置中心 + +购买后获得阅读权限。 \ No newline at end of file diff --git a/doc/jbootadmin/db.md b/doc/jbootadmin/db.md new file mode 100644 index 0000000000000000000000000000000000000000..13c6de98683fe32e5538f581f452efdca61af2b7 --- /dev/null +++ b/doc/jbootadmin/db.md @@ -0,0 +1,513 @@ +# JbootAdmin 数据库设计 + +JbootAdmin 目前的表结构如下: + + +**账户信息表** 用来存储每个账户的基本信息、账号密码 +```sql +CREATE TABLE `account` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tenant_id` bigint(20) unsigned DEFAULT NULL COMMENT '租户用户ID,租户ID是自己的时候,自己是租户的管理员', + `superior_tenant_id` bigint(20) unsigned DEFAULT NULL COMMENT '上级租户ID', + `loginname` varchar(128) DEFAULT NULL COMMENT '登录名', + `nickname` varchar(128) DEFAULT NULL COMMENT '昵称', + `password` varchar(128) DEFAULT NULL COMMENT '密码', + `salt` varchar(32) DEFAULT NULL COMMENT '盐', + `email` varchar(64) DEFAULT NULL COMMENT '邮件', + `mobile` varchar(32) DEFAULT NULL COMMENT '手机电话', + `domain` varchar(32) DEFAULT NULL COMMENT 'SaaS系统给用户分配的域名', + `avatar` varchar(256) DEFAULT NULL COMMENT '账户头像', + `type` tinyint(2) DEFAULT NULL COMMENT '账户类型', + `status` tinyint(2) DEFAULT NULL COMMENT '状态', + `created` datetime DEFAULT NULL COMMENT '创建日期', + `activated` datetime DEFAULT NULL COMMENT '激活时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `loginname` (`loginname`) USING BTREE, + UNIQUE KEY `email` (`email`) USING BTREE, + UNIQUE KEY `mobile` (`mobile`) USING BTREE, + KEY `created` (`created`) USING BTREE, + KEY `tenant_id` (`tenant_id`) USING BTREE, + KEY `superior_tenant_id` (`superior_tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='账号信息表,保存用户账号信息。'; +``` + +**账户地址** 用户的收货地址表 +```sql +CREATE TABLE `account_address` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `account_id` bigint(20) unsigned NOT NULL COMMENT '账户 id', + `username` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '姓名', + `mobile` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号', + `province` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '省', + `city` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '市', + `district` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '区(县)', + `detail` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '详细地址到门牌号', + `zipcode` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮政编码', + `default_enable` tinyint(1) DEFAULT '0' COMMENT '是否默认,1是,0否', + `options` text COLLATE utf8mb4_unicode_ci COMMENT '扩展字段', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `created` datetime DEFAULT NULL COMMENT '新增时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `account_id` (`account_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='收货地址'; +``` + +**用户属性表** 用来保持用户的扩展属性 +```sql +CREATE TABLE `account_attr` ( + `account_id` bigint(20) unsigned NOT NULL COMMENT '账户ID', + `name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '属性名称', + `content` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '属性内容', + PRIMARY KEY (`account_id`,`name`) USING BTREE, + KEY `content` (`content`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='账户属性表'; +``` + +**账户部门关系表** 账户和部门的 多对多 映射关系 +```sql +CREATE TABLE `account_dept` ( + `account_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '账户ID', + `dept_id` bigint(20) unsigned NOT NULL COMMENT '部门ID', + PRIMARY KEY (`dept_id`,`account_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='部门和账户的多对多关系表'; +``` + +**账户第三方关系表** +```sql +CREATE TABLE `account_openid` ( + `account_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID', + `type` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '第三方类型:wechat,dingding,qq...', + `openid` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '第三方的openId的值', + `access_token` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '可能用不到', + `expired_time` datetime DEFAULT NULL COMMENT 'access_token的过期时间', + `nickname` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称', + `avatar` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像', + `options` text COLLATE utf8mb4_unicode_ci COMMENT '其他扩展属性', + `created` datetime DEFAULT NULL COMMENT '创建日期', + `modified` datetime DEFAULT NULL COMMENT '修改日期', + PRIMARY KEY (`type`,`account_id`) USING BTREE, + KEY `type_openid` (`type`,`openid`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='账号第三方账号绑定表'; +``` + +```sql +CREATE TABLE `account_option` ( + `account_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '账户ID', + `name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '扩展属性名称', + `content` text COLLATE utf8mb4_unicode_ci COMMENT '扩展属性内容', + PRIMARY KEY (`account_id`,`name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='账户配置'; +``` + +```sql +CREATE TABLE `account_permission` ( + `account_id` bigint(20) unsigned NOT NULL COMMENT '账户ID', + `permission_id` varchar(128) NOT NULL DEFAULT '' COMMENT '权限ID', + `own` tinyint(1) DEFAULT NULL COMMENT '1 拥有, 0 排除', + PRIMARY KEY (`account_id`,`permission_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='账户和权限的多对多映射表,这个很少用到,只有在极特殊情况下,可以通过这个对某些权限进行添加或者排除'; +``` + +```sql +CREATE TABLE `account_receive_msg` ( + `account_id` bigint(20) unsigned DEFAULT NULL COMMENT '账户id', + `send_msg_type` tinyint(2) unsigned DEFAULT NULL COMMENT '可接收消息类型', + KEY `account_id` (`account_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='通知功能'; +``` + +```sql +CREATE TABLE `account_role` ( + `account_id` bigint(20) unsigned NOT NULL COMMENT '账户ID', + `role_id` bigint(20) unsigned NOT NULL COMMENT '权限ID', + PRIMARY KEY (`account_id`,`role_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='账户和角色的多对多映射表'; +``` + +```sql +CREATE TABLE `account_scope` ( + `account_id` bigint(20) unsigned NOT NULL COMMENT '账户ID', + `scope_account_id` bigint(20) unsigned NOT NULL COMMENT '可以操作其他账户数据的账户ID', + PRIMARY KEY (`account_id`,`scope_account_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='账户的操作范围(只可以操作哪些用户的数据)'; +``` + +```sql +CREATE TABLE `account_session` ( + `id` varchar(32) NOT NULL DEFAULT '' COMMENT 'session id', + `client` varchar(32) NOT NULL DEFAULT '' COMMENT '客户端', + `platform` varchar(32) DEFAULT NULL COMMENT '登录平台', + `account_id` bigint(20) unsigned NOT NULL COMMENT '账户id', + `expire_at` datetime NOT NULL COMMENT '到期时间', + `options` text COMMENT '扩展属性', + `created` datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='账户登录session'; +``` + +```sql +CREATE TABLE `account_station` ( + `account_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '账户ID', + `station_id` bigint(20) unsigned NOT NULL COMMENT '岗位ID', + PRIMARY KEY (`station_id`,`account_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='岗位和账户的多对多关系'; +``` + +```sql +CREATE TABLE `dev_project` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '项目名称', + `module_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '模块名称', + `old_module_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '旧模块名称,用于在模块名称更新的时候,需要记录之前的模块名称', + `package_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '模块报名', + `description` text COLLATE utf8mb4_unicode_ci COMMENT '模块描述', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='项目表,对应的是 idea(或者eclipse)的代码模块'; +``` + +```sql +CREATE TABLE `dev_tablefield` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '字段名称', + `table_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '字段所在的表名', + `title` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '字段的标题', + `render_type` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '渲染类型', + `valid_required` tinyint(1) DEFAULT NULL COMMENT '是否必填', + `valid_mobile` tinyint(1) DEFAULT NULL COMMENT '是否是手机', + `valid_email` tinyint(1) DEFAULT NULL COMMENT '是否是邮件', + `show_in_list` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否在列表里显示', + `show_in_edit` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否在编辑页面编辑', + `search_type` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '搜索类型 like、相等', + PRIMARY KEY (`id`) USING BTREE, + KEY `table_name` (`table_name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='表字段配置'; +``` + +```sql +CREATE TABLE `dev_tableinfo` ( + `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '表名', + `project_id` int(11) DEFAULT NULL COMMENT '所在项目', + `controller_mapping` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Controller 的 url 映射', + `title` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '标题', + `alias` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '别名', + `help_text` text COLLATE utf8mb4_unicode_ci COMMENT '帮助内容', + `crumbs_text` text COLLATE utf8mb4_unicode_ci COMMENT '面包屑内容', + `description` text COLLATE utf8mb4_unicode_ci COMMENT '描述或备注', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='表生成内容配置'; +``` + +```sql +CREATE TABLE `log_action` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `account_id` bigint(20) unsigned DEFAULT NULL COMMENT '用户ID', + `tenant_id` bigint(20) unsigned DEFAULT NULL, + `action` varchar(512) DEFAULT NULL COMMENT '访问路径', + `name` varchar(128) DEFAULT NULL, + `query` varchar(512) DEFAULT NULL COMMENT '访问参数', + `ip` varchar(64) DEFAULT NULL COMMENT 'IP', + `ua` varchar(1024) DEFAULT NULL COMMENT '浏览器', + `created` datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `account_id` (`account_id`) USING BTREE, + KEY `tenant_id` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户行为日志'; +``` + +```sql +CREATE TABLE `log_login` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `account_id` bigint(20) unsigned NOT NULL COMMENT '登录账户', + `tenant_id` bigint(20) unsigned DEFAULT NULL, + `client` varchar(100) DEFAULT NULL COMMENT '登录客户端', + `ip` varchar(100) DEFAULT NULL COMMENT '登录的I IP 地址', + `ua` varchar(512) DEFAULT NULL COMMENT '登录的浏览器 ua', + `platform` varchar(32) DEFAULT NULL COMMENT '登录平台', + `created` datetime NOT NULL COMMENT '登录时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `account_id` (`account_id`) USING BTREE, + KEY `tenant_id` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户登录日志'; +``` + +```sql +CREATE TABLE `sys_cnarea` ( + `id` mediumint(7) unsigned NOT NULL AUTO_INCREMENT, + `level` tinyint(2) unsigned NOT NULL COMMENT '层级', + `parent_code` bigint(14) unsigned NOT NULL DEFAULT '0' COMMENT '父级行政代码', + `area_code` bigint(14) unsigned NOT NULL DEFAULT '0' COMMENT '行政代码', + `zip_code` mediumint(6) unsigned zerofill NOT NULL DEFAULT '000000' COMMENT '邮政编码', + `city_code` char(6) NOT NULL DEFAULT '' COMMENT '区号', + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称', + `short_name` varchar(50) NOT NULL DEFAULT '' COMMENT '简称', + `merger_name` varchar(50) NOT NULL DEFAULT '' COMMENT '组合名', + `pinyin` varchar(30) NOT NULL DEFAULT '' COMMENT '拼音', + `lng` decimal(10,6) NOT NULL DEFAULT '0.000000' COMMENT '经度', + `lat` decimal(10,6) NOT NULL DEFAULT '0.000000' COMMENT '纬度', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_code` (`area_code`) USING BTREE, + KEY `idx_parent_code` (`parent_code`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='中国行政地区表'; +``` + +```sql +CREATE TABLE `sys_dept` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '部门ID', + `type` tinyint(2) DEFAULT NULL COMMENT '部门类型', + `pid` bigint(20) unsigned DEFAULT NULL COMMENT '上级部门id', + `code` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '部门编码', + `name` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '部门名称', + `description` text COLLATE utf8mb4_unicode_ci COMMENT '部门描述', + `sort_no` int(11) DEFAULT NULL COMMENT '排序编号', + `tenant_id` bigint(20) unsigned DEFAULT NULL COMMENT '租户ID', + PRIMARY KEY (`id`) USING BTREE, + KEY `tenant_id` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='部门表'; +``` + +```sql +CREATE TABLE `sys_dept_account` ( + `dept_id` bigint(20) unsigned NOT NULL COMMENT '部门ID', + `account_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '账户ID', + PRIMARY KEY (`dept_id`,`account_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='部门和账户的多对多关系表'; +``` + +```sql +CREATE TABLE `sys_menu` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `menu_id` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单对应html的id属性的值', + `menu_pid` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '通过 PID 来设置上下级关系', + `text` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '菜单内容', + `icon` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '菜单ICON', + `url` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '菜单URL地址', + `target` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '菜单的打开类型', + `type` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '菜单类型', + `platform` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '菜单所在的平台,一个系统下可能存在多个平台', + `sort_no` int(11) DEFAULT NULL COMMENT '排序值', + `options` text COLLATE utf8mb4_unicode_ci COMMENT '其他扩展字段', + PRIMARY KEY (`id`) USING BTREE, + KEY `platform` (`platform`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='系统菜单'; +``` + +```sql +CREATE TABLE `sys_message` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '消息标题', + `level` tinyint(2) NOT NULL COMMENT '内容级别(1普通 2一般 3紧急 4弹窗)', + `type` tinyint(2) DEFAULT NULL COMMENT '内容类型(1系统 2业务 3公告 4其它)', + `content` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '消息内容', + `receive_type` tinyint(2) NOT NULL COMMENT '接受者类型(0全部 1用户 2部门 3角色 4岗位)', + `send_account_id` bigint(20) unsigned DEFAULT NULL COMMENT '发送者用户编码', + `send_account_name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '发送者用户姓名', + `send_date` datetime DEFAULT NULL COMMENT '发送时间', + `notify_type` tinyint(2) DEFAULT NULL COMMENT '通知类型(PC APP 短信 邮件 微信)多选', + `status` tinyint(2) NOT NULL COMMENT '状态(0正常 1删除 4审核 5驳回 9草稿)', + `reply_status` tinyint(2) DEFAULT NULL COMMENT '是否支持回复等', + `reply_id` bigint(20) unsigned DEFAULT NULL COMMENT '回复的消息ID', + `created_by` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建者', + `created` datetime NOT NULL COMMENT '创建时间', + `modified_by` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '更新者', + `modified` datetime NOT NULL COMMENT '更新时间', + `remarks` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注信息', + PRIMARY KEY (`id`) USING BTREE, + KEY `send_account_id` (`send_account_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='系统消息'; +``` + +```sql +CREATE TABLE `sys_message_send_record` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `message_id` bigint(20) unsigned NOT NULL COMMENT '所属消息', + `send_account_id` bigint(20) unsigned DEFAULT NULL COMMENT '发送消息用户', + `receive_account_id` bigint(20) unsigned DEFAULT NULL COMMENT '接受者用户', + `read_status` tinyint(2) NOT NULL COMMENT '读取状态(0未送达 1已读 2未读)', + `read_date` datetime DEFAULT NULL COMMENT '阅读时间', + `reply_status` tinyint(2) DEFAULT NULL COMMENT '回复状态', + `reply_date` datetime DEFAULT NULL COMMENT '回复时间', + `is_star` tinyint(1) DEFAULT NULL COMMENT '是否标星', + PRIMARY KEY (`id`) USING BTREE, + KEY `send_account_id` (`send_account_id`) USING BTREE, + KEY `receive_account_id` (`receive_account_id`) USING BTREE, + KEY `read_status` (`read_status`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='消息发送记录表'; +``` + +```sql +CREATE TABLE `sys_option` ( + `name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '配置key', + `content` text COLLATE utf8mb4_unicode_ci COMMENT '配置内容', + PRIMARY KEY (`name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='系统配置表'; +``` + +```sql +CREATE TABLE `sys_permission` ( + `id` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限ID', + `group_id` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '所属的权限组ID', + `name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '权限名称', + `description` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '权限描述', + `type` tinyint(2) DEFAULT NULL COMMENT '权限类型,1 菜单,2 Action,3 逻辑权限, 4 敏感数据权限, 5 页面元素,6 其他类型', + `platform` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `sort_no` int(11) DEFAULT NULL COMMENT '权限排序,只用于展示', + `created` datetime DEFAULT NULL COMMENT '创建修改', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `group_id` (`group_id`) USING BTREE, + KEY `platform` (`platform`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='权限表'; +``` + +```sql +CREATE TABLE `sys_permission_group` ( + `id` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限组ID', + `name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '权限组名称', + `description` text COLLATE utf8mb4_unicode_ci COMMENT '权限组描述', + `sort_no` int(11) DEFAULT NULL COMMENT '权限组排序', + `platform` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '权限组所在的平台', + `type` tinyint(2) DEFAULT NULL COMMENT '权限组类型,1 菜单,2 Action,3 逻辑权限, 4 敏感数据权限, 5 页面元素,6 其他类型', + PRIMARY KEY (`id`) USING BTREE, + KEY `platform` (`platform`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='对权限进行分组,只用于显示的作用,不起逻辑控制作用'; +``` + +```sql +CREATE TABLE `sys_role` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '角色ID', + `pid` bigint(20) unsigned DEFAULT NULL COMMENT '父级 id', + `tenant_id` bigint(20) unsigned DEFAULT NULL COMMENT '住户 ID', + `name` varchar(128) NOT NULL DEFAULT '' COMMENT '角色名称', + `desc` text COMMENT '角色的描述', + `flag` varchar(64) DEFAULT '' COMMENT '角色标识,全局唯一,sa 为超级管理员', + `sort_no` int(11) DEFAULT NULL COMMENT '排序编码', + `created` datetime NOT NULL COMMENT '创建时间', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `tenant_id` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='角色表'; +``` + +```sql +CREATE TABLE `sys_role_permission` ( + `role_id` bigint(20) unsigned NOT NULL COMMENT '角色ID', + `permission_id` varchar(128) NOT NULL DEFAULT '' COMMENT '权限ID', + PRIMARY KEY (`role_id`,`permission_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='角色和权限的多对多映射表'; +``` + +```sql +CREATE TABLE `sys_station` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '岗位名称', + `type` tinyint(2) DEFAULT NULL COMMENT '岗位分类(高管、中层、基层)', + `sort_no` int(10) DEFAULT NULL COMMENT '岗位排序(升序)', + `status` tinyint(2) DEFAULT '0' COMMENT '状态(0正常 1删除 2停用)', + `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `modifiy_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者', + `modified` datetime DEFAULT NULL COMMENT '更新时间', + `remarks` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注信息', + `tenant_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '租户ID', + PRIMARY KEY (`id`) USING BTREE, + KEY `tenant_id` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='岗位表'; +``` + +```sql +CREATE TABLE `wechat_account` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '公众号名称', + `type` tinyint(4) DEFAULT NULL COMMENT '公众号类型', + `tenant_id` bigint(20) unsigned DEFAULT NULL COMMENT '租户ID', + `app_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '公众号的 APP ID', + `app_secret` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '公众号的 APP Secret', + `app_token` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '公众号配置的 token', + `encoding_aes_key` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Aes 加密 key', + `message_encrypt` tinyint(1) DEFAULT NULL COMMENT '是否进行加密', + `description` text COLLATE utf8mb4_unicode_ci COMMENT '公众号描述', + `sort_no` int(11) DEFAULT NULL COMMENT '排序编号', + `status` tinyint(2) DEFAULT NULL COMMENT '状态', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `app_id` (`app_id`) USING BTREE, + KEY `tenant_id` (`tenant_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='微信公众号表'; +``` + +```sql +CREATE TABLE `wechat_account_keyword` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `wechat_account_id` bigint(20) unsigned DEFAULT NULL COMMENT '归属公众号', + `wechat_account_appid` varchar(64) DEFAULT NULL, + `keyword` varchar(128) DEFAULT NULL COMMENT '关键字', + `reply_content_type` tinyint(2) DEFAULT NULL COMMENT '回复类型', + `reply_content` text COMMENT '回复内容,保存内容是一个 json', + `options` text, + `status` tinyint(2) DEFAULT NULL COMMENT '是否启用', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `ak` (`wechat_account_appid`,`keyword`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户自定义关键字回复表'; +``` + +```sql +CREATE TABLE `wechat_account_menu` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键 ID', + `pid` bigint(20) unsigned DEFAULT NULL COMMENT '父级ID', + `wechat_account_id` bigint(20) unsigned DEFAULT NULL COMMENT '归属公众号', + `text` varchar(512) DEFAULT NULL COMMENT '文本内容', + `keyword` varchar(128) DEFAULT NULL COMMENT '关键字', + `type` varchar(32) DEFAULT '' COMMENT '菜单类型', + `sort_no` int(11) DEFAULT '0' COMMENT '排序字段', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `status` tinyint(2) DEFAULT NULL COMMENT '状态', + PRIMARY KEY (`id`) USING BTREE, + KEY `wechat_account_id` (`wechat_account_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='微信公众号菜单表'; +``` + +```sql +CREATE TABLE `wechat_account_msg` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `wechat_account_id` bigint(20) unsigned DEFAULT NULL COMMENT '微信公众号ID', + `wechat_account_appid` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '微信公众号的 AppID', + `user_open_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '微信消息发送用户', + `user_nickname` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '发送消息的用户昵称', + `user_avatar` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '发送消息的用户头像', + `content_type` tinyint(2) DEFAULT NULL COMMENT '消息类型', + `content` text COLLATE utf8mb4_unicode_ci COMMENT '消息内容', + `created` datetime DEFAULT NULL COMMENT '消息的接收时间', + `is_replied` tinyint(1) DEFAULT NULL COMMENT '是否已经回复', + `reply_date` datetime DEFAULT NULL COMMENT '回复时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `wechat_account_id` (`wechat_account_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='微信公众号消息表'; +``` + +```sql +CREATE TABLE `wechat_account_msg_reply` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `wechat_account_id` bigint(20) unsigned DEFAULT NULL COMMENT '回复的微信公众号ID', + `reply_msg_id` bigint(20) unsigned DEFAULT NULL COMMENT '回复的消息ID', + `reply_to_open_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '回复的用户ID', + `reply_content_type` tinyint(2) DEFAULT NULL COMMENT '回复的消息类型', + `reply_content` text COLLATE utf8mb4_unicode_ci COMMENT '回复内容', + `created` datetime DEFAULT NULL COMMENT '回复时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `wechat_account_id` (`wechat_account_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='微信公众号消息回复表'; +``` + +```sql +CREATE TABLE `wechat_account_option` ( + `wechat_account_appid` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '微信公众号配置key', + `content` text COLLATE utf8mb4_unicode_ci COMMENT '微信公众号配置Neri', + PRIMARY KEY (`wechat_account_appid`,`name`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='微信公众号配置'; + +``` \ No newline at end of file diff --git a/doc/jbootadmin/deploy.md b/doc/jbootadmin/deploy.md new file mode 100644 index 0000000000000000000000000000000000000000..bdc1024a6fbb5dbcf1a86a4847cff54a7b01ef63 --- /dev/null +++ b/doc/jbootadmin/deploy.md @@ -0,0 +1,115 @@ +# JbootAdmin 部署文档 + + +JbootAdmin 部署过程主要分为以下几个步骤: +- 第一步:编译 +- 第二步:上传到服务器 +- 第三步:启动 + +## 第一步:编译 + +编译的目的是为了让我们的源码(包含 java、js、css 图片等)打包成一个可以被执行的 jar 文件,有了 jar 文件后,我们便可以通过 java 命令 java -jar 进行启动。 + +在 JbootAdmin 中,我们需要通过 Maven 进行编译的,因此,在编译之前需要在您的电脑里,安装好 maven ,配置好 Maven 的环境变量。而 Maven 是使用 Java 开发的,因此在您的电脑安装好 Java 也是必须的。 + +当安装好 Maven 后,我们需要通过命令窗口(shell) 方式进入项目所在根目录,然后执行如下代码: + +``` +mvn clean package +``` + +稍等片刻,会在 `starter-cms/target` 目录下,生成文件:starter-cms-1.0.0-jar-with-dependencies.jar。 + + +## 第二步:上传到服务器 + +上传的工具有很多,比如 xshell 等,关于工具的使用请自行参考相关工具的文档,如果您的电脑是 linux 或者 mac 电脑,可以通过如下命令上传。 + +``` +scp 本地文件目录 root@ip:/服务器目录 +``` + +如下的命令是将本地文件 starter-cms-1.0.0-jar-with-dependencies.jar 上传到服务器 192.168.1.100 的 /data/wwww 目录里。 + +``` +scp /your/path/to/starter-cms-1.0.0-jar-with-dependencies.jar root@192.168.1.100:/data/wwww +``` + +## 第三步:启动 + +通过 ssh 登录到服务器后,进入到 `starter-cms-1.0.0-jar-with-dependencies.jar` 文件所在目录,执行命令 + +java -jar starter-cms-1.0.0-jar-with-dependencies.jar 就可以启动项目,但是这种启动的方式是 "前台" 模式,当我们推出命令窗口后,项目也会自动停止了,因此,我们需要通过 nohup 将其后台启动。 + + +命令如下: + +``` +nohup java -jar ./starter-cms-1.0.0-jar-with-dependencies.jar >./log_cms.log 2>&1 & +``` + +同时项目的相关错误日志会输出到 ./log_cms.log 这个文件里。 + +为了更加方便我们启动项目,我们也可以在 `starter-cms-1.0.0-jar-with-dependencies.jar` 目录下写一个 shell 脚本,用来启动或者关闭我们的项目,例如: + +``` +#!/bin/bash + +COMMAND="$1" + +if [[ "$COMMAND" != "start" ]] && [[ "$COMMAND" != "stop" ]] && [[ "$COMMAND" != "restart" ]]; then + echo "Usage: $0 start | stop | restart" + exit 0 +fi + + +# 生成 class path 值 + +function start() +{ + # 运行为后台进程,并且将信息输出到 log_cms.log 文件 + nohup java -jar ./starter-cms-1.0.0-jar-with-dependencies.jar --jboot.app.mode=product >./log_cms.log 2>&1 & +} + +function stop() +{ + kill `pgrep -f starter-cms` 2>/dev/null +} + +if [[ "$COMMAND" == "start" ]]; then + start +elif [[ "$COMMAND" == "stop" ]]; then + stop +else + stop + start +fi +``` + +假设这个文件名称为 `cms_deploy.sh` ,我们可以执行如下命令对项目进行控制 + +- 启动项目 `./cms_deploy.sh start` +- 停止项目 `./cms_deploy.sh stop` +- 重启项目 `./cms_deploy.sh restart` + + +## 注意事项: + +### 1、配置问题 + +当我们把项目打包成一个 jar 文件后,所有的配置内容都存放在 jar 文件里的 jboot.porperites 里了,无法对其进行修改,但是如果一定要修改其配置,可以把这个配置添加到启动参数里,例如 + +``` +java -jar ./starter-cms-1.0.0-jar-with-dependencies.jar --jboot.app.mode=product +``` + +等同于在 jboot.properties 文件里添加了 `jboot.app.mode=product` 的配置,而且会覆盖 jboot.properties 原本的配置。其他参数同理。 + +另一个方案是在项目里有两个 properties 文件,分别是 jboot.properties 和 jboot-product.properties 文件,当我们启动应用的时候,如果当前的 app 模式是 product,那么系统将会优先读取 +`jboot-product.properties` 文件里的内容,只有读取不到的时候,才会去读取 `jboot.propertie` 的内容。更多关于配置问题,请参考:http://jbootprojects.gitee.io/docs/docs/config.html + + +### 2、安全问题 + +一般情况下,我们不会把数据库(或者 redis等)的明文密码直接配置在 `jboot.properties` 文件里,配置在 `jboot.properties` 里的密码一般都是加密的,关于配置内容加密的解决方案,请参考: +http://jbootprojects.gitee.io/docs/docs/config.html#%E9%85%8D%E7%BD%AE%E5%86%85%E5%AE%B9%E5%8A%A0%E5%AF%86%E8%A7%A3%E5%AF%86 diff --git a/doc/jbootadmin/feature.md b/doc/jbootadmin/feature.md new file mode 100644 index 0000000000000000000000000000000000000000..c464e5df30ef73ee89b8c1f4ffe2188a30426c48 --- /dev/null +++ b/doc/jbootadmin/feature.md @@ -0,0 +1,324 @@ +# JbootAdmin 功能介绍 + +[[TOC]] + +## 基础功能 + + +### 账户管理 + +![](./features/account.png) + +### 部门管理 + +![](./features/dept.jpeg) + +### 职位管理 + +![](./features/station.jpeg) + +### 角色管理 + +![](./features/role.jpeg) + + +### 角色的权限分配 +对角色的权限进行分配、包括菜单的权限、功能的权限、逻辑权限(根据业务进行人为定义的权限)、敏感数据权限(根据业务进行人为定义的、涉及数据敏感的权限) + +![](./features/role_permission.jpeg) + +### 参数配置 + +![](./features/sys_option.jpeg) + +### 行政区划 + +![](./features/sys_area.jpeg) + +### 数据字典 +JbootAdmin 中的数据字典不同于其他传统意义的数据字典。JbootAdmin 的数据字典可以生成枚举代码 `Enum` ,通过 `Enum` 又可以方便在在 Java 代码和模板中使用。 + +比如,在后台创建的枚举,可以直接生成如下代码: + +```java +@JFinalSharedEnum +public enum PayType { + + ALIPAY(1, "支付宝"), + WECHAT(10, "微信支付"), + + public int value; + public String text; + + PayType(int value, String text) { + this.value = value; + this.text = text; + } + + + public static String text(Integer value) { + if (value != null) { + for (AccountType type : values()) { + if (type.value == value) { + return type.text; + } + } + } + return null; + } + + + public static boolean isAlipay(Integer value) { + return value != null && ALIPAY.value == value; + } + +} +``` + +而以上代码又可以方便在 Java 或者 模板中使用,例如: + +```html + + + #(PayType.ALIPAY.text)
+ #(PayType.text(1))
+ #(PayType.isAlipay(1))
+ + +``` + +![](./features/sys_dict.jpeg) + +![](./features/sys_dict_edit.jpeg) + +### 微信公众号对接 + +支持多个微信公众号,支持菜单配置、根据关键字自动回复、默认回复配置 等等 + +![](./features/wechat_account.jpeg) + +自动回复 +![](./features/wechat_reply.jpeg) + +通过关键字自动回复... + +![](./features/wechat_keyword.jpeg) + +![](./features/wechat_keyword_config.jpeg) + + +## 特色功能(独创) + +### 特色功能1:同一套代码支持 Tab 模式和独立页面模式 +JbootAdmin 同一套代码,后台支持 Tab 模式,也支持独立页面模式,同时 Tab 模式和独立页面模式支持用户自主切换,也支持后台配置为固定,不允许用户切换。 如下图所示是 Tab 模式: + +![](./images/jbootadmin-demo.jpg) + +### 特色功能2:免手动维护的权限列表 +在一般的系统中,需要我们一边开发,一边手动定义系统有哪些权限,但是在 JbootAdmin 中,所有的权限都是免手动维护的,我们可以通过后台,一键自动生成权限列表,存储到数据库里去。这样避免了繁杂的人为手动维护,也大大减少了出错的可能性。 + +![](./features/build_permission.jpeg) + +### 特色功能3:免手动维护的系统菜单 + +原理同免维护的权限列表。后台一键构建左边菜单的功能。 + +![](./features/build_memu.jpeg) + + +## API文档自动生成 + +Jboot API 功能自动根据代码、自动生成文档、Debug 页面、对数据进行 Mock 等功能。 + +### API 代码 + + +![](./features/apidoc_code.png) + +### API 生成文档 + +![](./features/apidoc_info.jpeg) + +### API Debug + +通过 Debug 功能,我们可以方便的对 API 进行调试。 + +![](./features/apidoc_debug.jpeg) + +### API Mock + +通过 API Mock 功能,我们可以模拟 API 数据,在前后端分离的场景下,我们可以使用此功能先给前端团队正常调用数据,等我们完成 API 开发再删除 Mock 数据。 + +![](./features/apidoc_mock.jpeg) + +## 强大代码生成功能 + +在使用 JbootAdmin 的代码生成器之前,我们可以先创建项目,然后对该项目进行数据配置。 + +### 项目列表 +![](./features/dev_project.jpeg) + +### 创建项目,配置数据源 +![](./features/dev_project_edit.jpeg) + +### 根据数据表,生成代码 +![](./features/dev_codgen.jpeg) + +### 根据某个表,生成 Model、Service、Provider、Controller、Html 等代码 +![](./features/dev_codgen_gen.jpeg) + +### 配置某个表对应的 Controller 映射等 +![](./features/dev_codgen_edit.jpeg) + +### 对表的字段进行配置等 +![](./features/dev_codgen_fields.jpeg) + + +## 运维功能 + +JbootAdmin 提供了强大的运维功能。1 是内置的功能,2 是通过适配第三方来进行配置。 + +### 分布式应用列表 + +可以查看分布式下的某个应用情况 + +![](./features/devops_apps.jpeg) + +### 分布式监控大盘 + +可以查看分布式下,每个应用所在的机器硬件情况。 + +![](./features/devops_d.jpeg) + +### 分布式缓存监控 + +对分布式缓存情况查看,刷新等,支持了 ehcache、redis、caffeine、ehredis 等等。 + +![](./features/devops_cache.jpeg) + +### Sentinel 分布式限流 + +支持自建 Sentinel 控制,也支持阿里云的 AHAS 进行限流控制。 + +![](./features/devops_ahas.png) + +### 基于 Nacos 门户网关自动发现 + +![](./features/devops_gateway_nacos1.jpeg) + +![](./features/devops_gateway_nacos2.jpeg) + +### 基于 Grafana 对 Jboot 的 JVM 进行监控 + +![](./features/devops_grafana_jboot_jvm.png) + +### 更多 + + 基于 Dubbo Admin 对 Jboot RPC 控制等不再截图... + +## Demos示例 + +JbootAdmin 提供了一些 Demos 示例,方便用户对 JbootAdmin 内置的前端组件进行全面的了解。 + +### 产品列表 + +支持在产品列表里对产品进行基本的操作 + +![](./features/demo_product.jpeg) + +### 产品编辑 + +在产品编辑中可以对产品的属性进行配置、支持多规格、多单位等等... + +![](./features/demo_product_edit.jpeg) + +### 产品库存入库单 + +![](./features/demo_warehousein.jpeg) + + +### 查看入库单 + +![](./features/demo_warehousein_view.jpeg) + + +### 编辑入库单 + +![](./features/demo_warehousein_edit1.jpeg) + +![](./features/demo_warehousein_edit2.jpeg) + + + +## 课堂8 - 在线教育系统 + +课堂8 是一个基于 JbootAdmin 开发的在线教育系统。 + +### 课堂8 - 首页 + +![](./features/ketang8_index.jpeg) + +### 课堂8 - 课程详情 + +在课程详情中,可以设置课程的标题、简介、营销简介、章节目录,是否免费试看、限时价(秒杀价)、会员价等等.... + +![](./features/ketang8_course_detail.jpeg) + +### 课堂8 - 在线学习 + +当用户未登录时,需要登录才能观看。 +![](./features/ketang8_course_study.jpeg) + +用户登录后,可以正常观看视频,观看的过程中,会记录课程的当前进度,用户可以通过用户中心再次进入观看,继续学习。 +![](./features/ketang8_course_study2.jpeg) + +### 课堂8 - 用户微信注册或登录 + +其流程是:扫码微信二维码 → 关注我们的公众号 → 自动注册。这个过程,不需要用户填写信息。因此,他的注册成本大大降低。 + +![](./features/ketang8_login_wechat.jpeg) + +### 课堂8 - 用户手机登录 + +![](./features/ketang8_login_mobile1.jpeg) + +![](./features/ketang8_login_mobile2.jpeg) + +### 课堂8 - 用户中心在线学习 + +在用户中心中,可以看到自己的学习时间、每个课程的学习进度等等。 + +![](./features/ketang8_ucenter_studied.jpeg) + + +### 课堂8 - 修改个人资料 + +![](./features/ketang8_ucenter_modify.jpeg) + + +### 课堂8 - 修改个人头像 + +修改头像中,设计的技术包含了,上传图片、到分布式附件中心,对分布式附件里的图片进行预览和在线剪辑等等功能... + +![](./features/ketang8_ucenter_avatar.jpeg) + +### 课堂8 - 在线支付购买课程 + +在线支付环节,看起来内容少,但工作量和细节是巨大的。 + +- 在 PC 模式下,必须支持 微信支付 和 支付宝支付的选择。 +- 在 微信 里,必须隐藏掉微信支付和支付宝支付的选择方式,只能用微信支付。 +- 在 H5 浏览器里(比如 UC 浏览器),能够选择支付方式,并在支付的时候自动唤起(打开)手机里的支付宝和微信的 APP 进行支付。 + +![](./features/ketang8_buy.jpeg) + +![](./features/ketang8_buy1.png) + +### 课堂8 - 更多 + +除此之外,我们还做了很多你看不见的工作: + +- 基于阿里云的视频加密播放。 +- 前后台分离部署,前后台分别部署在不同的机器里。 +- 涉及到的Web安全防护,比如 XSS、CSRF 等等。 +- ...... \ No newline at end of file diff --git a/doc/jbootadmin/features/account.png b/doc/jbootadmin/features/account.png new file mode 100644 index 0000000000000000000000000000000000000000..dbb7b2be232362b28aee3385d9d612caffccdd76 Binary files /dev/null and b/doc/jbootadmin/features/account.png differ diff --git a/doc/jbootadmin/features/action_log.jpeg b/doc/jbootadmin/features/action_log.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..664c5b7c2ee067ee732f96c9212cc4ff752ba89e Binary files /dev/null and b/doc/jbootadmin/features/action_log.jpeg differ diff --git a/doc/jbootadmin/features/apidoc_code.png b/doc/jbootadmin/features/apidoc_code.png new file mode 100644 index 0000000000000000000000000000000000000000..946d25db3b0e91a8a67bd6ac1aea752f1285154b Binary files /dev/null and b/doc/jbootadmin/features/apidoc_code.png differ diff --git a/doc/jbootadmin/features/apidoc_debug.jpeg b/doc/jbootadmin/features/apidoc_debug.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..adff7e65e044e6abb4b6b838de2563fb4b53aec0 Binary files /dev/null and b/doc/jbootadmin/features/apidoc_debug.jpeg differ diff --git a/doc/jbootadmin/features/apidoc_info.jpeg b/doc/jbootadmin/features/apidoc_info.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..125dd3a9ec416d78c30597d6bb4399d37f64cdf1 Binary files /dev/null and b/doc/jbootadmin/features/apidoc_info.jpeg differ diff --git a/doc/jbootadmin/features/apidoc_mock.jpeg b/doc/jbootadmin/features/apidoc_mock.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cd9c032df9a695086c75066e1cbc67d2a54ce663 Binary files /dev/null and b/doc/jbootadmin/features/apidoc_mock.jpeg differ diff --git a/doc/jbootadmin/features/build_memu.jpeg b/doc/jbootadmin/features/build_memu.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..607b26fb07c59f38da15bc1e435e04bcf0258823 Binary files /dev/null and b/doc/jbootadmin/features/build_memu.jpeg differ diff --git a/doc/jbootadmin/features/build_permission.jpeg b/doc/jbootadmin/features/build_permission.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..93cb2a55d7c3e535c774e3c932c4d838838fc269 Binary files /dev/null and b/doc/jbootadmin/features/build_permission.jpeg differ diff --git a/doc/jbootadmin/features/demo_product.jpeg b/doc/jbootadmin/features/demo_product.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..adb998873fef6ddde251f200b4c445a6b7523d0d Binary files /dev/null and b/doc/jbootadmin/features/demo_product.jpeg differ diff --git a/doc/jbootadmin/features/demo_product_edit.jpeg b/doc/jbootadmin/features/demo_product_edit.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..694ae75da40ee76ad017a8c173ae3b663173b423 Binary files /dev/null and b/doc/jbootadmin/features/demo_product_edit.jpeg differ diff --git a/doc/jbootadmin/features/demo_warehousein.jpeg b/doc/jbootadmin/features/demo_warehousein.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..89dc6a7055fecc8211ea5ccf846a267c811ef31a Binary files /dev/null and b/doc/jbootadmin/features/demo_warehousein.jpeg differ diff --git a/doc/jbootadmin/features/demo_warehousein_edit1.jpeg b/doc/jbootadmin/features/demo_warehousein_edit1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b35b4c39d28cd609c2e46bfc8408fdb7b43ddce8 Binary files /dev/null and b/doc/jbootadmin/features/demo_warehousein_edit1.jpeg differ diff --git a/doc/jbootadmin/features/demo_warehousein_edit2.jpeg b/doc/jbootadmin/features/demo_warehousein_edit2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..2308bfcda12b01e549cb3fc6039af0d61628e723 Binary files /dev/null and b/doc/jbootadmin/features/demo_warehousein_edit2.jpeg differ diff --git a/doc/jbootadmin/features/demo_warehousein_view.jpeg b/doc/jbootadmin/features/demo_warehousein_view.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..58852dbec017a973df1dec5ca443428b0795b3ef Binary files /dev/null and b/doc/jbootadmin/features/demo_warehousein_view.jpeg differ diff --git a/doc/jbootadmin/features/dept.jpeg b/doc/jbootadmin/features/dept.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..38e74dee9cf21146ad4229a915a364babcfeac39 Binary files /dev/null and b/doc/jbootadmin/features/dept.jpeg differ diff --git a/doc/jbootadmin/features/dev_codgen.jpeg b/doc/jbootadmin/features/dev_codgen.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..a050dd0e85cd93767d24887f0017a963a8a42c29 Binary files /dev/null and b/doc/jbootadmin/features/dev_codgen.jpeg differ diff --git a/doc/jbootadmin/features/dev_codgen_edit.jpeg b/doc/jbootadmin/features/dev_codgen_edit.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..39a601fb018434bfb36b37d9b8a0959d51afd095 Binary files /dev/null and b/doc/jbootadmin/features/dev_codgen_edit.jpeg differ diff --git a/doc/jbootadmin/features/dev_codgen_fields.jpeg b/doc/jbootadmin/features/dev_codgen_fields.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..fc3761f83971997281261b57414ab17137ca7ca0 Binary files /dev/null and b/doc/jbootadmin/features/dev_codgen_fields.jpeg differ diff --git a/doc/jbootadmin/features/dev_codgen_gen.jpeg b/doc/jbootadmin/features/dev_codgen_gen.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..4467b8efdd928667b0aea7420df1559f75f9e654 Binary files /dev/null and b/doc/jbootadmin/features/dev_codgen_gen.jpeg differ diff --git a/doc/jbootadmin/features/dev_menu_gen.jpeg b/doc/jbootadmin/features/dev_menu_gen.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..dfcf02af968f32e54fa752ff5e71a5a029c3d34f Binary files /dev/null and b/doc/jbootadmin/features/dev_menu_gen.jpeg differ diff --git a/doc/jbootadmin/features/dev_permission_gen.jpeg b/doc/jbootadmin/features/dev_permission_gen.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..0c235f759fa072b7ba038605d0c539f10cb8dbf8 Binary files /dev/null and b/doc/jbootadmin/features/dev_permission_gen.jpeg differ diff --git a/doc/jbootadmin/features/dev_project.jpeg b/doc/jbootadmin/features/dev_project.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..615fdc39b3e50ee8412dd1ca45c3819913a3be84 Binary files /dev/null and b/doc/jbootadmin/features/dev_project.jpeg differ diff --git a/doc/jbootadmin/features/dev_project_edit.jpeg b/doc/jbootadmin/features/dev_project_edit.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..737c819e269efbbad055b0ed1071454d1f0e163d Binary files /dev/null and b/doc/jbootadmin/features/dev_project_edit.jpeg differ diff --git a/doc/jbootadmin/features/devops_ahas.png b/doc/jbootadmin/features/devops_ahas.png new file mode 100644 index 0000000000000000000000000000000000000000..c39e62d13e8c76ded274e236446d33b3e5317f49 Binary files /dev/null and b/doc/jbootadmin/features/devops_ahas.png differ diff --git a/doc/jbootadmin/features/devops_apps.jpeg b/doc/jbootadmin/features/devops_apps.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d41201949f13128f76de15db865856cc44b6977d Binary files /dev/null and b/doc/jbootadmin/features/devops_apps.jpeg differ diff --git a/doc/jbootadmin/features/devops_cache.jpeg b/doc/jbootadmin/features/devops_cache.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8eeb421f5b012f4ed859d9eb8425c04e9f596a02 Binary files /dev/null and b/doc/jbootadmin/features/devops_cache.jpeg differ diff --git a/doc/jbootadmin/features/devops_d.jpeg b/doc/jbootadmin/features/devops_d.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b18971a9d2a663c6f77f940473328ff004a1c93e Binary files /dev/null and b/doc/jbootadmin/features/devops_d.jpeg differ diff --git a/doc/jbootadmin/features/devops_gateway_nacos1.jpeg b/doc/jbootadmin/features/devops_gateway_nacos1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..0ec1ff19ec042b8a087449840f6b72964b9a5497 Binary files /dev/null and b/doc/jbootadmin/features/devops_gateway_nacos1.jpeg differ diff --git a/doc/jbootadmin/features/devops_gateway_nacos2.jpeg b/doc/jbootadmin/features/devops_gateway_nacos2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..869dc72d03f58ad57d2f5ff9908a387c29c8234e Binary files /dev/null and b/doc/jbootadmin/features/devops_gateway_nacos2.jpeg differ diff --git a/doc/jbootadmin/features/devops_grafana_jboot_jvm.png b/doc/jbootadmin/features/devops_grafana_jboot_jvm.png new file mode 100644 index 0000000000000000000000000000000000000000..c9e29539cf6291675207124cdddaa0e1c9bcef45 Binary files /dev/null and b/doc/jbootadmin/features/devops_grafana_jboot_jvm.png differ diff --git a/doc/jbootadmin/features/ketang8_buy.jpeg b/doc/jbootadmin/features/ketang8_buy.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c0afce2fe235fe7ef835391c523a58208c39fd6c Binary files /dev/null and b/doc/jbootadmin/features/ketang8_buy.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_buy1.png b/doc/jbootadmin/features/ketang8_buy1.png new file mode 100644 index 0000000000000000000000000000000000000000..b178e66f273eb8da92bf28a16564208d1c6d59e2 Binary files /dev/null and b/doc/jbootadmin/features/ketang8_buy1.png differ diff --git a/doc/jbootadmin/features/ketang8_course_detail.jpeg b/doc/jbootadmin/features/ketang8_course_detail.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b33aaf9f7dde907ce895f61219d4ebd943252dbc Binary files /dev/null and b/doc/jbootadmin/features/ketang8_course_detail.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_course_study.jpeg b/doc/jbootadmin/features/ketang8_course_study.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..57ee184b719665f6efecbcbe1de9a6ff2bdbecfa Binary files /dev/null and b/doc/jbootadmin/features/ketang8_course_study.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_course_study2.jpeg b/doc/jbootadmin/features/ketang8_course_study2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..52daa47b386a46958c5d897e6755a50e94e40a14 Binary files /dev/null and b/doc/jbootadmin/features/ketang8_course_study2.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_index.jpeg b/doc/jbootadmin/features/ketang8_index.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7b8e3f9389724083556d548109f0c0d2087b3c79 Binary files /dev/null and b/doc/jbootadmin/features/ketang8_index.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_login_mobile1.jpeg b/doc/jbootadmin/features/ketang8_login_mobile1.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8baaa1576e910e9f64a611639e0d62471a16609d Binary files /dev/null and b/doc/jbootadmin/features/ketang8_login_mobile1.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_login_mobile2.jpeg b/doc/jbootadmin/features/ketang8_login_mobile2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..857e16177689006a178446c1695adecd4add0dbd Binary files /dev/null and b/doc/jbootadmin/features/ketang8_login_mobile2.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_login_wechat.jpeg b/doc/jbootadmin/features/ketang8_login_wechat.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f2ecae1ccc942b1a84a5beb031ff021ade6c79c2 Binary files /dev/null and b/doc/jbootadmin/features/ketang8_login_wechat.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_ucenter_avatar.jpeg b/doc/jbootadmin/features/ketang8_ucenter_avatar.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1c086f7f71cc7168346431cb1e2e0ae1059f32d5 Binary files /dev/null and b/doc/jbootadmin/features/ketang8_ucenter_avatar.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_ucenter_modify.jpeg b/doc/jbootadmin/features/ketang8_ucenter_modify.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..17d5e0809c5119b79cafa375066f6f8ad7495bb2 Binary files /dev/null and b/doc/jbootadmin/features/ketang8_ucenter_modify.jpeg differ diff --git a/doc/jbootadmin/features/ketang8_ucenter_studied.jpeg b/doc/jbootadmin/features/ketang8_ucenter_studied.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..608d19603f4ead4c1fc247318a26e5e43152e217 Binary files /dev/null and b/doc/jbootadmin/features/ketang8_ucenter_studied.jpeg differ diff --git a/doc/jbootadmin/features/role.jpeg b/doc/jbootadmin/features/role.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..ac1ae47f0f9e418cd6e34c202912a7192677aa7d Binary files /dev/null and b/doc/jbootadmin/features/role.jpeg differ diff --git a/doc/jbootadmin/features/role_permission.jpeg b/doc/jbootadmin/features/role_permission.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..dc47fbffb4b3f0d841505b52025367d06ba5f282 Binary files /dev/null and b/doc/jbootadmin/features/role_permission.jpeg differ diff --git a/doc/jbootadmin/features/station.jpeg b/doc/jbootadmin/features/station.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..79ff0d795a6544ce1e59e9f15b15e962754e93d8 Binary files /dev/null and b/doc/jbootadmin/features/station.jpeg differ diff --git a/doc/jbootadmin/features/sys_area.jpeg b/doc/jbootadmin/features/sys_area.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..a88d80713f619a48484700d75f6f5af8d66914e6 Binary files /dev/null and b/doc/jbootadmin/features/sys_area.jpeg differ diff --git a/doc/jbootadmin/features/sys_dict.jpeg b/doc/jbootadmin/features/sys_dict.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d44fae72b9d2f2c73f3476d00b072aeceacbe11c Binary files /dev/null and b/doc/jbootadmin/features/sys_dict.jpeg differ diff --git a/doc/jbootadmin/features/sys_dict_edit.jpeg b/doc/jbootadmin/features/sys_dict_edit.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..75475206ff244d217b274179639ed5e8fb7d5815 Binary files /dev/null and b/doc/jbootadmin/features/sys_dict_edit.jpeg differ diff --git a/doc/jbootadmin/features/sys_option.jpeg b/doc/jbootadmin/features/sys_option.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..9c6b930a2a67876d33467d414f774feebc792641 Binary files /dev/null and b/doc/jbootadmin/features/sys_option.jpeg differ diff --git a/doc/jbootadmin/features/wechat_account.jpeg b/doc/jbootadmin/features/wechat_account.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..6a4dd9a24d42f1c0e026a18da8516d32b3c0c15b Binary files /dev/null and b/doc/jbootadmin/features/wechat_account.jpeg differ diff --git a/doc/jbootadmin/features/wechat_keyword.jpeg b/doc/jbootadmin/features/wechat_keyword.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..91fb3e2b9fb51e5a614c27ec8ca5a3b4a449c0ea Binary files /dev/null and b/doc/jbootadmin/features/wechat_keyword.jpeg differ diff --git a/doc/jbootadmin/features/wechat_keyword_config.jpeg b/doc/jbootadmin/features/wechat_keyword_config.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cadab2cdb0c6f3e7e8be693955d34e22381c9e10 Binary files /dev/null and b/doc/jbootadmin/features/wechat_keyword_config.jpeg differ diff --git a/doc/jbootadmin/features/wechat_reply.jpeg b/doc/jbootadmin/features/wechat_reply.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..5400f7019e462eee675536b2de3c650a467b184a Binary files /dev/null and b/doc/jbootadmin/features/wechat_reply.jpeg differ diff --git a/doc/jbootadmin/front.md b/doc/jbootadmin/front.md new file mode 100644 index 0000000000000000000000000000000000000000..6c3b3fe9cb78e79a0a86f77c9997c9d521cf5d81 --- /dev/null +++ b/doc/jbootadmin/front.md @@ -0,0 +1,133 @@ +# 前端组件 + +JbootAdmin 内置了大量的组件,我们几乎不需要编写任何的 JavaScript 代码就可以轻松的使用这些前段组件。 + +## Layer + +Layer 是一个功能强大的弹出层,弹出层里既可以通过一个 Div 来渲染,也可以通过一个 IFrame 来渲染弹出。 + +示例: + +```html + 示例 +``` + +当我们点击该 `a` 标签链接时,此链接将以弹窗的方式访问 `user.html` 。若是 `input` 或者 `textarea` 等添加了 `open-type="layer"` 属性,那么当 `input` 或者 `textarea` 获得焦点的时候,会自动打开弹窗 Layer。 + + +在组件中,可以配置的属性如下: + + +- data-layer-type : 打开 layer 的类型 +- data-layer-title : 打开 layer 的标题 +- data-layer-anim : 打开 layer 的动画 +- data-layer-shade-close +- data-layer-shade +- data-layer-area +- data-layer-content +- data-layer-binds : layer 关闭后,对数据进行绑定 +- data-layer-end : 当 layer 关闭后,执行的方法 + +## Switchery 开关 + + + +```html + +``` + +其支持的属性内容如下: + +- for: 自动同步 switchery 的值到其他的 input +- data-ctrl: 用于控制某个 div 显示或隐藏 +- data-open-sync :当此 switchery 开启的时候,自动同步到其他的 switchery 一起开启。 +- data-close-sync : 当此 switchery 关闭的时候,自动同步其他的 switchery 一起关闭。 +- data-change-function : 当此 switchery 点击开启或者关闭时执行的方法 + + +## 确认操作弹出组件 + +在很多场景下,在用户要进行某个操作之前,我们需要弹出一个对话框让用户进行确定。比如我们要删除某个数据,我们需要弹出一个确认框让用户确认之后,在进行下一步的删除行动。 + + +```html + +``` + +或者 如下代码用于 删除数据 的弹出确认。 + +```html + +``` + + + +其支持的属性内容如下: + +- data-title : 弹出确认框标题 +- data-text: 弹出确认框内容 +- data-btn-text: 弹出确认框按钮内容 +- data-success-title : 成功弹出标题 +- data-success-text: 成功弹出内容 +- data-success-function : ajax 提交数据成功执行的方法 +- data-success-goto: ajax 执行数据成功后,跳转到的页面url地址 +- data-success-message: ajax 支持成功后,弹出提示内容 +- data-fail-function: ajax 提交失败后,执行的 js 方法 +- data-fail-message: ajax 提交失败后,弹出的提示内容。 + + +## 链接自动 Ajax 提交 + +```html + +``` + +当 `a` 标签有 `open-type="ajax"` 时,我们点击 `a` 标签的时候,自动会自动以 ajax 方式提交到 `/href/to/your/path` ,但是往往我们需要执行完成 ajax 提交后,会执行某些动作,此时需要添加 + +`data-success-function` 属性。 + +```html + + + + + + +
+
+

WebSocket Chat

+
+ + +
+ +
+ + +
+
+
+ + \ No newline at end of file diff --git a/src/main/java/io/jboot/Jboot.java b/src/main/java/io/jboot/Jboot.java index db8257ad4b1d3e32606f53150098e39e8982bc10..604833df700903edf0ea116c629aa9aea4fb078c 100644 --- a/src/main/java/io/jboot/Jboot.java +++ b/src/main/java/io/jboot/Jboot.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package io.jboot; import com.codahale.metrics.MetricRegistry; import com.jfinal.aop.Aop; +import io.jboot.aop.JbootAopFactory; import io.jboot.app.config.JbootConfigManager; import io.jboot.components.cache.JbootCache; import io.jboot.components.cache.JbootCacheManager; @@ -25,12 +26,13 @@ import io.jboot.components.event.JbootEventManager; import io.jboot.components.mq.Jbootmq; import io.jboot.components.mq.JbootmqManager; import io.jboot.components.rpc.JbootrpcManager; -import io.jboot.components.rpc.JbootrpcServiceConfig; +import io.jboot.components.rpc.JbootrpcReferenceConfig; import io.jboot.components.serializer.JbootSerializer; import io.jboot.components.serializer.JbootSerializerManager; import io.jboot.support.metric.JbootMetricManager; import io.jboot.support.redis.JbootRedis; import io.jboot.support.redis.JbootRedisManager; +import io.jboot.utils.StrUtil; public class Jboot { @@ -55,6 +57,17 @@ public class Jboot { } + /** + * 获取指定的缓存 + * + * @param name + * @return + */ + public static JbootCache getCache(String name) { + return JbootCacheManager.me().getCache(name); + } + + /** * 获取 JbootRedis 工具类,方便操作Redis请求 * @@ -64,11 +77,21 @@ public class Jboot { return JbootRedisManager.me().getRedis(); } + /** + * 获取指定的 Redis + * + * @param name + * @return + */ + public static JbootRedis getRedis(String name) { + return JbootRedisManager.me().getRedis(name); + } + /** * 获取 MetricRegistry * - * @return // + * @return */ public static MetricRegistry getMetric() { return JbootMetricManager.me().metric(); @@ -84,6 +107,16 @@ public class Jboot { return JbootmqManager.me().getJbootmq(); } + /** + * 获取指定的 MQ + * + * @param name + * @return + */ + public static Jbootmq getMq(String name) { + return JbootmqManager.me().getJbootmq(name); + } + /** * 获取序列化对象 @@ -95,6 +128,17 @@ public class Jboot { } + /** + * 获取序列化对象 + * @param name + * @return + */ + public static JbootSerializer getSerializer(String name) { + return JbootSerializerManager.me().getSerializer(name); + } + + + /** * 获取配置信息 * @@ -144,6 +188,19 @@ public class Jboot { } + /** + * 读取某个配置信息 + * + * @param key + * @param defaultValue 当获取不到的时候发挥此默认值 + * @return + */ + public static String configValue(String key, String defaultValue) { + String value = configValue(key); + return StrUtil.isNotBlank(value) ? value : defaultValue; + } + + /** * 获取 RPC 服务 * @@ -152,7 +209,7 @@ public class Jboot { * @return */ public static T service(Class clazz) { - return service(clazz, new JbootrpcServiceConfig()); + return service(clazz, new JbootrpcReferenceConfig()); } /** @@ -163,7 +220,7 @@ public class Jboot { * @param * @return */ - public static T service(Class clazz, JbootrpcServiceConfig config) { + public static T service(Class clazz, JbootrpcReferenceConfig config) { return JbootrpcManager.me().getJbootrpc().serviceObtain(clazz, config); } @@ -173,7 +230,7 @@ public class Jboot { * @param event */ public static void sendEvent(JbootEvent event) { - JbootEventManager.me().pulish(event); + JbootEventManager.me().publish(event); } /** @@ -188,27 +245,25 @@ public class Jboot { /** - * 使用 Aop.get 代替 + * 根据类名获取 Aop 下的 Bean * * @param clazz * @param * @return */ - @Deprecated - public static T bean(Class clazz) { + public static T getBean(Class clazz) { return Aop.get(clazz); } - /** - * 使用 Aop.inject 代替 + * 根据名称获取 Aop 下的 Bean * - * @param object + * @param name + * @param + * @return */ - @Deprecated - public static void injectMembers(Object object) { - Aop.inject(object); + public static T getBean(String name) { + return JbootAopFactory.me().getBean(name); } - } diff --git a/src/main/java/io/jboot/JbootConsts.java b/src/main/java/io/jboot/JbootConsts.java index 852f4bb487732092967f612726c635ffe8cbcda8..cfe68ca32946d433ae21fd5a26d823e418ee0ba2 100644 --- a/src/main/java/io/jboot/JbootConsts.java +++ b/src/main/java/io/jboot/JbootConsts.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,9 @@ package io.jboot; */ public class JbootConsts { - public static String VERSION = "3.0.1"; + public static String VERSION = "4.1.4"; - public static final String ATTR_REQUEST = "REQUEST"; public static final String ATTR_CONTEXT_PATH = "CPATH"; diff --git a/src/main/java/io/jboot/aop/DefaultValueInterceptor.java b/src/main/java/io/jboot/aop/DefaultValueInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..5822a955462d498ba263f2792fb8f340123a9a7e --- /dev/null +++ b/src/main/java/io/jboot/aop/DefaultValueInterceptor.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.aop.annotation.DefaultValue; +import io.jboot.core.weight.Weight; +import io.jboot.utils.ObjectUtil; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +@AutoLoad +@Weight(999) +public class DefaultValueInterceptor implements Interceptor, InterceptorBuilder { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + DefaultValue defaultValue = parameters[index].getAnnotation(DefaultValue.class); + if (defaultValue != null) { + Object arg = inv.getArg(index); + if (arg == null || isPrimitiveDefaultValue(arg, parameters[index].getType())) { + Object value = ObjectUtil.convert(defaultValue.value(), parameters[index].getType()); + if (value != null) { + inv.setArg(index, value); + } + } + } + } + + inv.invoke(); + } + + public static boolean isPrimitiveDefaultValue(Object value, Class paraClass) { + if (paraClass == int.class || paraClass == long.class || paraClass == float.class || paraClass == double.class || paraClass == short.class) { + return ((Number) value).intValue() == 0; + } else if (paraClass == boolean.class) { + return !(boolean) value; + } + return false; + } + + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + Parameter[] parameters = method.getParameters(); + if (parameters != null && parameters.length > 0) { + for (Parameter p : parameters) { + if (p.getAnnotation(DefaultValue.class) != null) { + interceptors.addIfNotExist(this); + break; + } + } + } + + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/aop/InterceptorBuilder.java b/src/main/java/io/jboot/aop/InterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0cfe9302233a29c9e9f6d08924ba4cae468803 --- /dev/null +++ b/src/main/java/io/jboot/aop/InterceptorBuilder.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop; + +import com.jfinal.core.Controller; +import io.jboot.web.controller.JbootController; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + * @author michael yang (fuhai999@gmail.com) + *

+ * InterceptorBuilder 用于控制某个方法已经添加好的拦截器,可以对其删除或者添加 + * + *

+ * 配置方法:
+ * public void onInit() {
+ *     InterceptorBuilderManager.me().addInterceptorBuilder(new MyInterceptorBuilder());
+ * }
+ *
+ * 或者给 MyInterceptorBuilder 类添加 @AutoLoad 注解
+ * 
+ */ +public interface InterceptorBuilder { + + void build(Class targetClass, Method method, Interceptors interceptors); + + class Util { + + public static boolean isChildClassOf(Class childClass, Class parentClass) { + return parentClass.isAssignableFrom(childClass); + } + + public static boolean isController(Class serviceClass) { + return Controller.class.isAssignableFrom(serviceClass); + } + + public static boolean isJbootController(Class serviceClass) { + return JbootController.class.isAssignableFrom(serviceClass); + } + + public static
boolean hasAnnotation(Class targetClass, Class annotationClass) { + return targetClass.getAnnotation(annotationClass) != null; + } + + + public static boolean hasAnnotation(Method method, Class annotationClass) { + return method.getAnnotation(annotationClass) != null; + } + + + public static boolean hasAnyAnnotation(Class targetClass, Class... annotationClass) { + for (Class clazz : annotationClass) { + if (hasAnnotation(targetClass, clazz)) { + return true; + } + } + return false; + } + + + public static boolean hasAnyAnnotation(Method method, Class... annotationClass) { + for (Class clazz : annotationClass) { + if (hasAnnotation(method, clazz)) { + return true; + } + } + return false; + } + + + public static Annotation getAnyAnnotation(Class targetClass, Class... annotationClass) { + for (Class clazz : annotationClass) { + Annotation a = targetClass.getAnnotation(clazz); + if (a != null) { + return a; + } + } + return null; + } + + + public static Annotation getAnyAnnotation(Method method, Class... annotationClass) { + for (Class clazz : annotationClass) { + Annotation a = method.getAnnotation(clazz); + if (a != null) { + return a; + } + } + return null; + } + + + public static boolean hasAnnotation(Class targetClass, Method method, Class annotationClass) { + return hasAnnotation(targetClass, annotationClass) || hasAnnotation(method, annotationClass); + } + } +} diff --git a/src/main/java/io/jboot/aop/InterceptorBuilderManager.java b/src/main/java/io/jboot/aop/InterceptorBuilderManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c61b52a2f509a4cf306698050216756ed05380d --- /dev/null +++ b/src/main/java/io/jboot/aop/InterceptorBuilderManager.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop; + +import com.jfinal.aop.Interceptor; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.core.weight.WeightUtil; +import io.jboot.utils.ClassScanner; +import io.jboot.utils.ClassUtil; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Predicate; + +public class InterceptorBuilderManager{ + + + private static InterceptorBuilderManager me = new InterceptorBuilderManager(); + + + public static InterceptorBuilderManager me() { + return me; + } + + private InterceptorBuilderManager() { + List> builderClasses = ClassScanner.scanSubClass(InterceptorBuilder.class,true); + for (Class builderClass : builderClasses){ + if (builderClass.getAnnotation(AutoLoad.class) != null){ + addInterceptorBuilder(ClassUtil.newInstance(builderClass,false)); + } + } + + InterceptorCache.clear(); + } + + private List interceptorBuilders = new CopyOnWriteArrayList(); + + + public List getInterceptorBuilders() { + return interceptorBuilders; + } + + + + public void addInterceptorBuilder(InterceptorBuilder interceptorBuilder) { + if (interceptorBuilder == null) { + throw new NullPointerException("interceptorBuilder must not be null."); + } + this.interceptorBuilders.add(interceptorBuilder); + WeightUtil.sort(this.interceptorBuilders); + + InterceptorCache.clear(); + } + + + + + public void addInterceptorBuilder(Collection interceptorBuilders) { + if (interceptorBuilders == null) { + throw new NullPointerException("interceptorBuilder must not be null."); + } + this.interceptorBuilders.addAll(interceptorBuilders); + WeightUtil.sort(this.interceptorBuilders); + + InterceptorCache.clear(); + } + + + + public void removeInterceptorBuilder(Predicate filter){ + if (interceptorBuilders != null && !interceptorBuilders.isEmpty()){ + if (interceptorBuilders.removeIf(filter)) { + InterceptorCache.clear(); + } + } + } + + + + public Interceptor[] build(Class targetClass, Method method, Interceptor[] inters) { + if (interceptorBuilders != null && interceptorBuilders.size() > 0) { + Interceptors interceptors = new Interceptors(inters); + for (InterceptorBuilder builder : interceptorBuilders) { + builder.build(targetClass, method, interceptors); + } + return interceptors.toArray(); + } + return inters; + } + + + +} diff --git a/src/main/java/io/jboot/aop/InterceptorCache.java b/src/main/java/io/jboot/aop/InterceptorCache.java new file mode 100644 index 0000000000000000000000000000000000000000..79efd8f2c41649922d03edf8303a5b4a28f70d16 --- /dev/null +++ b/src/main/java/io/jboot/aop/InterceptorCache.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop; + +import com.jfinal.aop.Interceptor; +import com.jfinal.kit.HashKit; +import com.jfinal.kit.SyncWriteMap; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Objects; + +public class InterceptorCache { + + private static final Map cache = new SyncWriteMap<>(2048, 0.25F); + + public static void put(MethodKey methodKey, Interceptor[] inters) { + Objects.requireNonNull(methodKey, "methodKey can not be null"); + Objects.requireNonNull(inters, "inters can not be null"); + + cache.putIfAbsent(methodKey, inters); + } + + public static Interceptor[] get(MethodKey methodKey) { + return cache.get(methodKey); + } + + public static MethodKey getMethodKey(Class target, Method method) { + long paraHash = HashKit.FNV_OFFSET_BASIS_64; + Class[] paraTypes = method.getParameterTypes(); + for (Class pt : paraTypes) { + paraHash ^= pt.getName().hashCode(); + paraHash *= HashKit.FNV_PRIME_64; + } + + return new MethodKey(target.getName().hashCode(), method.getName().hashCode(), paraHash); + } + + public static void clear() { + cache.clear(); + } + + + public static class MethodKey { + final int classHash; + final int methodHash; + final long paraHash; + + MethodKey(int classHash, int methodHash, long paraHash) { + this.classHash = classHash; + this.methodHash = methodHash; + this.paraHash = paraHash; + } + + @Override + public int hashCode() { + return classHash ^ methodHash ^ ((int) paraHash); + } + + /** + * 通过比较三部分 hash 值,避免超大规模场景下可能的 key 值碰撞 + *

+ * 不必判断 if (methodKey instanceof MethodKey),因为所有 key 类型必须要相同 + * 不必判断 if (this == methodKey),因为每次用于取值的 methodKey 都是新建的 + */ + @Override + public boolean equals(Object methodKey) { + MethodKey mk = (MethodKey) methodKey; + return mk != null && classHash == mk.classHash && methodHash == mk.methodHash && paraHash == mk.paraHash; + } + + @Override + public String toString() { + return "classHash = " + classHash + "\nmethodHash = " + methodHash + "\nparaHash = " + paraHash; + } + } +} diff --git a/src/main/java/io/jboot/aop/Interceptors.java b/src/main/java/io/jboot/aop/Interceptors.java new file mode 100644 index 0000000000000000000000000000000000000000..b61bedb621893942603ca59539aa02c4e1966129 --- /dev/null +++ b/src/main/java/io/jboot/aop/Interceptors.java @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop; + +import com.jfinal.aop.Aop; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.InterceptorManager; +import io.jboot.utils.ClassUtil; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class Interceptors { + + private List warppers = new ArrayList<>(); + + private int minimalWeight = 1; + private int currentWeight = 1; + + public Interceptors() { + } + + public Interceptors(Interceptor[] inters) { + if (inters != null && inters.length > 0) { + for (Interceptor interceptor : inters) { + add(interceptor); + } + } + } + + public void add(Interceptor interceptor) { + warppers.add(new InterceptorWarpper(interceptor, currentWeight++)); + } + + public void add(Class interceptorClass) { + add(singleton(interceptorClass)); + } + + public void add(Interceptor interceptor, int weight) { + warppers.add(new InterceptorWarpper(interceptor, weight)); + } + + public void add(Class interceptorClass, int weight) { + add(singleton(interceptorClass), weight); + } + + + public void addIfNotExist(Interceptor interceptor) { + if (!contains(interceptor)) { + add(interceptor); + } + } + + public void addIfNotExist(Class interceptorClass) { + if (!contains(interceptorClass)) { + add(singleton(interceptorClass)); + } + } + + + public void addToFirst(Interceptor interceptor) { + warppers.add(new InterceptorWarpper(interceptor, --minimalWeight)); + } + + public void addToFirst(Class interceptorClass) { + addToFirst(singleton(interceptorClass)); + } + + + public void addToFirstIfNotExist(Interceptor interceptor) { + if (!contains(interceptor)) { + warppers.add(new InterceptorWarpper(interceptor, --minimalWeight)); + } + } + + public void addToFirstIfNotExist(Class interceptorClass) { + if (!contains(interceptorClass)) { + addToFirst(singleton(interceptorClass)); + } + } + + public boolean addBefore(Interceptor interceptor, Predicate filter) { + Integer weight = null; + for (InterceptorWarpper warpper : warppers) { + if (filter.test(warpper.interceptor)) { + weight = warpper.weight; + break; + } + } + + //所有在 新加 的拦截器往前推 1 + if (weight != null) { + for (InterceptorWarpper warpper : warppers) { + if (warpper.weight < weight) { + warpper.weight--; + } + } + minimalWeight--; + warppers.add(new InterceptorWarpper(interceptor, --weight)); + return true; + } else { + return false; + } + } + + public boolean addBefore(Class interceptorClass, Predicate filter) { + return addBefore(singleton(interceptorClass), filter); + } + + public boolean addBefore(Interceptor interceptor, Class toClass) { + return addBefore(interceptor, interceptor1 -> interceptor1.getClass() == toClass); + } + + public boolean addBefore(Class interceptorClass, Class toClass) { + return addBefore(singleton(interceptorClass), toClass); + } + + + public boolean addAfter(Interceptor interceptor, Predicate filter) { + Integer weight = null; + for (InterceptorWarpper warpper : warppers) { + if (filter.test(warpper.interceptor)) { + weight = warpper.weight; + break; + } + } + + //所有在 新加 的拦截器往后推 1 + if (weight != null) { + for (InterceptorWarpper warpper : warppers) { + if (warpper.weight > weight) { + warpper.weight++; + } + } + currentWeight++; + warppers.add(new InterceptorWarpper(interceptor, ++weight)); + return true; + } else { + return false; + } + } + + public boolean addAfter(Class interceptorClass, Predicate filter) { + return addAfter(singleton(interceptorClass), filter); + } + + + public boolean addAfter(Interceptor interceptor, Class toClass) { + return addAfter(interceptor, interceptor1 -> interceptor1.getClass() == toClass); + } + + + public boolean addAfter(Class interceptorClass, Class toClass) { + return addAfter(singleton(interceptorClass), toClass); + } + + + public boolean remove(Interceptor interceptor) { + return warppers.removeIf(interceptorWarpper -> interceptorWarpper.interceptor == interceptor); + } + + + public boolean remove(Predicate predicate) { + return warppers.removeIf(warpper -> predicate.test(warpper.interceptor)); + } + + + public boolean remove(Class clazz) { + return warppers.removeIf(interceptorWarpper -> interceptorWarpper.interceptor.getClass() == clazz); + } + + + public Integer getWeight(Interceptor interceptor) { + for (InterceptorWarpper warpper : warppers) { + if (warpper.interceptor == interceptor) { + return warpper.weight; + } + } + return null; + } + + + public Integer getWeight(Class clazz) { + for (InterceptorWarpper warpper : warppers) { + if (warpper.interceptor.getClass() == clazz) { + return warpper.weight; + } + } + return null; + } + + + public List toList() { + // 排序:weight 越小越靠前 + warppers.sort(Comparator.comparingInt(InterceptorWarpper::getWeight)); + return warppers.stream().map(InterceptorWarpper::getInterceptor) + .collect(Collectors.toList()); + } + + + public Interceptor[] toArray() { + if (warppers == null || warppers.size() == 0) { + return InterceptorManager.NULL_INTERS; + } else { + // 排序:weight 越小越靠前 + warppers.sort(Comparator.comparingInt(InterceptorWarpper::getWeight)); + + Interceptor[] inters = new Interceptor[warppers.size()]; + for (int i = 0; i < warppers.size(); i++) { + inters[i] = warppers.get(i).getInterceptor(); + } + return inters; + } + } + + + public boolean contains(Interceptor interceptor) { + for (InterceptorWarpper warpper : warppers) { + if (warpper.interceptor == interceptor) { + return true; + } + } + return false; + } + + + public boolean contains(Class clazz) { + for (InterceptorWarpper warpper : warppers) { + if (warpper.interceptor.getClass() == clazz) { + return true; + } + } + return false; + } + + + public int getMinimalWeight() { + return minimalWeight; + } + + public int getCurrentWeight() { + return currentWeight; + } + + + private Interceptor singleton(Class interceptorClass) { + return ClassUtil.singleton(interceptorClass, false); + } + + public List getWarppers() { + return warppers; + } + + static class InterceptorWarpper { + + private Interceptor interceptor; + private int weight; + + public InterceptorWarpper(Interceptor interceptor, int weight) { + this.interceptor = Aop.inject(interceptor); + this.weight = weight; + } + + public Interceptor getInterceptor() { + return interceptor; + } + + public void setInterceptor(Interceptor interceptor) { + this.interceptor = interceptor; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + } + +} diff --git a/src/main/java/io/jboot/aop/JbootAopFactory.java b/src/main/java/io/jboot/aop/JbootAopFactory.java index 71624bd6c23c42fbcc2b4ad8ee495f3434f4eb76..11487b28fa2b9d5f45439c5c63f151a9cd924e80 100644 --- a/src/main/java/io/jboot/aop/JbootAopFactory.java +++ b/src/main/java/io/jboot/aop/JbootAopFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,40 +18,42 @@ package io.jboot.aop; import com.jfinal.aop.AopFactory; import com.jfinal.aop.Inject; import com.jfinal.core.Controller; -import com.jfinal.ext.proxy.CglibProxyFactory; -import com.jfinal.kit.LogKit; import com.jfinal.log.Log; import com.jfinal.plugin.activerecord.Model; import com.jfinal.proxy.Proxy; import com.jfinal.proxy.ProxyManager; -import io.jboot.aop.annotation.Bean; -import io.jboot.aop.annotation.BeanExclude; -import io.jboot.aop.annotation.ConfigValue; -import io.jboot.aop.annotation.StaticConstruct; +import io.jboot.aop.annotation.*; +import io.jboot.aop.cglib.JbootCglibProxyFactory; +import io.jboot.aop.javassist.JbootJavassistProxyFactory; +import io.jboot.app.JbootApplicationConfig; +import io.jboot.app.config.JbootConfigKit; import io.jboot.app.config.JbootConfigManager; import io.jboot.app.config.annotation.ConfigModel; import io.jboot.components.event.JbootEventListener; import io.jboot.components.mq.JbootmqMessageListener; -import io.jboot.components.rpc.Jbootrpc; -import io.jboot.components.rpc.JbootrpcManager; -import io.jboot.components.rpc.JbootrpcServiceConfig; +import io.jboot.components.rpc.*; import io.jboot.components.rpc.annotation.RPCInject; import io.jboot.db.model.JbootModel; +import io.jboot.exception.JbootException; import io.jboot.service.JbootServiceBase; import io.jboot.utils.*; import io.jboot.web.controller.JbootController; +import javax.annotation.PostConstruct; import java.io.Serializable; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class JbootAopFactory extends AopFactory { private static final Log LOG = Log.getLog(JbootAopFactory.class); //排除默认的映射 - private final static Class[] DEFAULT_EXCLUDES_MAPPING_CLASSES = new Class[]{ + private final static Class[] DEFAULT_EXCLUDES_MAPPING_CLASSES = new Class[]{ JbootEventListener.class , JbootmqMessageListener.class , Serializable.class @@ -64,14 +66,32 @@ public class JbootAopFactory extends AopFactory { return me; } + private boolean defaultLazyInit = false; + + public boolean isDefaultLazyInit() { + return defaultLazyInit; + } + + public void setDefaultLazyInit(boolean defaultLazyInit) { + this.defaultLazyInit = defaultLazyInit; + } + + private Map beansCache = new ConcurrentHashMap<>(); + private Map> beanNameClassesMapping = new ConcurrentHashMap<>(); + private JbootAopFactory() { - ProxyManager.me().setProxyFactory(new CglibProxyFactory()); + + if ("javassist".equalsIgnoreCase(JbootApplicationConfig.get().getProxy())) { + ProxyManager.me().setProxyFactory(new JbootJavassistProxyFactory()); + } else { + ProxyManager.me().setProxyFactory(new JbootCglibProxyFactory()); + } + setInjectSuperClass(true); initBeanMapping(); } - @Override protected Object createObject(Class targetClass) { ConfigModel configModel = targetClass.getAnnotation(ConfigModel.class); @@ -87,31 +107,66 @@ public class JbootAopFactory extends AopFactory { return Proxy.get(targetClass); } + @Override protected void doInject(Class targetClass, Object targetObject) throws ReflectiveOperationException { targetClass = getUsefulClass(targetClass); - Field[] fields = targetClass.getDeclaredFields(); + doInjectTargetClass(targetClass, targetObject); + doInvokePostConstructMethod(targetClass, targetObject); + } - if (fields.length != 0) { - for (Field field : fields) { + /** + * 执行 @PostConstruct 注解方法 + * + * @param targetClass + * @param targetObject + * @throws ReflectiveOperationException + */ + protected void doInvokePostConstructMethod(Class targetClass, Object targetObject) throws ReflectiveOperationException { + Method[] methods = targetClass.getDeclaredMethods(); + for (Method method : methods) { + if (method.getParameterCount() == 0 && method.getAnnotation(PostConstruct.class) != null) { + method.setAccessible(true); + method.invoke(targetObject); + break; + } + } + + Class superClass = targetClass.getSuperclass(); + if (notSystemClass(superClass)) { + doInvokePostConstructMethod(superClass, targetObject); + } + } - Inject inject = field.getAnnotation(Inject.class); - if (inject != null) { - doInjectJFinalOrginal(targetObject, field, inject); - continue; - } - ConfigValue configValue = field.getAnnotation(ConfigValue.class); - if (configValue != null) { - doInjectConfigValue(targetObject, field, configValue); - continue; + /** + * 执行注入操作 + * + * @param targetClass + * @param targetObject + * @throws ReflectiveOperationException + */ + protected void doInjectTargetClass(Class targetClass, Object targetObject) throws ReflectiveOperationException { + Field[] fields = targetClass.getDeclaredFields(); + + if (fields.length != 0) { + for (Field field : fields) { + Object fieldValue = null; + if (defaultLazyInit) { + fieldValue = createFieldObjectLazy(targetObject, field); + } else { + Lazy Lazy = field.getAnnotation(Lazy.class); + if (Lazy != null) { + fieldValue = createFieldObjectLazy(targetObject, field); + } else { + fieldValue = createFieldObjectNormal(targetObject, field); + } } - RPCInject rpcInject = field.getAnnotation(RPCInject.class); - if (rpcInject != null) { - doInjectRPC(targetObject, field, rpcInject); - continue; + if (fieldValue != null) { + field.setAccessible(true); + field.set(targetObject, fieldValue); } } } @@ -119,20 +174,71 @@ public class JbootAopFactory extends AopFactory { // 是否对超类进行注入 if (injectSuperClass) { - Class c = targetClass.getSuperclass(); - if (c != JbootController.class - && c != Controller.class - && c != JbootServiceBase.class - && c != Object.class - && c != JbootModel.class - && c != Model.class - && c != null - ) { - doInject(c, targetObject); + Class superClass = targetClass.getSuperclass(); + if (notSystemClass(superClass)) { + doInjectTargetClass(superClass, targetObject); } } } + + protected Object createFieldObjectLazy(Object targetObject, Field field) throws ReflectiveOperationException { + return JbootLazyLoaderFactory.me().getLoader().loadLazyObject(targetObject, field); + } + + + public Object createFieldObjectNormal(Object targetObject, Field field) throws ReflectiveOperationException { + Inject inject = field.getAnnotation(Inject.class); + if (inject != null) { + Bean bean = field.getAnnotation(Bean.class); + String beanName = bean != null ? AnnotationUtil.get(bean.name()) : null; + if (StrUtil.isNotBlank(beanName)) { + return createFieldObjectByBeanName(targetObject, field, beanName); + } else { + return createFieldObjectByJfinalOriginal(targetObject, field, inject); + } + } + + RPCInject rpcInject = field.getAnnotation(RPCInject.class); + if (rpcInject != null) { + return createFieldObjectByRPCComponent(targetObject, field, rpcInject); + } + + ConfigValue configValue = field.getAnnotation(ConfigValue.class); + if (configValue != null) { + return createFieldObjectByConfigValue(targetObject, field, configValue); + } + + return null; + } + + + protected boolean notSystemClass(Class clazz) { + return clazz != JbootController.class + && clazz != Controller.class + && clazz != JbootServiceBase.class + && clazz != Object.class + && clazz != JbootModel.class + && clazz != Model.class + && clazz != null; + } + + + private Object createFieldObjectByBeanName(Object targetObject, Field field, String beanName) throws ReflectiveOperationException { + Object fieldInjectedObject = beansCache.get(beanName); + if (fieldInjectedObject == null) { + Class fieldInjectedClass = beanNameClassesMapping.get(beanName); + if (fieldInjectedClass == null || fieldInjectedClass == Void.class) { + fieldInjectedClass = field.getType(); + } + + fieldInjectedObject = doGet(fieldInjectedClass); + beansCache.put(beanName, fieldInjectedObject); + } + + return fieldInjectedObject; + } + /** * JFinal 原生 service 注入 * @@ -141,16 +247,13 @@ public class JbootAopFactory extends AopFactory { * @param inject * @throws ReflectiveOperationException */ - private void doInjectJFinalOrginal(Object targetObject, Field field, Inject inject) throws ReflectiveOperationException { + private Object createFieldObjectByJfinalOriginal(Object targetObject, Field field, Inject inject) throws ReflectiveOperationException { Class fieldInjectedClass = inject.value(); if (fieldInjectedClass == Void.class) { fieldInjectedClass = field.getType(); } - Object fieldInjectedObject = doGet(fieldInjectedClass); - - setFieldValue(field, targetObject, fieldInjectedObject); - + return doGet(fieldInjectedClass); } /** @@ -160,21 +263,18 @@ public class JbootAopFactory extends AopFactory { * @param field * @param rpcInject */ - private void doInjectRPC(Object targetObject, Field field, RPCInject rpcInject) { - + private Object createFieldObjectByRPCComponent(Object targetObject, Field field, RPCInject rpcInject) { try { - JbootrpcServiceConfig serviceConfig = new JbootrpcServiceConfig(rpcInject); Class fieldInjectedClass = field.getType(); - + JbootrpcReferenceConfig config = ReferenceConfigCache.get(fieldInjectedClass, rpcInject); Jbootrpc jbootrpc = JbootrpcManager.me().getJbootrpc(); - - Object fieldInjectedObject = jbootrpc.serviceObtain(fieldInjectedClass, serviceConfig); - - setFieldValue(field, targetObject, fieldInjectedObject); - + return jbootrpc.serviceObtain(fieldInjectedClass, config); + } catch (NullPointerException npe) { + LOG.error("Can not inject rpc service for \"" + field.getName() + "\" in class \"" + ClassUtil.getUsefulClass(targetObject.getClass()).getName() + "\", because @RPCInject.check ==\"true\" and target is not available. \n" + rpcInject, npe); } catch (Exception ex) { - LOG.error("can not inject rpc service in " + targetObject.getClass() + " by config " + rpcInject, ex); + LOG.error("Can not inject rpc service for \"" + field.getName() + "\" in class \"" + ClassUtil.getUsefulClass(targetObject.getClass()).getName() + "\" \n" + rpcInject, ex); } + return null; } /** @@ -185,33 +285,21 @@ public class JbootAopFactory extends AopFactory { * @param configValue * @throws IllegalAccessException */ - private void doInjectConfigValue(Object targetObject, Field field, ConfigValue configValue) throws IllegalAccessException { + private Object createFieldObjectByConfigValue(Object targetObject, Field field, ConfigValue configValue) throws IllegalAccessException { String key = AnnotationUtil.get(configValue.value()); Class fieldInjectedClass = field.getType(); - String value = getConfigValue(key, targetObject, field); + String value = JbootConfigManager.me().getConfigValue(key); + Object fieldObject = null; if (StrUtil.isNotBlank(value)) { - Object fieldInjectedObject = JbootConfigManager.me().convert(fieldInjectedClass, value); - - setFieldValue(field, targetObject, fieldInjectedObject); - return; + fieldObject = JbootConfigKit.convert(fieldInjectedClass, value, field.getGenericType()); } - if (configValue.requireNullOrBlank()) { + if (fieldObject == null) { field.setAccessible(true); - if (fieldInjectedClass == int.class) { - field.set(targetObject, 0); - } else if (fieldInjectedClass == boolean.class) { - field.set(targetObject, false); - } else { - field.set(targetObject, null); - } + fieldObject = field.get(targetObject); } - } - - - private String getConfigValue(String key, Object targetObject, Field field) { - return AnnotationUtil.getConfigValueByKeyString(key); + return fieldObject; } @@ -239,7 +327,6 @@ public class JbootAopFactory extends AopFactory { return this; } else { singletonCache.remove(mappingClass); - LogKit.warn("Aop Class[" + from + "] mapping changed from " + mappingClass + " to " + to); } } @@ -258,27 +345,90 @@ public class JbootAopFactory extends AopFactory { * 初始化 @Bean 注解的映射关系 */ private void initBeanMapping() { - List classes = ClassScanner.scanClassByAnnotation(Bean.class, true); - for (Class implClass : classes) { - Class[] interfaceClasses = implClass.getInterfaces(); + // 初始化 @Configuration 里的 beans + initConfigurationBeansObject(); + + // 添加映射 + initBeansMapping(); + } + - if (interfaceClasses == null || interfaceClasses.length == 0) { - continue; + /** + * 初始化 @Configuration 里的 bean 配置 + */ + private void initConfigurationBeansObject() { + List configurationClasses = ClassScanner.scanClassByAnnotation(Configuration.class, true); + for (Class configurationClass : configurationClasses) { + Object configurationObj = ClassUtil.newInstance(configurationClass, false); + if (configurationObj == null) { + throw new NullPointerException("Can not new instance for class: " + configurationClass.getName()); } + Method[] methods = configurationClass.getDeclaredMethods(); + for (Method method : methods) { + Bean beanAnnotation = method.getAnnotation(Bean.class); + if (beanAnnotation != null) { + Class returnType = method.getReturnType(); + if (returnType == void.class) { + throw new JbootException("@Bean annotation can not use for void method: " + ClassUtil.buildMethodString(method)); + } + + String beanName = StrUtil.obtainDefault(AnnotationUtil.get(beanAnnotation.name()), method.getName()); + if (beansCache.containsKey(beanName)) { + throw new JbootException("Application has contains beanName \"" + beanName + "\" for " + getBean(beanName) + + ", Can not add again by method: " + ClassUtil.buildMethodString(method)); + } + + try { + Object methodObj = method.invoke(configurationObj); + if (methodObj != null) { + beansCache.put(beanName, methodObj); + singletonCache.put(returnType, methodObj); + } + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + } + } + } - Class[] excludes = buildExcludeClasses(implClass); - for (Class interfaceClass : interfaceClasses) { - if (inExcludes(interfaceClass, excludes) == false) { - this.addMapping(interfaceClass, implClass); + /** + * 添加 所有的 Bean 和实现类 的映射 + */ + private void initBeansMapping() { + List classes = ClassScanner.scanClassByAnnotation(Bean.class, true); + for (Class implClass : classes) { + Bean bean = (Bean) implClass.getAnnotation(Bean.class); + String beanName = AnnotationUtil.get(bean.name()); + if (StrUtil.isNotBlank(beanName)) { + if (beanNameClassesMapping.containsKey(beanName)) { + throw new JbootException("application has contains beanName \"" + beanName + "\" for " + getBean(beanName) + + ", can not add for class " + implClass); + } + beanNameClassesMapping.put(beanName, implClass); + } else { + Class[] interfaceClasses = implClass.getInterfaces(); + + if (interfaceClasses.length == 0) { + //add self + this.addMapping(implClass, implClass); + } else { + Class[] excludes = buildExcludeClasses(implClass); + for (Class interfaceClass : interfaceClasses) { + if (!inExcludes(interfaceClass, excludes)) { + this.addMapping(interfaceClass, implClass); + } + } } } } } - private Class[] buildExcludeClasses(Class implClass) { - BeanExclude beanExclude = (BeanExclude) implClass.getAnnotation(BeanExclude.class); + + private Class[] buildExcludeClasses(Class implClass) { + BeanExclude beanExclude = implClass.getAnnotation(BeanExclude.class); //对某些系统的类 进行排除,例如:Serializable 等 return beanExclude == null @@ -286,12 +436,35 @@ public class JbootAopFactory extends AopFactory { : ArrayUtil.concat(DEFAULT_EXCLUDES_MAPPING_CLASSES, beanExclude.value()); } - private boolean inExcludes(Class interfaceClass, Class[] excludes) { - for (Class ex : excludes) { + + private boolean inExcludes(Class interfaceClass, Class[] excludes) { + for (Class ex : excludes) { if (ex.isAssignableFrom(interfaceClass)) { return true; } } return false; } + + + public T getBean(String name) { + T ret = (T) beansCache.get(name); + if (ret == null) { + if (beanNameClassesMapping.containsKey(name)) { + try { + ret = (T) doGet(beanNameClassesMapping.get(name)); + beansCache.put(name, ret); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + } + + return ret; + } + + public void setBean(String name, Object obj) { + beansCache.put(name, obj); + } + } diff --git a/src/main/java/io/jboot/aop/JbootAopInvocation.java b/src/main/java/io/jboot/aop/JbootAopInvocation.java deleted file mode 100644 index ff035747e3d32f6ad1cabfa8cc704d532fc40a93..0000000000000000000000000000000000000000 --- a/src/main/java/io/jboot/aop/JbootAopInvocation.java +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.aop; - -import com.jfinal.aop.Interceptor; -import com.jfinal.aop.Invocation; -import io.jboot.components.cache.interceptor.JbootCacheEvictInterceptor; -import io.jboot.components.cache.interceptor.JbootCacheInterceptor; -import io.jboot.components.cache.interceptor.JbootCachePutInterceptor; -import io.jboot.components.cache.interceptor.JbootCachesEvictInterceptor; -import io.jboot.components.limiter.LimiterInterceptor; -import io.jboot.exception.JbootException; -import io.jboot.support.metric.JbootMetricInterceptor; -import io.jboot.support.seata.interceptor.SeataGlobalTransactionalInterceptor; -import io.jboot.support.sentinel.SentinelInterceptor; - -import java.lang.reflect.Method; - - -public class JbootAopInvocation extends Invocation { - - private static final Interceptor[] ALL_INTERS = { - new SentinelInterceptor(), - new JbootMetricInterceptor(), - new JbootCacheEvictInterceptor(), - new JbootCachesEvictInterceptor(), - new JbootCachePutInterceptor(), - new JbootCacheInterceptor(), - new LimiterInterceptor(), - new SeataGlobalTransactionalInterceptor() - }; - - - private Interceptor[] inters = ALL_INTERS; - private Invocation originInvocation; - - private int index = 0; - - - public JbootAopInvocation(Invocation originInvocation) { - this.originInvocation = originInvocation; - } - - - @Override - public void invoke() { - if (index < inters.length) { - inters[index++].intercept(this); - } - // index++ ensure invoke action only one time - else if (index++ == inters.length) { - try { - originInvocation.invoke(); - } catch (Throwable throwable) { - if (throwable instanceof RuntimeException) { - throw (RuntimeException) throwable; - } else { - throw new JbootException(throwable.getMessage(), throwable); - } - } - } - } - - - @Override - public Method getMethod() { - return originInvocation.getMethod(); - } - - @Override - public String getMethodName() { - return getMethod().getName(); - } - - @Override - public T getTarget() { - return (T) originInvocation.getTarget(); - } - - @Override - public Object getArg(int index) { - return originInvocation.getArg(index); - } - - @Override - public void setArg(int index, Object value) { - originInvocation.setArg(index, value); - } - - @Override - public Object[] getArgs() { - return originInvocation.getArgs(); - } - - @Override - public T getReturnValue() { - return originInvocation.getReturnValue(); - } - - @Override - public void setReturnValue(Object returnValue) { - originInvocation.setReturnValue(returnValue); - } -} diff --git a/src/main/java/io/jboot/aop/JbootAopInterceptor.java b/src/main/java/io/jboot/aop/JbootLazyLoader.java similarity index 63% rename from src/main/java/io/jboot/aop/JbootAopInterceptor.java rename to src/main/java/io/jboot/aop/JbootLazyLoader.java index adfcc0da648fecb00ca7778c8aec71f1a1605da1..1227de042eb44573073a03bdd054d3b497d0a700 100644 --- a/src/main/java/io/jboot/aop/JbootAopInterceptor.java +++ b/src/main/java/io/jboot/aop/JbootLazyLoader.java @@ -1,5 +1,8 @@ +package io.jboot.aop; + +import java.lang.reflect.Field; /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.aop; - -import com.jfinal.aop.Interceptor; -import com.jfinal.aop.Invocation; - -public class JbootAopInterceptor implements Interceptor { - - @Override - public void intercept(Invocation inv) { - JbootAopInvocation invocation = new JbootAopInvocation(inv); - invocation.invoke(); - } +public interface JbootLazyLoader { + Object loadLazyObject(Object targetObject, Field field) throws ReflectiveOperationException; } diff --git a/src/main/java/io/jboot/aop/JbootLazyLoaderFactory.java b/src/main/java/io/jboot/aop/JbootLazyLoaderFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..f1db0cce7145b1fa77e5f82a84487697fa194617 --- /dev/null +++ b/src/main/java/io/jboot/aop/JbootLazyLoaderFactory.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop; + +import io.jboot.aop.cglib.JbootCglibLazyLoader; +import io.jboot.app.JbootApplicationConfig; +import net.sf.cglib.proxy.Enhancer; + +public class JbootLazyLoaderFactory { + + private static final JbootLazyLoaderFactory me = new JbootLazyLoaderFactory(); + + public static JbootLazyLoaderFactory me() { + return me; + } + + public JbootLazyLoader getLoader() { + if ("javassist".equalsIgnoreCase(JbootApplicationConfig.get().getProxy())) { + //javassist 直接返回 createFieldObjectNormal 的内容,而非 lazy Object + return (targetObject, field) -> JbootAopFactory.me().createFieldObjectNormal(targetObject, field); + } else { + return (targetObject, field) -> Enhancer.create(field.getType(), new JbootCglibLazyLoader(targetObject, field)); + } + } +} diff --git a/src/main/java/io/jboot/aop/ValueFilter.java b/src/main/java/io/jboot/aop/ValueFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..e8194016f6f577a91e94b306e363f68c41647ed6 --- /dev/null +++ b/src/main/java/io/jboot/aop/ValueFilter.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop; + +public interface ValueFilter { + Object doFilter(Object orignal); +} diff --git a/src/main/java/io/jboot/aop/ValueFilterInterceptor.java b/src/main/java/io/jboot/aop/ValueFilterInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..a5c44134c0b71604043ca9abebbf18c5a67f1637 --- /dev/null +++ b/src/main/java/io/jboot/aop/ValueFilterInterceptor.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop; + +import com.jfinal.aop.Aop; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.aop.annotation.FilterBy; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +@AutoLoad +public class ValueFilterInterceptor implements Interceptor, InterceptorBuilder { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + FilterBy filter = parameters[index].getAnnotation(FilterBy.class); + if (filter != null) { + Object original = inv.getArg(index); + + Class[] classes = filter.value(); + for (Class aClass : classes) { + ValueFilter vf = Aop.get(aClass); + original = vf.doFilter(original); + } + + inv.setArg(index, original); + } + } + + inv.invoke(); + } + + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + Parameter[] parameters = method.getParameters(); + if (parameters != null && parameters.length > 0) { + for (Parameter p : parameters) { + if (p.getAnnotation(FilterBy.class) != null) { + interceptors.addIfNotExist(this); + break; + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/aop/annotation/AutoLoad.java b/src/main/java/io/jboot/aop/annotation/AutoLoad.java new file mode 100644 index 0000000000000000000000000000000000000000..ae298c35062892ed706d686055b202af9347d934 --- /dev/null +++ b/src/main/java/io/jboot/aop/annotation/AutoLoad.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface AutoLoad { +} \ No newline at end of file diff --git a/src/main/java/io/jboot/aop/annotation/Bean.java b/src/main/java/io/jboot/aop/annotation/Bean.java index a43ed14171ebddf364fc8e40a65ffed08156f7c4..c9bde5bda2ccf3af98be502198395b5f90a9263c 100644 --- a/src/main/java/io/jboot/aop/annotation/Bean.java +++ b/src/main/java/io/jboot/aop/annotation/Bean.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.lang.annotation.*; @Inherited @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) public @interface Bean { String name() default ""; } \ No newline at end of file diff --git a/src/main/java/io/jboot/aop/annotation/BeanExclude.java b/src/main/java/io/jboot/aop/annotation/BeanExclude.java index 6c889a7c373cb2aeb1b55d1357823e1339b5c569..bb028e3b37d7e98a51e8ba3be98bdb183d62d9e2 100644 --- a/src/main/java/io/jboot/aop/annotation/BeanExclude.java +++ b/src/main/java/io/jboot/aop/annotation/BeanExclude.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,5 +21,5 @@ import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface BeanExclude { - Class[] value(); + Class[] value(); } \ No newline at end of file diff --git a/src/main/java/io/jboot/aop/annotation/ConfigValue.java b/src/main/java/io/jboot/aop/annotation/ConfigValue.java index 17c6f7f604c29dcab3ecfd725e8686e46757a016..45ba1f003cd94073d8b11fb31ad9894b43f34e1d 100644 --- a/src/main/java/io/jboot/aop/annotation/ConfigValue.java +++ b/src/main/java/io/jboot/aop/annotation/ConfigValue.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,4 @@ public @interface ConfigValue { String value(); - /** - * 当读取配置内容为空的时候,是否进行赋值 - * @return 默认为 true - */ - boolean requireNullOrBlank() default false; - } diff --git a/src/main/java/io/jboot/aop/annotation/Configuration.java b/src/main/java/io/jboot/aop/annotation/Configuration.java new file mode 100644 index 0000000000000000000000000000000000000000..bf96657d0f5392c619d0645cb853cc0dc36c395f --- /dev/null +++ b/src/main/java/io/jboot/aop/annotation/Configuration.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Configuration { + String name() default ""; +} \ No newline at end of file diff --git a/src/main/java/io/jboot/aop/annotation/DefaultValue.java b/src/main/java/io/jboot/aop/annotation/DefaultValue.java new file mode 100644 index 0000000000000000000000000000000000000000..209498a1d8d349c2744cbfff210ab53344a7be34 --- /dev/null +++ b/src/main/java/io/jboot/aop/annotation/DefaultValue.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.annotation; + +import java.lang.annotation.*; + + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +public @interface DefaultValue { + String value(); +} diff --git a/src/main/java/io/jboot/aop/annotation/FilterBy.java b/src/main/java/io/jboot/aop/annotation/FilterBy.java new file mode 100644 index 0000000000000000000000000000000000000000..eb08fbce457c70a25277e835b3c090d0cbb926f1 --- /dev/null +++ b/src/main/java/io/jboot/aop/annotation/FilterBy.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.annotation; + +import io.jboot.aop.ValueFilter; + +import java.lang.annotation.*; + + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +public @interface FilterBy { + Class[] value(); +} diff --git a/src/main/java/io/jboot/aop/annotation/Lazy.java b/src/main/java/io/jboot/aop/annotation/Lazy.java new file mode 100644 index 0000000000000000000000000000000000000000..6eb59598254ead352349b3baa6797e2596cc6d14 --- /dev/null +++ b/src/main/java/io/jboot/aop/annotation/Lazy.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface Lazy { + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/aop/annotation/StaticConstruct.java b/src/main/java/io/jboot/aop/annotation/StaticConstruct.java index b62d1b381c1485b1e4c8e7c20c3b956f74b78983..800d05ee70565f5526134f1becd29b9309ac4cdc 100644 --- a/src/main/java/io/jboot/aop/annotation/StaticConstruct.java +++ b/src/main/java/io/jboot/aop/annotation/StaticConstruct.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/aop/annotation/Transactional.java b/src/main/java/io/jboot/aop/annotation/Transactional.java new file mode 100644 index 0000000000000000000000000000000000000000..1dcade0ec9a6dbb044ae23c825b57ec6d07c648d --- /dev/null +++ b/src/main/java/io/jboot/aop/annotation/Transactional.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.annotation; + +import java.lang.annotation.*; + +/** + * @author michael yang + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface Transactional { + + /** + * 使用哪个数据源 + * + * @return + */ + String config() default ""; + + /** + * 事务隔离级别 + * + * @return + */ + int transactionLevel() default -1; + + /** + * return false 的时候,是否进行回滚 + * + * @return + */ + boolean rollbackForFalse() default false; + + /** + * return ret.fail 的时候,是否进行回滚 + * + * @return + */ + boolean rollbackForRetFail() default false; + + + /** + * 返回 null 的时候,是否进行回滚 + * + * @return + */ + boolean rollbackForNull() default false; + + + /** + * 配置允许哪些异常不回滚 + * + * @return + */ + Class[] noRollbackFor() default {}; + + /** + * 是否在新的线程里执行,在 Controller 的 Action 方法下配置无效 + * 在 Controller 里,不能以新的线程在运行 + * + * @return + */ + boolean inNewThread() default false; + + /** + * 使用哪个线程池来运行线程,需要在启动的时候,通过 TransactionalManager 来配置线程池及其名称 + * + * @return + */ + String threadPoolName() default ""; + + /** + * 是否以阻塞的方式运行线程,这个配置只有在返回值 void 情况下配置生效 + * 有返回值的,此配置无效,默认都是阻塞运行线程的方式运行 + * + * @return + */ + boolean threadWithBlocked() default false; + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/aop/cglib/JbootCglibCallback.java b/src/main/java/io/jboot/aop/cglib/JbootCglibCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..445304361d8fd3cf039da4d2f91cec63545247e9 --- /dev/null +++ b/src/main/java/io/jboot/aop/cglib/JbootCglibCallback.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.cglib; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.InterceptorManager; +import com.jfinal.aop.Invocation; +import io.jboot.aop.InterceptorBuilderManager; +import io.jboot.aop.InterceptorCache; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + + +public class JbootCglibCallback implements MethodInterceptor { + + private static final Set excludedMethodName = buildExcludedMethodName(); + private static final InterceptorManager interManager = InterceptorManager.me(); + private static final InterceptorBuilderManager builderManager = InterceptorBuilderManager.me(); + + @Override + public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + if (excludedMethodName.contains(method.getName())) { + return methodProxy.invokeSuper(target, args); + } + + Class targetClass = target.getClass().getSuperclass(); + //ClassUtil.getUsefulClass(target.getClass()); + + InterceptorCache.MethodKey key = InterceptorCache.getMethodKey(targetClass, method); + Interceptor[] inters = InterceptorCache.get(key); + if (inters == null) { + inters = interManager.buildServiceMethodInterceptor(targetClass, method); + inters = builderManager.build(targetClass, method, inters); + + InterceptorCache.put(key, inters); + } + + if (inters.length == 0) { + return methodProxy.invokeSuper(target, args); + } else { + Invocation invocation = new Invocation(target, method, inters, + x -> methodProxy.invokeSuper(target, x), args); + invocation.invoke(); + return invocation.getReturnValue(); + } + } + + + private static Set buildExcludedMethodName() { + Set excludedMethodName = new HashSet(64, 0.25F); + Method[] methods = Object.class.getDeclaredMethods(); + for (Method m : methods) { + excludedMethodName.add(m.getName()); + } + // getClass() registerNatives() can not be enhanced + // excludedMethodName.remove("getClass"); + // excludedMethodName.remove("registerNatives"); + return excludedMethodName; + } + +} + + diff --git a/src/main/java/io/jboot/aop/cglib/JbootCglibLazyLoader.java b/src/main/java/io/jboot/aop/cglib/JbootCglibLazyLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..55a4a1bd2923780001c5e3f7c011a2813de34df6 --- /dev/null +++ b/src/main/java/io/jboot/aop/cglib/JbootCglibLazyLoader.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.cglib; + +import io.jboot.aop.JbootAopFactory; +import net.sf.cglib.proxy.LazyLoader; + +import java.lang.reflect.Field; + +public class JbootCglibLazyLoader implements LazyLoader { + + private static JbootAopFactory factory = JbootAopFactory.me(); + + private Object targetObject; + private Field field; + + public JbootCglibLazyLoader(Object targetObject, Field field) { + this.targetObject = targetObject; + this.field = field; + } + + @Override + public Object loadObject() throws Exception { + return factory.createFieldObjectNormal(targetObject, field); + } + + +} diff --git a/src/main/java/io/jboot/aop/cglib/JbootCglibProxyFactory.java b/src/main/java/io/jboot/aop/cglib/JbootCglibProxyFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..eb18aba0a6bfd4a90643199895ed668eed19275f --- /dev/null +++ b/src/main/java/io/jboot/aop/cglib/JbootCglibProxyFactory.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.cglib; + +import com.jfinal.proxy.ProxyFactory; +import net.sf.cglib.proxy.MethodInterceptor; + +import java.util.function.Function; + +/** + * JbootCglibProxyFactory 用于扩展 cglib 的代理模式 + * + *

+ * 配置方法:
+ * public void configConstant(Constants me) {
+ *     ProxyManager.me().setProxyFactory(new JbootCglibProxyFactory());
+ * }
+ * 
+ */ +public class JbootCglibProxyFactory extends ProxyFactory { + + /** + * 方便在单元测试的时候,可以对任意 class 进行 Mock + */ + private static Function, MethodInterceptor> methodInterceptor = aClass -> new JbootCglibCallback(); + + public static Function, MethodInterceptor> getMethodInterceptor() { + return methodInterceptor; + } + + public static void setMethodInterceptor(Function, MethodInterceptor> methodInterceptor) { + JbootCglibProxyFactory.methodInterceptor = methodInterceptor; + } + + @Override + public T get(Class target) { + try { + return (T) net.sf.cglib.proxy.Enhancer.create(target, methodInterceptor.apply(target)); + } catch (Throwable e) { + //原始的错误,无法给出哪个类无法被创建,错误信息不够友好 + throw new RuntimeException("Can not create object for class:\"" + target.getName() + "\", cause:" + e.getMessage(), e); + } + } + + +} + + + diff --git a/src/main/java/io/jboot/aop/javassist/JbootJavassistHandler.java b/src/main/java/io/jboot/aop/javassist/JbootJavassistHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..4bd7d53fb1701ab40976852ca21eda44eebb7884 --- /dev/null +++ b/src/main/java/io/jboot/aop/javassist/JbootJavassistHandler.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.javassist; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.InterceptorManager; +import com.jfinal.aop.Invocation; +import io.jboot.aop.InterceptorBuilderManager; +import io.jboot.aop.InterceptorCache; +import javassist.util.proxy.MethodHandler; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + + +public class JbootJavassistHandler implements MethodHandler { + + private static final Set excludedMethodName = buildExcludedMethodName(); + private static final InterceptorManager interManager = InterceptorManager.me(); + private static final InterceptorBuilderManager builderManager = InterceptorBuilderManager.me(); + + + @Override + public Object invoke(Object self, Method originalMethod, Method proxyMethod, Object[] args) throws Throwable { + + if (excludedMethodName.contains(originalMethod.getName())) { + return proxyMethod.invoke(self, args); + } + + Class targetClass = self.getClass().getSuperclass(); + //ClassUtil.getUsefulClass(self.getClass()); + + InterceptorCache.MethodKey key = InterceptorCache.getMethodKey(targetClass, originalMethod); + Interceptor[] inters = InterceptorCache.get(key); + if (inters == null) { + inters = interManager.buildServiceMethodInterceptor(targetClass, originalMethod); + inters = builderManager.build(targetClass, originalMethod, inters); + + InterceptorCache.put(key, inters); + } + + if (inters.length == 0) { + return proxyMethod.invoke(self, args); + } else { + Invocation invocation = new Invocation(self, originalMethod, inters, + x -> proxyMethod.invoke(self, x), args); + invocation.invoke(); + return invocation.getReturnValue(); + } + } + + + private static Set buildExcludedMethodName() { + Set excludedMethodName = new HashSet(64, 0.25F); + Method[] methods = Object.class.getDeclaredMethods(); + for (Method m : methods) { + excludedMethodName.add(m.getName()); + } + // getClass() registerNatives() can not be enhanced + // excludedMethodName.remove("getClass"); + // excludedMethodName.remove("registerNatives"); + return excludedMethodName; + } + + +} + + diff --git a/src/main/java/io/jboot/aop/javassist/JbootJavassistProxyFactory.java b/src/main/java/io/jboot/aop/javassist/JbootJavassistProxyFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..f9c69595180bbb1a6e31f7f26f2c15725f1ab2f2 --- /dev/null +++ b/src/main/java/io/jboot/aop/javassist/JbootJavassistProxyFactory.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.aop.javassist; + +import com.jfinal.kit.LogKit; +import com.jfinal.proxy.ProxyFactory; +import javassist.util.proxy.MethodHandler; +import javassist.util.proxy.ProxyObject; + +import java.util.function.Function; + +/** + * JbootCglibProxyFactory 用于扩展 cglib 的代理模式 + * + *

+ * 配置方法:
+ * public void configConstant(Constants me) {
+ *     ProxyManager.me().setProxyFactory(new JbootJavassistProxyFactory());
+ * }
+ * 
+ */ +public class JbootJavassistProxyFactory extends ProxyFactory { + + /** + * 方便在单元测试的时候,可以对任意 class 进行 Mock + */ + private static Function, MethodHandler> methodInterceptor = aClass -> new JbootJavassistHandler(); + + public static Function, MethodHandler> getMethodInterceptor() { + return methodInterceptor; + } + + public static void setMethodInterceptor(Function, MethodHandler> methodInterceptor) { + JbootJavassistProxyFactory.methodInterceptor = methodInterceptor; + } + + @Override + public T get(Class target) { + javassist.util.proxy.ProxyFactory factory = new javassist.util.proxy.ProxyFactory(); + factory.setSuperclass(target); + final Class proxyClass = factory.createClass(); + + + T proxyObject = null; + try { + proxyObject = (T) proxyClass.newInstance(); + ((ProxyObject) proxyObject).setHandler(methodInterceptor.apply(target)); + } catch (Throwable e) { + LogKit.error(e.toString(), e); + } + + return proxyObject; + } + + +} + + + diff --git a/src/main/java/io/jboot/aop/jfinal/JfinalHandlers.java b/src/main/java/io/jboot/aop/jfinal/JfinalHandlers.java index 1ebde97473a94cfe7220f5081e014e13b22c0232..5b8bc27362d31064415be9376882bb3ba9518203 100644 --- a/src/main/java/io/jboot/aop/jfinal/JfinalHandlers.java +++ b/src/main/java/io/jboot/aop/jfinal/JfinalHandlers.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/aop/jfinal/JfinalPlugins.java b/src/main/java/io/jboot/aop/jfinal/JfinalPlugins.java index de090b299f8d2eb1df37904e1f242c058df21e28..8e55bd19dccfefb414cc1a5d680a4567f4a3da8e 100644 --- a/src/main/java/io/jboot/aop/jfinal/JfinalPlugins.java +++ b/src/main/java/io/jboot/aop/jfinal/JfinalPlugins.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,10 @@ import com.jfinal.aop.Aop; import com.jfinal.config.Plugins; import com.jfinal.plugin.IPlugin; +import java.util.List; + /** - * Jfinal Plugins 的代理类,方便为Plugin插件的自动注入功能 + * Jfinal Plugins 的代理类,方便为 Plugin 插件的自动注入功能 */ public class JfinalPlugins { @@ -39,4 +41,8 @@ public class JfinalPlugins { public Plugins getPlugins() { return plugins; } + + public List getPluginList() { + return plugins.getPluginList(); + } } diff --git a/src/main/java/io/jboot/apidoc/ApiDocConfig.java b/src/main/java/io/jboot/apidoc/ApiDocConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..eeebf167885f2b92e66610d7797eb6532509d5c1 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiDocConfig.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import com.jfinal.kit.PathKit; +import com.jfinal.kit.Ret; +import io.jboot.utils.FileUtil; + +import java.io.File; + +public class ApiDocConfig { + + private String basePath = "apidoc"; + private String packagePrefix; + + + private boolean allInOneEnable = false; + private String allInOneTitle = "Api Document"; + private String allInOneNotes; + private String allInOneFilePath = "apidoc"; + + private String mockJsonPath = "api-mock.json"; + private String remarksJsonPath = "api-remarks.json"; + + private Class defaultContainerClass = Ret.class; + + + public String getBasePath() { + return basePath; + } + + public String getBasePathAbsolute() { + if (FileUtil.isAbsolutePath(basePath)) { + return basePath; + } + return FileUtil.getCanonicalPath(new File(PathKit.getRootClassPath(), "../../" + basePath)); + } + + public void setBasePath(String basePath) { + this.basePath = basePath; + } + + public String getPackagePrefix() { + return packagePrefix; + } + + public void setPackagePrefix(String packagePrefix) { + this.packagePrefix = packagePrefix; + } + + + public boolean isAllInOneEnable() { + return allInOneEnable; + } + + public void setAllInOneEnable(boolean allInOneEnable) { + this.allInOneEnable = allInOneEnable; + } + + public String getAllInOneTitle() { + return allInOneTitle; + } + + public void setAllInOneTitle(String allInOneTitle) { + this.allInOneTitle = allInOneTitle; + } + + public String getAllInOneNotes() { + return allInOneNotes; + } + + public void setAllInOneNotes(String allInOneNotes) { + this.allInOneNotes = allInOneNotes; + } + + public String getAllInOneFilePath() { + return allInOneFilePath; + } + + public void setAllInOneFilePath(String allInOneFilePath) { + this.allInOneFilePath = allInOneFilePath; + } + + public String getMockJsonPath() { + return mockJsonPath; + } + + public void setMockJsonPath(String mockJsonPath) { + this.mockJsonPath = mockJsonPath; + } + + public String getMockJsonPathAbsolute() { + if (FileUtil.isAbsolutePath(mockJsonPath)) { + return mockJsonPath; + } + return new File(PathKit.getRootClassPath(), mockJsonPath).getAbsolutePath(); + } + + public String getRemarksJsonPath() { + return remarksJsonPath; + } + + public void setRemarksJsonPath(String remarksJsonPath) { + this.remarksJsonPath = remarksJsonPath; + } + + public String getRemarksJsonPathAbsolute() { + if (FileUtil.isAbsolutePath(remarksJsonPath)) { + return remarksJsonPath; + } + return new File(PathKit.getRootClassPath(), remarksJsonPath).getAbsolutePath(); + } + + public Class getDefaultContainerClass() { + return defaultContainerClass; + } + + public void setDefaultContainerClass(Class defaultContainerClass) { + this.defaultContainerClass = defaultContainerClass; + } +} diff --git a/src/main/java/io/jboot/apidoc/ApiDocManager.java b/src/main/java/io/jboot/apidoc/ApiDocManager.java new file mode 100644 index 0000000000000000000000000000000000000000..44d8b9b76a90770bbbe244bb5ab02e15b59e59b1 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiDocManager.java @@ -0,0 +1,536 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.parser.Feature; +import com.jfinal.core.Controller; +import com.jfinal.kit.JsonKit; +import com.jfinal.kit.Ret; +import com.jfinal.kit.StrKit; +import com.jfinal.plugin.activerecord.Page; +import io.jboot.apidoc.annotation.Api; +import io.jboot.apidoc.annotation.ApiOper; +import io.jboot.apidoc.annotation.ApiResp; +import io.jboot.apidoc.annotation.ApiResps; +import io.jboot.utils.*; + +import java.io.File; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +public class ApiDocManager { + + private static final ApiDocManager me = new ApiDocManager(); + + public static ApiDocManager me() { + return me; + } + + //渲染器 + private ApiDocRender render = ApiDocRender.MARKDOWN_RENDER; + + //每个类对于的属性名称,一般支持从数据库读取 字段配置 填充,来源于 api-remarks.json + private Map> modelFieldRemarks = new HashMap<>(); + private Map> defaultModelFieldRemarks = new HashMap<>(); + + //ClassType Mocks,来源于 api-mock.json + private Map classTypeMockDatas = new HashMap<>(); + private Map, ApiMockBuilder> classTypeMockBuilders = new HashMap<>(); + + //ApiOperation 排序方式 + private Comparator operationComparator; + + private ApiDocManager() { + initDefaultClassTypeMockBuilder(); + } + + private void initDefaultClassTypeMockBuilder() { + addClassTypeMockBuilders(Ret.class, ApiMockBuilders.retBuilder); + addClassTypeMockBuilders(Map.class, ApiMockBuilders.mapBuilder); + addClassTypeMockBuilders(List.class, ApiMockBuilders.listBuilder); + addClassTypeMockBuilders(Page.class, ApiMockBuilders.pageBuilder); + addClassTypeMockBuilders(String.class, ApiMockBuilders.stringBuilder); + } + + public ApiDocRender getRender() { + return render; + } + + public void setRender(ApiDocRender render) { + this.render = render; + } + + public Map> getModelFieldRemarks() { + return modelFieldRemarks; + } + + public void setModelFieldRemarks(Map> modelFieldRemarks) { + this.modelFieldRemarks = modelFieldRemarks; + } + + public void addModelFieldRemarks(String classOrSimpleName, Map fieldNames) { + this.modelFieldRemarks.put(classOrSimpleName, fieldNames); + } + + public Map getClassTypeMockDatas() { + return classTypeMockDatas; + } + + public void setClassTypeMockDatas(Map classTypeMockDatas) { + this.classTypeMockDatas = classTypeMockDatas; + } + + public void addClassTypeMocks(String classType, Object mockData) { + this.classTypeMockDatas.put(classType, mockData); + } + + public Object getClassTypeMockData(String classType) { + return this.classTypeMockDatas.get(classType); + } + + public Comparator getOperationComparator() { + return operationComparator; + } + + public void setOperationComparator(Comparator operationComparator) { + this.operationComparator = operationComparator; + } + + public Map, ApiMockBuilder> getClassTypeMockBuilders() { + return classTypeMockBuilders; + } + + public void setClassTypeMockBuilders(Map, ApiMockBuilder> classTypeMockBuilders) { + this.classTypeMockBuilders = classTypeMockBuilders; + } + + public void addClassTypeMockBuilders(Class forClass, ApiMockBuilder builder) { + this.classTypeMockBuilders.put(forClass, builder); + } + + + String buildMockJson(ClassType classType, Method method) { + return ApiDocUtil.prettyJson(JsonKit.toJson(doBuildMockObject(classType, method, 0))); + } + + + Object doBuildMockObject(ClassType classType, Method method, int level) { + Object retObject = getClassTypeMockData(classType.toString()); + + if (retObject == null) { + retObject = getClassTypeMockData(classType.getMainClass().getName()); + } + + if (retObject == null) { + retObject = getClassTypeMockData(StrKit.firstCharToLowerCase(classType.getMainClass().getSimpleName())); + } + + if (retObject != null) { + return retObject; + } + + for (Class aClass : classTypeMockBuilders.keySet()) { + if (aClass.isAssignableFrom(classType.getMainClass())) { + Object object = classTypeMockBuilders.get(aClass).build(classType, method, level); + if (object != null) { + return object; + } + } + } + return null; + } + + + Map> buildRemarks(ClassType classType, Method method) { + Map> retMap = new LinkedHashMap<>(); + doBuildRemarks(retMap, classType, method, 0); + return retMap; + } + + + private void doBuildRemarks(Map> retMap, ClassType classType, Method method, int level) { + Class mainClass = classType.getMainClass(); + + Set apiResponses = new LinkedHashSet<>(); + + //根据默认的配置构建 + doBuildRemarksByDefault(apiResponses, classType, method); + + List> dataTypeClasses = null; + + //根据方法的 @Resp 来构建 + if (level == 0) { + dataTypeClasses = doBuildRemarksByMethodAnnotation(apiResponses, method); + } + + //根据配置文件来构建 + doBuildRemarksByConfig(apiResponses, classType, method); + + if (!apiResponses.isEmpty()) { + retMap.put(mainClass.getSimpleName(), new LinkedList<>(apiResponses)); + } + + //必须执行在 retMap.put 之后,才能保证 remarks 表格在文档中的顺序 + if (dataTypeClasses != null) { + for (Class dataType : dataTypeClasses) { + doBuildRemarks(retMap, new ClassType(dataType), method, level + 1); + } + } + + + ClassType[] types = classType.getGenericTypes(); + if (types != null) { + for (ClassType type : types) { + doBuildRemarks(retMap, type, method, level + 1); + } + } + } + + + private void doBuildRemarksByDefault(Set apiResponses, ClassType classType, Method method1) { + Map defaultModelRemarks = defaultModelFieldRemarks.get(classType.getMainClass().getName()); + if (defaultModelRemarks == null || defaultModelRemarks.isEmpty()) { + return; + } + + List getterMethods = ReflectUtil.searchMethodList(classType.getMainClass(), m -> m.getParameterCount() == 0 + && m.getReturnType() != void.class + && Modifier.isPublic(m.getModifiers()) + && (m.getName().startsWith("get") || m.getName().startsWith("is")) + && !"getClass".equals(m.getName()) + ); + + Map filedAndMethodMap = new HashMap<>(); + for (Method getterMethod : getterMethods) { + filedAndMethodMap.put(ApiDocUtil.getterMethod2Field(getterMethod), getterMethod); + } + + for (String key : defaultModelRemarks.keySet()) { + + ApiResponse apiResponse = new ApiResponse(); + apiResponse.setField(key); + apiResponse.setRemarks(defaultModelRemarks.get(key)); + + Method getterMethod = filedAndMethodMap.get(key); + if (getterMethod != null) { + apiResponse.setDataAndClassType(getterMethod.getReturnType()); + } + //若没有 getter 方法,一般情况下 map 或者 ret 等 + //此时,需要通过 Mock 数据来对 key 的 dataType 进行推断 + else { + Object object = doBuildMockObject(classType, method1, 0); + if (object instanceof Map) { + Object value = ((Map) object).get(key); + if (value != null) { + apiResponse.setDataAndClassType(value.getClass()); + } + } + } + + apiResponses.add(apiResponse); + } + + } + + + private List> doBuildRemarksByMethodAnnotation(Set apiResponses, Method method) { + + List apiResponses1 = new LinkedList<>(); + List> dataTypes = new ArrayList<>(); + + ApiResps apiResps = method.getAnnotation(ApiResps.class); + if (apiResps != null) { + for (ApiResp apiResp : apiResps.value()) { + apiResponses1.add(new ApiResponse(apiResp)); + dataTypes.add(apiResp.dataType()); + } + } + + ApiResp apiResp = method.getAnnotation(ApiResp.class); + if (apiResp != null) { + apiResponses1.add(new ApiResponse(apiResp)); + dataTypes.add(apiResp.dataType()); + } + + apiResponses.addAll(apiResponses1); + return dataTypes; + } + + + private void doBuildRemarksByConfig(Set apiResponses, ClassType classType, Method method1) { + Map configRemarks = getConfigRemarks(classType.getMainClass()); + if (configRemarks == null || configRemarks.isEmpty()) { + return; + } + + List getterMethods = ReflectUtil.searchMethodList(classType.getMainClass(), m -> m.getParameterCount() == 0 + && m.getReturnType() != void.class + && Modifier.isPublic(m.getModifiers()) + && (m.getName().startsWith("get") || m.getName().startsWith("is")) + && !"getClass".equals(m.getName()) + ); + + Map filedAndMethodMap = new HashMap<>(); + for (Method getterMethod : getterMethods) { + filedAndMethodMap.put(ApiDocUtil.getterMethod2Field(getterMethod), getterMethod); + } + + + for (String key : configRemarks.keySet()) { + + ApiResponse apiResponse = new ApiResponse(); + apiResponse.setField(key); + apiResponse.setRemarks(configRemarks.get(key)); + + Method getterMethod = filedAndMethodMap.get(key); + if (getterMethod != null) { + apiResponse.setDataAndClassType(getterMethod.getReturnType()); + } + //若没有 getter 方法,一般情况下 map 或者 ret 等 + //此时,需要通过 Mock 数据来对 key 的 dataType 进行推断 + else { + Object object = doBuildMockObject(classType, method1, 0); + if (object instanceof Map) { + Object value = ((Map) object).get(key); + if (value != null) { + apiResponse.setDataAndClassType(value.getClass()); + } + } + } + + apiResponses.add(apiResponse); + } + } + + private Map getConfigRemarks(Class clazz) { + Map ret = modelFieldRemarks.get(clazz.getName()); + return ret != null ? ret : modelFieldRemarks.get(StrKit.firstCharToLowerCase(clazz.getSimpleName())); + } + + + /** + * 生成 API 文档 + * + * @param config + */ + public void genDocs(ApiDocConfig config) { + List controllerClasses = ClassScanner.scanClass(aClass -> Controller.class.isAssignableFrom(aClass) && aClass.getAnnotation(Api.class) != null); + if (controllerClasses.isEmpty()) { + return; + } + + initMockJson(config); + initModelRemarks(config); + + List apiDocuments = new ArrayList<>(); + + //所有 Controller 构建为同一个文档 + if (config.isAllInOneEnable()) { + ApiDocument document = new ApiDocument(); + document.setValue(config.getAllInOneTitle()); + document.setNotes(config.getAllInOneNotes()); + document.setFilePath(config.getAllInOneFilePath()); + + buildAllInOneDocument(document, controllerClasses, config); + + apiDocuments.add(document); + } + //不同的 Controller 构建为不同的文档 + else { + for (Class controllerClass : controllerClasses) { + if (StrUtil.isNotBlank(config.getPackagePrefix())) { + if (controllerClass.getName().startsWith(config.getPackagePrefix())) { + apiDocuments.add(buildDocument(controllerClass, config)); + } + } else { + apiDocuments.add(buildDocument(controllerClass, config)); + } + } + } + + if (render != null) { + render.render(apiDocuments, config); + } + } + + + private void initMockJson(ApiDocConfig config) { + File mockJsonFile = new File(config.getMockJsonPathAbsolute()); + if (mockJsonFile.exists()) { + String mockJsonString = FileUtil.readString(mockJsonFile); + JSONObject mockJsonObject = JSONObject.parseObject(mockJsonString, Feature.OrderedField); + for (String classTypeKey : mockJsonObject.keySet()) { + this.classTypeMockDatas.put(classTypeKey, mockJsonObject.get(classTypeKey)); + } + } + } + + + private void initModelRemarks(ApiDocConfig config) { + + Map pageRemarks = new LinkedHashMap<>(); + pageRemarks.put("totalRow", "总行数"); + pageRemarks.put("pageNumber", "当前页码"); + pageRemarks.put("firstPage", "是否是第一页"); + pageRemarks.put("lastPage", "是否是最后一页"); + pageRemarks.put("totalPage", "总页数"); + pageRemarks.put("pageSize", "每页数据量"); + pageRemarks.put("list", "数据列表"); + defaultModelFieldRemarks.put(Page.class.getName(), pageRemarks); + + + Map retRemarks = new LinkedHashMap<>(); + retRemarks.put("state", "状态,成功 ok,失败 fail"); + defaultModelFieldRemarks.put(Ret.class.getName(), retRemarks); + + Map apiRetRemarks = new LinkedHashMap<>(); + apiRetRemarks.put("state", "状态,成功 ok,失败 fail"); + apiRetRemarks.put("errorCode", "错误码,可能返回 null"); + apiRetRemarks.put("message", "错误消息"); + apiRetRemarks.put("data", "数据"); + defaultModelFieldRemarks.put(ApiRet.class.getName(), apiRetRemarks); + + + File modelJsonFile = new File(config.getRemarksJsonPathAbsolute()); + if (modelJsonFile.exists()) { + String modelJsonString = FileUtil.readString(modelJsonFile); + JSONObject modelJsonObject = JSONObject.parseObject(modelJsonString, Feature.OrderedField); + for (String classOrSimpleName : modelJsonObject.keySet()) { + Map remarks = new LinkedHashMap<>(); + JSONObject modelRemarks = modelJsonObject.getJSONObject(classOrSimpleName); + modelRemarks.forEach((k, v) -> remarks.put(k, String.valueOf(v))); + addModelFieldRemarks(classOrSimpleName, remarks); + } + } + } + + + private void buildAllInOneDocument(ApiDocument document, List controllerClasses, ApiDocConfig config) { + for (Class controllerClass : controllerClasses) { + buildOperation(document, controllerClass, config); + } + + List operations = document.getApiOperations(); + if (operations != null) { + if (operationComparator != null) { + operations.sort(operationComparator); + } else { + operations.sort(Comparator.comparing(ApiOperation::getActionKey)); + } + } + } + + + private ApiDocument buildDocument(Class controllerClass, ApiDocConfig config) { + + Api api = controllerClass.getAnnotation(Api.class); + ApiDocument document = new ApiDocument(); + document.setControllerClass(controllerClass); + document.setValue(api.value()); + document.setNotes(api.notes()); + document.setFilePathByControllerPath(ApiDocUtil.getControllerPath(controllerClass)); + + + String filePath = api.filePath(); + if (StrUtil.isNotBlank(filePath)) { + document.setFilePath(filePath); + } + + buildOperation(document, controllerClass, config); + + List operations = document.getApiOperations(); + if (operations != null) { + if (operationComparator != null) { + operations.sort(operationComparator); + } else { + operations.sort(new Comparator() { + @Override + public int compare(ApiOperation o1, ApiOperation o2) { + return o1.getOrderNo() == o2.getOrderNo() ? o1.getMethod().getName().compareTo(o2.getMethod().getName()) : o1.getOrderNo() - o2.getOrderNo(); + } + }); + } + } + + return document; + } + + + private void buildOperation(ApiDocument document, Class controllerClass, ApiDocConfig config) { + + List methods = ReflectUtil.searchMethodList(controllerClass, + method -> method.getAnnotation(ApiOper.class) != null && Modifier.isPublic(method.getModifiers())); + + String controllerPath = ApiDocUtil.getControllerPath(controllerClass); + HttpMethod defaultHttpMethod = ApiDocUtil.getControllerMethod(controllerClass); + + for (Method method : methods) { + ApiOper apiOper = method.getAnnotation(ApiOper.class); + + ApiOperation apiOperation = new ApiOperation(); + apiOperation.setControllerClass(controllerClass); + + Class containerClass = apiOper.containerClass() != void.class ? apiOper.containerClass() : config.getDefaultContainerClass(); + apiOperation.setMethodAndInfo(method, controllerPath, ApiDocUtil.getMethodHttpMethods(method, defaultHttpMethod), containerClass); + + + apiOperation.setValue(apiOper.value()); + apiOperation.setNotes(apiOper.notes()); + apiOperation.setParaNotes(apiOper.paraNotes()); + apiOperation.setOrderNo(apiOper.orderNo()); + apiOperation.setContentType(apiOper.contentType()); + + document.addOperation(apiOperation); + } + + + Api api = controllerClass.getAnnotation(Api.class); + if (api != null) { + Class[] collectClasses = api.collect(); + for (Class collectClass : collectClasses) { + + List collectMethods = ReflectUtil.searchMethodList(collectClass, + method -> method.getAnnotation(ApiOper.class) != null && Modifier.isPublic(method.getModifiers())); + + String collectControllerPath = ApiDocUtil.getControllerPath(collectClass); + HttpMethod collectDefaultHttpMethod = ApiDocUtil.getControllerMethod(controllerClass); + + for (Method method : collectMethods) { + ApiOper apiOper = method.getAnnotation(ApiOper.class); + + ApiOperation apiOperation = new ApiOperation(); + apiOperation.setControllerClass(collectClass); + + Class containerClass = apiOper.containerClass() != void.class ? apiOper.containerClass() : config.getDefaultContainerClass(); + apiOperation.setMethodAndInfo(method, collectControllerPath, ApiDocUtil.getMethodHttpMethods(method, collectDefaultHttpMethod), containerClass); + + + apiOperation.setValue(apiOper.value()); + apiOperation.setNotes(apiOper.notes()); + apiOperation.setParaNotes(apiOper.paraNotes()); + apiOperation.setOrderNo(apiOper.orderNo()); + apiOperation.setContentType(apiOper.contentType()); + + document.addOperation(apiOperation); + } + } + } + } + + +} diff --git a/src/main/java/io/jboot/apidoc/ApiDocRender.java b/src/main/java/io/jboot/apidoc/ApiDocRender.java new file mode 100644 index 0000000000000000000000000000000000000000..1d171a3de205bc94ba853a7eff34ff7f5d2eac32 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiDocRender.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import com.jfinal.template.Engine; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public interface ApiDocRender { + ApiDocRender MARKDOWN_RENDER = new ApiDocRender() { + private Engine engine = new Engine("apidoc"); + private String template = "#(\"#\") #(document.value ??)\n" + + "\n" + + "#(document.notes ??)" + + "#for(operation : document.apiOperations)" + + "\n" + + "\n" + + "\n" + + "#(\"##\") #(operation.value ??)" + + "\n" + + "#if(operation.notes)" + + "\n\n#(operation.notes ??) \n" + + "#end" + + "\n" + + "#('####') 接口信息:\n" + + "- 访问路径: `#(operation.actionKey ??)`\n" + + "- 数据类型: `#(operation.contentType.value ??)`\n" + + + "#if(operation.hasParameter())" + + "\n#('####') 请求参数:\n" + + "\n" + + "| 参数 | 名称 | 数据类型 | 是否必须 | 提交方式 | 描述 | \n" + + "| --- | --- | --- | --- | --- | --- |\n" + + "#for(parameter : operation.apiParameters)" + + "| #(parameter.name ??) | #(parameter.value ??) | `#(parameter.dataType ??)` | #(parameter.require ? '是' : '否') | #(parameter.httpMethodsString ??) | #(parameter.notesString ??) | \n" + + "#end" + + "#end" + //参数表格信息 + + "\n" + + "\n" + + + "#if(operation.paraNotes)" + + "> #(operation.paraNotes ??)" + + "#end" + //参数配置 + + "#if(operation.retType)" + + "\n" + + "\n" + + "#('####') 数据响应:`#(operation.retType ??)`\n\n" + + + "#for(item : operation.retRemarks)" + + "#(item.key ??)\n\n" + + "| 字段 | 数据类型 | 描述 | \n" + + "| --- | --- | --- | \n" + + "#for(info : item.value)" + + "| #(info.field ??) | `#(info.classType ??)` | #(info.remarks ??) | \n" + + "#end" + + "\n\n" + + "#end" + //end 响应字段表格 + + "#if(operation.retMockJson)" + + "**JSON 示例:**\n" + + "```json\n" + + "#(operation.retMockJson ??)\n" + + "```" + + "#end" + //end json示例 + + "#end" + // end operation + "\n" + + "\n" + + "#end"; + + @Override + public void render(List apiDocuments, ApiDocConfig config) { + try { + for (ApiDocument document : apiDocuments) { + doRender(document, config); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void doRender(ApiDocument document, ApiDocConfig config) throws IOException { + Map templateParas = new HashMap<>(); + templateParas.put("config", config); + templateParas.put("document", document); + + File file = new File(config.getBasePathAbsolute(), document.getFilePath() + ".md"); + if (!file.exists()) { + file.getParentFile().mkdirs(); + file.createNewFile(); + } + System.err.println("Jboot generated apidoc -----> " + file.getCanonicalPath()); + engine.getTemplateByString(template).render(templateParas, file); + } + }; + + + void render(List apiDocuments, ApiDocConfig config); +} diff --git a/src/main/java/io/jboot/apidoc/ApiDocUtil.java b/src/main/java/io/jboot/apidoc/ApiDocUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..733ac18dc7ea185a72803bfa1c82c89d67690e35 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiDocUtil.java @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.parser.Feature; +import com.jfinal.config.Routes; +import com.jfinal.core.ActionKey; +import com.jfinal.core.Path; +import com.jfinal.kit.StrKit; +import io.jboot.apidoc.annotation.ApiResp; +import io.jboot.apidoc.annotation.ApiResps; +import io.jboot.core.listener.JbootAppListener; +import io.jboot.core.listener.JbootAppListenerManager; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.StrUtil; +import io.jboot.web.controller.annotation.*; + +import java.lang.reflect.Method; +import java.util.*; + +public class ApiDocUtil { + + + public static String getControllerPath(Class controllerClass) { + RequestMapping rm = controllerClass.getAnnotation(RequestMapping.class); + if (rm != null) { + return AnnotationUtil.get(rm.value()); + } + + Path path = controllerClass.getAnnotation(Path.class); + if (path != null) { + return AnnotationUtil.get(path.value()); + } + + PostMapping pm = controllerClass.getAnnotation(PostMapping.class); + if (pm != null) { + return AnnotationUtil.get(pm.value()); + } + + GetMapping gm = controllerClass.getAnnotation(GetMapping.class); + if (gm != null) { + return AnnotationUtil.get(gm.value()); + } + + + return tryToGetInAppListener(controllerClass); + } + + private static Map, String> controllerPathMap = null; + + private static String tryToGetInAppListener(Class controllerClass) { + + if (controllerPathMap != null) { + return controllerPathMap.get(controllerClass); + } else { + controllerPathMap = new HashMap<>(); + } + + List listeners = JbootAppListenerManager.me().getListeners(); + if (listeners == null || listeners.isEmpty()) { + return null; + } + + + Routes baseRoutes = new Routes() { + @Override + public void config() { + } + + @Override + public Routes add(Routes childRoutes) { + childRoutes.config(); + //all child routes + childRoutes.getRouteItemList() + .forEach(route -> controllerPathMap.put(route.getControllerClass(), route.getControllerPath())); + return this; + } + }; + + listeners.forEach(jbootAppListener -> jbootAppListener.onRouteConfig(baseRoutes)); + + //base Routes + baseRoutes.getRouteItemList().forEach(route -> controllerPathMap.put(route.getControllerClass(), route.getControllerPath())); + + return controllerPathMap.get(controllerClass); + } + + + public static HttpMethod getControllerMethod(Class controllerClass) { + RequestMapping rm = controllerClass.getAnnotation(RequestMapping.class); + if (rm != null) { + return HttpMethod.ALL; + } + + Path path = controllerClass.getAnnotation(Path.class); + if (path != null) { + return HttpMethod.ALL; + } + + PostMapping pm = controllerClass.getAnnotation(PostMapping.class); + if (pm != null) { + return HttpMethod.POST; + } + + GetMapping gm = controllerClass.getAnnotation(GetMapping.class); + if (gm != null) { + return HttpMethod.GET; + } + return HttpMethod.ALL; + } + + + public static HttpMethod[] getMethodHttpMethods(Method method, HttpMethod defaultMethod) { + Set httpMethods = new HashSet<>(); + if (method.getAnnotation(GetRequest.class) != null) { + httpMethods.add(HttpMethod.GET); + } + if (method.getAnnotation(PostRequest.class) != null) { + httpMethods.add(HttpMethod.POST); + } + if (method.getAnnotation(PutRequest.class) != null) { + httpMethods.add(HttpMethod.PUT); + } + if (method.getAnnotation(DeleteRequest.class) != null) { + httpMethods.add(HttpMethod.DELETE); + } + if (method.getAnnotation(PatchRequest.class) != null) { + httpMethods.add(HttpMethod.PATCH); + } + return httpMethods.isEmpty() ? new HttpMethod[]{defaultMethod} : httpMethods.toArray(new HttpMethod[]{}); + } + + + private static final String SLASH = "/"; + + public static String getActionKey(Method method, String controllerPath) { + String methodName = method.getName(); + ActionKey ak = method.getAnnotation(ActionKey.class); + String actionKey; + if (ak != null) { + actionKey = ak.value().trim(); + + if (actionKey.startsWith(SLASH)) { + //actionKey = actionKey + } else if (actionKey.startsWith("./")) { + actionKey = controllerPath + actionKey.substring(1); + } else { + actionKey = SLASH + actionKey; + } + } else if (methodName.equals("index")) { + actionKey = controllerPath; + } else { + actionKey = controllerPath.equals(SLASH) ? SLASH + methodName : controllerPath + SLASH + methodName; + } + + return actionKey; + } + + + public static List getApiResponseInMethod(Method method) { + + List retList = new LinkedList<>(); + + ApiResps apiResps = method.getAnnotation(ApiResps.class); + if (apiResps != null) { + for (ApiResp apiResp : apiResps.value()) { + retList.add(new ApiResponse(apiResp)); + } + } + + ApiResp apiResp = method.getAnnotation(ApiResp.class); + if (apiResp != null) { + retList.add(new ApiResponse(apiResp)); + } + + return retList; + } + + + public static String prettyJson(String json) { + if (StrUtil.isBlank(json)) { + return json; + } + JSONObject jsonObject = null; + try { + jsonObject = JSONObject.parseObject(json, Feature.OrderedField); + } catch (Exception e) { + return json; + } + return JSONObject.toJSONString(jsonObject, true); + } + + + public static String getterMethod2Field(Method getterMethod) { + String methodName = getterMethod.getName(); + if (methodName.startsWith("get") && methodName.length() > 3) { + return StrKit.firstCharToLowerCase(methodName.substring(3)); + } else if (methodName.startsWith("is") && methodName.length() > 2) { + return StrKit.firstCharToLowerCase(methodName.substring(2)); + } + return null; + } + + +} diff --git a/src/main/java/io/jboot/apidoc/ApiDocument.java b/src/main/java/io/jboot/apidoc/ApiDocument.java new file mode 100644 index 0000000000000000000000000000000000000000..c25f55cad6d8fc47c5636c169fa37545639b54ff --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiDocument.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import io.jboot.utils.StrUtil; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; + +public class ApiDocument implements Serializable { + + private String value; + private String notes; + private String filePath; + + private List apiOperations; + + private Class controllerClass; + + public ApiDocument() { + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + public void setFilePathByControllerPath(String controllerPath) { + if (controllerPath == null || StrUtil.isBlank(controllerPath)) { + throw new IllegalArgumentException("The request mapping path of Controller \"" + getControllerClass().getName() + "\" is empty."); + } + + if ("/".equals(controllerPath)) { + controllerPath = "index"; + } else if (controllerPath.startsWith("/")) { + controllerPath = controllerPath.substring(1); + } + if (controllerPath.contains("/")) { + controllerPath = controllerPath.replace("/", "_"); + } + + this.filePath = controllerPath; + } + + public List getApiOperations() { + return apiOperations; + } + + public void setApiOperations(List apiOperations) { + this.apiOperations = apiOperations; + } + + public void addOperation(ApiOperation apiOperation) { + if (apiOperations == null) { + apiOperations = new LinkedList<>(); + } + apiOperations.add(apiOperation); + } + + public Class getControllerClass() { + return controllerClass; + } + + public void setControllerClass(Class controllerClass) { + this.controllerClass = controllerClass; + } + + @Override + public String toString() { + return "ApiDocument{" + + "value='" + value + '\'' + + ", notes='" + notes + '\'' + + '}'; + } +} diff --git a/src/main/java/io/jboot/apidoc/ApiJsonGenerator.java b/src/main/java/io/jboot/apidoc/ApiJsonGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..a2ba83ae7f352a29964e740ffc8a729da1a8282f --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiJsonGenerator.java @@ -0,0 +1,324 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.parser.Feature; +import com.jfinal.kit.PathKit; +import com.jfinal.kit.StrKit; +import com.jfinal.plugin.activerecord.generator.ColumnMeta; +import com.jfinal.plugin.activerecord.generator.MetaBuilder; +import com.jfinal.plugin.activerecord.generator.TableMeta; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.jboot.Jboot; +import io.jboot.codegen.CodeGenHelpler; +import io.jboot.db.datasource.DataSourceConfig; +import io.jboot.db.driver.DriverClassNames; +import io.jboot.utils.DateUtil; +import io.jboot.utils.FileUtil; +import io.jboot.utils.StrUtil; + +import javax.sql.DataSource; +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.temporal.Temporal; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class ApiJsonGenerator { + + /** + * 生成 Mock Json 数据 + */ + public static void genMockJson() { + genMockJson(new JsonGeneratorConfig("api-mock.json")); + } + + + /** + * 生成 Mock Json 数据 + * + * @param config + */ + public static void genMockJson(JsonGeneratorConfig config) { + + Map> root = new LinkedHashMap<>(); + + File file = new File(config.getJsonFilePathPathAbsolute()); + //如果文件存在,则先读取其配置,然后再修改 + if (file.exists()) { + String oldJson = FileUtil.readString(file); + JSONObject rootJsonObject = JSONObject.parseObject(oldJson, Feature.OrderedField); + if (rootJsonObject != null && !rootJsonObject.isEmpty()) { + for (String classOrSimpleName : rootJsonObject.keySet()) { + root.put(classOrSimpleName, rootJsonObject.getJSONObject(classOrSimpleName)); + } + } + } + + MetaBuilder builder = CodeGenHelpler.createMetaBuilder(config.getDatasource(), config.getType(), false); + List tableMetas = builder.build(); + + if (config.tableMetaFilter != null) { + tableMetas = tableMetas.stream() + .filter(config.tableMetaFilter) + .collect(Collectors.toList()); + } + + + for (TableMeta tableMeta : tableMetas) { + Map classMockData = new LinkedHashMap<>(); + for (ColumnMeta columnMeta : tableMeta.columnMetas) { + Object mockData = createMockData(columnMeta); + if (mockData != null && !"".equals(mockData)) { + classMockData.put(columnMeta.attrName, mockData); + } + } + if (!classMockData.isEmpty()) { + root.put(StrKit.firstCharToLowerCase(tableMeta.modelName), classMockData); + } + } + + + String jsonContent = JSONObject.toJSONString(root, true); + FileUtil.writeString(file, jsonContent); + + System.out.println("Gen Remarks Json File ----->" + FileUtil.getCanonicalPath(file)); + } + + + private static Object createMockData(ColumnMeta columnMeta) { + if (String.class.getName().equals(columnMeta.javaType)) { + return columnMeta.remarks; + } else if (Date.class.getName().equals(columnMeta.javaType)) { + return DateUtil.toDateTimeString(new Date()); + } else if (isTemporal(columnMeta.javaType)) { + return DateUtil.toDateTimeString(new Date()); + } else if (int.class.getName().equals(columnMeta.javaType)) { + return 1; + } else if (Integer.class.getName().equals(columnMeta.javaType)) { + return 100; + } else if (long.class.getName().equals(columnMeta.javaType)) { + return 1; + } else if (Long.class.getName().equals(columnMeta.javaType)) { + return 100; + } else if (short.class.getName().equals(columnMeta.javaType)) { + return 1; + } else if (Short.class.getName().equals(columnMeta.javaType)) { + return 100; + } else if (BigDecimal.class.getName().equals(columnMeta.javaType)) { + return BigDecimal.ONE; + } else if (BigInteger.class.getName().equals(columnMeta.javaType)) { + return BigDecimal.ONE; + } else if (boolean.class.getName().equals(columnMeta.javaType)) { + return Boolean.TRUE; + } else if (Boolean.class.getName().equals(columnMeta.javaType)) { + return Boolean.TRUE; + } else if (float.class.getName().equals(columnMeta.javaType)) { + return 1.0f; + } else if (Float.class.getName().equals(columnMeta.javaType)) { + return 1.0f; + } else if (double.class.getName().equals(columnMeta.javaType)) { + return 1.0d; + } else if (Double.class.getName().equals(columnMeta.javaType)) { + return 1.0d; + } else { + return ""; + } + } + + private static boolean isTemporal(String javaType) { + try { + return Temporal.class.isAssignableFrom(Class.forName(javaType)); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return false; + } + + + /** + * 生成 Model 的字段备注数据 + */ + public static void genRemarksJson() { + genRemarksJson(new JsonGeneratorConfig("api-remarks.json")); + } + + /** + * 生成 Model 的字段备注数据 + */ + public static void genRemarksJson(JsonGeneratorConfig config) { + + Map> root = new LinkedHashMap<>(); + + File file = new File(config.getJsonFilePathPathAbsolute()); + //如果文件存在,则先读取其配置,然后再修改 + if (file.exists()) { + String oldJson = FileUtil.readString(file); + JSONObject rootJsonObject = JSONObject.parseObject(oldJson, Feature.OrderedField); + if (rootJsonObject != null && !rootJsonObject.isEmpty()) { + for (String classOrSimpleName : rootJsonObject.keySet()) { + Map remarks = new LinkedHashMap<>(); + JSONObject modelRemarks = rootJsonObject.getJSONObject(classOrSimpleName); + modelRemarks.forEach((k, v) -> remarks.put(k, String.valueOf(v))); + root.put(classOrSimpleName, remarks); + } + } + } + + + MetaBuilder builder = CodeGenHelpler.createMetaBuilder(config.getDatasource(), config.getType(), false); + List tableMetas = builder.build(); + + if (config.tableMetaFilter != null) { + tableMetas = tableMetas.stream() + .filter(config.tableMetaFilter) + .collect(Collectors.toList()); + } + + + for (TableMeta tableMeta : tableMetas) { + Map modelRemarks = new LinkedHashMap<>(); + for (ColumnMeta columnMeta : tableMeta.columnMetas) { + if (StrUtil.isNotBlank(columnMeta.remarks)) { + modelRemarks.put(columnMeta.attrName, columnMeta.remarks); + } + } + if (!modelRemarks.isEmpty()) { + root.put(StrKit.firstCharToLowerCase(tableMeta.modelName), modelRemarks); + } + } + + + String jsonContent = JSONObject.toJSONString(root, true); + FileUtil.writeString(file, jsonContent); + + System.out.println("Gen Remarks Json File ----->" + FileUtil.getCanonicalPath(file)); + } + + + public static class JsonGeneratorConfig { + + private boolean useJbootDatasource = true; + + private String jdbcUrl; + private String userName; + private String password; + private String type = DataSourceConfig.TYPE_MYSQL; + + private String jsonFilePath; + private Predicate tableMetaFilter; + + public JsonGeneratorConfig() { + } + + public JsonGeneratorConfig(String jsonFilePath) { + this.jsonFilePath = jsonFilePath; + } + + public boolean isUseJbootDatasource() { + return useJbootDatasource; + } + + public void setUseJbootDatasource(boolean useJbootDatasource) { + this.useJbootDatasource = useJbootDatasource; + } + + public String getJdbcUrl() { + return jdbcUrl; + } + + public void setJdbcUrl(String jdbcUrl) { + this.jdbcUrl = jdbcUrl; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getJsonFilePath() { + return jsonFilePath; + } + + public void setJsonFilePath(String jsonFilePath) { + this.jsonFilePath = jsonFilePath; + } + + public String getJsonFilePathPathAbsolute() { + if (FileUtil.isAbsolutePath(jsonFilePath)) { + return jsonFilePath; + } + return FileUtil.getCanonicalPath(new File(PathKit.getRootClassPath(), "../../" + jsonFilePath)); + } + + + public Predicate getTableMetaFilter() { + return tableMetaFilter; + } + + public void setTableMetaFilter(Predicate tableMetaFilter) { + this.tableMetaFilter = tableMetaFilter; + } + + + public DataSource getDatasource() { + if (useJbootDatasource) { + DataSourceConfig datasourceConfig = Jboot.config(DataSourceConfig.class, "jboot.datasource"); + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(datasourceConfig.getUrl()); + config.setUsername(datasourceConfig.getUser()); + config.setPassword(datasourceConfig.getPassword()); + config.setDriverClassName(datasourceConfig.getDriverClassName()); + + return new HikariDataSource(config); + } else { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(this.jdbcUrl); + config.setUsername(this.userName); + config.setPassword(this.password); + config.setDriverClassName(DriverClassNames.getDefaultDriverClass(this.type)); + + return new HikariDataSource(config); + } + } + } +} diff --git a/src/main/java/io/jboot/apidoc/ApiMockBuilder.java b/src/main/java/io/jboot/apidoc/ApiMockBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..5ae6b9b616d3e581f48fb34f203efc90a8276331 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiMockBuilder.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import io.jboot.utils.ClassType; + +import java.lang.reflect.Method; + +public abstract class ApiMockBuilder { + + + protected Object getMockObject(ClassType classType, Method method, int level) { + return ApiDocManager.me().doBuildMockObject(classType, method, level + 1); + } + + public abstract Object build(ClassType classType, Method method, int level); +} diff --git a/src/main/java/io/jboot/apidoc/ApiMockBuilders.java b/src/main/java/io/jboot/apidoc/ApiMockBuilders.java new file mode 100644 index 0000000000000000000000000000000000000000..3e856bb9e46f7eb10f94be3498e61cd6b6edc8b9 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiMockBuilders.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import com.jfinal.kit.Ret; +import com.jfinal.plugin.activerecord.Page; +import io.jboot.utils.ClassType; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ApiMockBuilders { + + static ApiMockBuilder retBuilder = new ApiMockBuilder() { + @Override + public Object build(ClassType classType, Method method, int level) { + Ret ret = Ret.ok(); + if (classType.isGeneric()) { + ClassType[] genericTypes = classType.getGenericTypes(); + if (genericTypes.length == 1) { + Class type = genericTypes[0].getMainClass(); + if (List.class.isAssignableFrom(type)) { + ret.set("list", getMockObject(genericTypes[0], method, level)); + } else if (Map.class.isAssignableFrom(type)) { + ret.set("map", getMockObject(genericTypes[0], method, level)); + } else if (Page.class.isAssignableFrom(type)) { + ret.set("page", getMockObject(genericTypes[0], method, level)); + } else { + ret.set("object", getMockObject(genericTypes[0], method, level)); + } + } + } + List responses = ApiDocUtil.getApiResponseInMethod(method); + for (ApiResponse response : responses) { + Object mockObject = null; + if (level == 0 && !response.isType(Map.class) && !response.isType(Ret.class)) { + mockObject = getMockObject(response.getClassType(), method, level); + } + if (mockObject == null || "".equals(mockObject)) { + mockObject = response.getMockObject(); + } + ret.put(response.getField(), mockObject); + } + return ret; + } + }; + + + static ApiMockBuilder mapBuilder = new ApiMockBuilder() { + @Override + public Object build(ClassType classType, Method method, int level) { + // ret 让给 retBuilder 去构建 + if (Ret.class.isAssignableFrom(classType.getMainClass())) { + return null; + } + + Map map = new HashMap(); + if (classType.isGeneric()) { + Object key = getMockObject(classType.getGenericTypes()[0], method, level); + if (key == null) { + key = "key"; + } + Object value = getMockObject(classType.getGenericTypes()[1], method, level); + map.put(key, value); + } + + List responses = ApiDocUtil.getApiResponseInMethod(method); + for (ApiResponse response : responses) { + Object mockObject = null; + if (level == 0 && !response.isType(Map.class) && !response.isType(Ret.class)) { + mockObject = getMockObject(response.getClassType(), method, level); + } + if (mockObject == null || "".equals(mockObject)) { + mockObject = response.getMockObject(); + } + map.put(response.getField(), mockObject); + } + return map; + } + }; + + + static ApiMockBuilder listBuilder = new ApiMockBuilder() { + @Override + public Object build(ClassType classType, Method method, int level) { + List list = new ArrayList(); + if (classType.isGeneric()) { + Object value = getMockObject(classType.getGenericTypes()[0], method, level); + list.add(value); + Object value2 = getMockObject(classType.getGenericTypes()[0], method, level); + list.add(value2); + } + return list; + } + }; + + + static ApiMockBuilder pageBuilder = new ApiMockBuilder() { + @Override + public Object build(ClassType classType, Method method, int level) { + Page page = new Page(); + page.setPageNumber(1); + page.setPageSize(10); + page.setTotalPage(1); + page.setTotalRow(2); + + List list = new ArrayList(); + list.add(getMockObject(classType.getGenericTypes()[0], method, level)); + list.add(getMockObject(classType.getGenericTypes()[0], method, level)); + + page.setList(list); + return page; + } + }; + + + static ApiMockBuilder stringBuilder = new ApiMockBuilder() { + @Override + public Object build(ClassType classType, Method method, int level) { + return ""; + } + }; + + +} diff --git a/src/main/java/io/jboot/apidoc/ApiOperation.java b/src/main/java/io/jboot/apidoc/ApiOperation.java new file mode 100644 index 0000000000000000000000000000000000000000..a603239d6a1e1f102dd6a7c7ed2bc91f2febef65 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiOperation.java @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import io.jboot.aop.annotation.DefaultValue; +import io.jboot.apidoc.annotation.ApiPara; +import io.jboot.apidoc.annotation.ApiParas; +import io.jboot.utils.ClassType; +import io.jboot.utils.ClassUtil; +import io.jboot.web.json.JsonBody; + +import javax.validation.constraints.*; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class ApiOperation implements Serializable { + + private String value; //标题 + private String notes; //描述 + private String paraNotes; //参数描述,对所有参数的描述 + private int orderNo; //排序序号 + + private String actionKey; // 路径 + private ContentType contentType; + private List apiParameters; //所有的参数 + + private ClassType retType; //响应类型 + private String retMockJson; //响应 mock json + private Map> retRemarks; //响应的字段备注(描述),key 类名的简称,value 类名的字段 + + + private Class controllerClass; //所在的类 + private Method method; //对应的方法 + + + public ApiOperation() { + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + public String getParaNotes() { + return paraNotes; + } + + public void setParaNotes(String paraNotes) { + this.paraNotes = paraNotes; + } + + public int getOrderNo() { + return orderNo; + } + + public void setOrderNo(int orderNo) { + this.orderNo = orderNo; + } + + public String getActionKey() { + return actionKey; + } + + public void setActionKey(String actionKey) { + this.actionKey = actionKey; + } + + public ContentType getContentType() { + return contentType; + } + + public void setContentType(ContentType contentType) { + this.contentType = contentType; + } + + public List getApiParameters() { + return apiParameters; + } + + public void setApiParameters(List apiParameters) { + this.apiParameters = apiParameters; + } + + public void addApiParameter(ApiParameter parameter) { + if (apiParameters == null) { + apiParameters = new LinkedList<>(); + } + apiParameters.add(parameter); + } + + public boolean hasParameter() { + return apiParameters != null && apiParameters.size() > 0; + } + + public ClassType getRetType() { + return retType; + } + + public void setRetType(ClassType retType) { + this.retType = retType; + } + + public Method getMethod() { + return method; + } + + public void setMethod(Method method) { + this.method = method; + } + + public Class getControllerClass() { + return controllerClass; + } + + public void setControllerClass(Class controllerClass) { + this.controllerClass = controllerClass; + } + + + public void setMethodAndInfo(Method method, String controllerPath, HttpMethod[] defaultMethods, Class containerClass) { + + this.method = method; + this.actionKey = ApiDocUtil.getActionKey(method, controllerPath); + this.retType = ClassUtil.getClassType(method.getGenericReturnType(), getControllerClass()); + + if (retType.isVoid() && containerClass != null) { + retType = new ClassType(containerClass); + } + + this.retMockJson = ApiDocManager.me().buildMockJson(retType, method); + this.retRemarks = ApiDocManager.me().buildRemarks(retType, method); + + setParameters(method, defaultMethods); + } + + + private void setParameters(Method method, HttpMethod[] defaultMethods) { + ApiParas apiParas = method.getAnnotation(ApiParas.class); + if (apiParas != null) { + for (ApiPara apiPara : apiParas.value()) { + addApiParameter(new ApiParameter(apiPara, defaultMethods)); + } + } + + ApiPara apiPara = method.getAnnotation(ApiPara.class); + if (apiPara != null) { + addApiParameter(new ApiParameter(apiPara, defaultMethods)); + } + + Parameter[] parameters = method.getParameters(); + Type[] paraTypes = method.getGenericParameterTypes(); + for (int i = 0; i < parameters.length; i++) { + + ApiParameter apiParameter = new ApiParameter(); + Parameter parameter = parameters[i]; + apiParameter.setName(parameter.getName()); + apiParameter.setDataType(ClassUtil.getClassType(paraTypes[i], getControllerClass())); + + if (parameter.getAnnotation(JsonBody.class) != null) { + apiParameter.setHttpMethods(new HttpMethod[]{HttpMethod.POST}); + } else { + apiParameter.setHttpMethods(defaultMethods); + } + + ApiPara paraAnnotation = parameter.getAnnotation(ApiPara.class); + if (paraAnnotation != null) { + apiParameter.setValue(paraAnnotation.value()); + apiParameter.setNotes(paraAnnotation.notes()); + + if (paraAnnotation.method().length > 0) { + apiParameter.setHttpMethods(paraAnnotation.method()); + } + + if (paraAnnotation.require()) { + apiParameter.setNotBlank(true); + apiParameter.setRequire(true); + } + } + + if (parameter.getAnnotation(NotNull.class) != null) { + apiParameter.setRequire(true); + } + + if (parameter.getAnnotation(NotBlank.class) != null) { + apiParameter.setNotBlank(true); + apiParameter.setRequire(true); + } + + if (parameter.getAnnotation(NotEmpty.class) != null) { + apiParameter.setNotEmpty(true); + apiParameter.setRequire(true); + } + + if (parameter.getAnnotation(Email.class) != null) { + apiParameter.setEmail(true); + apiParameter.setRequire(true); + } + + Min min = parameter.getAnnotation(Min.class); + if (min != null) { + apiParameter.setMin(min.value()); + apiParameter.setRequire(true); + } + + Max max = parameter.getAnnotation(Max.class); + if (max != null) { + apiParameter.setMax(max.value()); + apiParameter.setRequire(true); + } + + Pattern pattern = parameter.getAnnotation(Pattern.class); + if (pattern != null) { + apiParameter.setPattern(pattern.regexp()); + apiParameter.setRequire(true); + } + + DefaultValue defaultValue = parameter.getAnnotation(DefaultValue.class); + if (defaultValue != null) { + apiParameter.setDefaultValue(defaultValue.value()); + } + + addApiParameter(apiParameter); + } + } + + public String getRetMockJson() { + return retMockJson; + } + + public void setRetMockJson(String retMockJson) { + this.retMockJson = retMockJson; + } + + public Map> getRetRemarks() { + return retRemarks; + } + + public void setRetRemarks(Map> retRemarks) { + this.retRemarks = retRemarks; + } + + @Override + public String toString() { + return "ApiOperation{" + + "value='" + value + '\'' + + ", notes='" + notes + '\'' + + ", actionKey='" + actionKey + '\'' + + '}'; + } +} diff --git a/src/main/java/io/jboot/apidoc/ApiParameter.java b/src/main/java/io/jboot/apidoc/ApiParameter.java new file mode 100644 index 0000000000000000000000000000000000000000..2591ca833e49e3fc30d57b61c2dd79ad30ae2837 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiParameter.java @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import io.jboot.apidoc.annotation.ApiPara; +import io.jboot.utils.ClassType; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.StrUtil; + +import java.io.Serializable; + +public class ApiParameter implements Serializable { + + private String name; + private String value; + private String notes; + + private ClassType dataType; + + private HttpMethod[] httpMethods; + private Boolean require; + private Boolean notBlank; + private Boolean notEmpty; + private Boolean email; + private Long min; + private Long max; + private String pattern; + private String defaultValue; + + public ApiParameter() { + } + + public ApiParameter(ApiPara apiPara, HttpMethod[] defaultMethods) { + this.name = apiPara.name(); + this.value = apiPara.value(); + this.notes = apiPara.notes(); + this.dataType = ClassUtil.getClassType(apiPara.dataType(), null); + this.httpMethods = apiPara.method().length == 0 ? defaultMethods : apiPara.method(); + this.require = apiPara.require(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + public ClassType getDataType() { + return dataType; + } + + public void setDataType(ClassType dataType) { + this.dataType = dataType; + } + + public HttpMethod[] getHttpMethods() { + return httpMethods; + } + + public void setHttpMethods(HttpMethod[] httpMethods) { + this.httpMethods = httpMethods; + } + + public Boolean getRequire() { + return require; + } + + public void setRequire(Boolean require) { + this.require = require; + } + + public Boolean getNotBlank() { + return notBlank; + } + + public void setNotBlank(Boolean notBlank) { + this.notBlank = notBlank; + } + + public Boolean getNotEmpty() { + return notEmpty; + } + + public void setNotEmpty(Boolean notEmpty) { + this.notEmpty = notEmpty; + } + + public Boolean getEmail() { + return email; + } + + public void setEmail(Boolean email) { + this.email = email; + } + + public Long getMin() { + return min; + } + + public void setMin(Long min) { + this.min = min; + } + + public Long getMax() { + return max; + } + + public void setMax(Long max) { + this.max = max; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public String getHttpMethodsString() { + StringBuilder sb = new StringBuilder(); + if (httpMethods != null) { + for (int i = 0; i < httpMethods.length; i++) { + sb.append(httpMethods[i].getValue()); + if (i != httpMethods.length - 1) { + sb.append(", "); + } + } + } + return sb.toString(); + } + + public String getNotesString() { + StringBuilder sb = new StringBuilder(); + if (StrUtil.isNotBlank(defaultValue)) { + sb.append("默认值:" + defaultValue); + } + + if (StrUtil.isNotBlank(notes)) { + if (sb.length() > 0) { + sb.append(" ;"); + } + sb.append(notes); + } + return sb.toString(); + } + + + @Override + public String toString() { + return "ApiParameter{" + + "name='" + name + '\'' + + ", value='" + value + '\'' + + ", notes='" + notes + '\'' + + '}'; + } +} diff --git a/src/main/java/io/jboot/apidoc/ApiResponse.java b/src/main/java/io/jboot/apidoc/ApiResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..c95c54937939f6f20959b28a1f9a34a59cb430ad --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiResponse.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +import com.alibaba.fastjson.JSONObject; +import io.jboot.apidoc.annotation.ApiResp; +import io.jboot.utils.ClassType; +import io.jboot.utils.StrUtil; + +import java.io.Serializable; +import java.util.Objects; + +public class ApiResponse implements Serializable { + + private String field; + private String dataType; + private ClassType classType; + private String remarks; + private String mock; + + + public ApiResponse() { + } + + public ApiResponse(ApiResp apiResp) { + this.field = apiResp.field(); + this.dataType = apiResp.dataType().getSimpleName(); + this.classType = new ClassType(apiResp.dataType(), apiResp.genericTypes()); + this.remarks = apiResp.notes(); + this.mock = apiResp.mock(); + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getDataType() { + return dataType; + } + + public void setDataType(String dataType) { + this.dataType = dataType; + } + + public void setDataAndClassType(Class clazz) { + this.dataType = clazz.getSimpleName(); + this.classType = new ClassType(clazz); + } + + public ClassType getClassType() { + return classType; + } + + public void setClassType(ClassType classType) { + this.classType = classType; + } + + public String getRemarks() { + return remarks; + } + + public void setRemarks(String remarks) { + this.remarks = remarks; + } + + public String getMock() { + return mock; + } + + public Object getMockObject() { + if (StrUtil.isBlank(mock)) { + return ""; + } + if ((mock.startsWith("{") && mock.endsWith("}")) || (mock.startsWith("[") && mock.endsWith("]"))) { + return JSONObject.parse(mock); + } else if (StrUtil.isNumeric(mock) && (Number.class.isAssignableFrom(classType.getMainClass()) || isType(int.class) || isType(long.class))) { + return Long.parseLong(mock); + } else if (StrUtil.isDecimal(mock) && (Number.class.isAssignableFrom(classType.getMainClass()) || isType(float.class) || isType(double.class))) { + return Double.parseDouble(mock); + } else { + return mock; + } + } + + public void setMock(String mock) { + this.mock = mock; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ApiResponse that = (ApiResponse) o; + return Objects.equals(field, that.field); + } + + public boolean isType(Class clazz) { + return this.classType != null && clazz == this.classType.getMainClass(); + } + + + @Override + public String toString() { + return "ApiResponse{" + + "name='" + field + '\'' + + ", dataType='" + dataType + '\'' + + ", remarks='" + remarks + '\'' + + '}'; + } +} diff --git a/src/main/java/io/jboot/apidoc/ApiRet.java b/src/main/java/io/jboot/apidoc/ApiRet.java new file mode 100644 index 0000000000000000000000000000000000000000..5c8ebbe7916fd4214fe03099ca6a3457bacab6c8 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ApiRet.java @@ -0,0 +1,130 @@ +package io.jboot.apidoc; + +import com.jfinal.kit.Ret; + +public class ApiRet { + + private static final String STATE_OK = "ok"; + private static final String STATE_FAIL = "fail"; + + //状态,使用 string 是为了兼容 JFinal 的 Ret,目前很多前端是通过 state 来进行判断的 + private String state; + + //错误码 + private Integer errorCode; + + //对本次状态码的描述 + private String message; + + //数据 + private T data; + + public static ApiRet by(Ret ret) { + ApiRet apiRet = new ApiRet(); + apiRet.state = ret.isOk() ? STATE_OK : (ret.isFail() ? STATE_FAIL : null); + for (Object key : ret.keySet()) { + if ("state".equals(key)) { + continue; + } + apiRet.data = ret.get(key); + } + return apiRet; + } + + public static ApiRet ok() { + ApiRet apiRet = new ApiRet(); + apiRet.state = STATE_OK; + return apiRet; + } + + public static ApiRet ok(Object data) { + ApiRet apiRet = new ApiRet(); + apiRet.state = STATE_OK; + apiRet.data = data; + return apiRet; + } + + public static ApiRet fail() { + ApiRet apiRet = new ApiRet(); + apiRet.state = STATE_FAIL; + return apiRet; + } + + + public static ApiRet fail(int errorCode) { + ApiRet apiRet = new ApiRet(); + apiRet.state = STATE_FAIL; + apiRet.errorCode = errorCode; + return apiRet; + } + + public static ApiRet fail(String message) { + ApiRet apiRet = new ApiRet(); + apiRet.state = STATE_FAIL; + apiRet.message = message; + return apiRet; + } + + + public static ApiRet fail(int errorCode, String message) { + ApiRet apiRet = new ApiRet(); + apiRet.state = STATE_FAIL; + apiRet.errorCode = errorCode; + apiRet.message = message; + return apiRet; + } + + + public ApiRet data(T data) { + this.data = data; + return this; + } + + public ApiRet message(String message) { + this.message = message; + return this; + } + + public ApiRet code(int errorCode) { + this.errorCode = errorCode; + return this; + } + + public ApiRet state(boolean ok) { + this.state = ok ? STATE_OK : STATE_FAIL; + return this; + } + + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public Integer getErrorCode() { + return errorCode; + } + + public void setErrorCode(Integer errorCode) { + this.errorCode = errorCode; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} diff --git a/src/main/java/io/jboot/apidoc/ContentType.java b/src/main/java/io/jboot/apidoc/ContentType.java new file mode 100644 index 0000000000000000000000000000000000000000..69b026518d38c6234c575a474fc6b015d26b00a5 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/ContentType.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +public enum ContentType { + + DEFAULT("application/x-www-form-urlencoded"), JSON("application/json"); + + private String value; + + ContentType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/io/jboot/apidoc/HttpMethod.java b/src/main/java/io/jboot/apidoc/HttpMethod.java new file mode 100644 index 0000000000000000000000000000000000000000..7fbdec8a7feefe1138f3ba9b44e8d021281f5df6 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/HttpMethod.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc; + +public enum HttpMethod { + + ALL("*"), GET("GET"), POST("POST"), PUT("PUT"), PATCH("PATCH"), DELETE("DELETE"); + + private String value; + + HttpMethod(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/io/jboot/apidoc/annotation/Api.java b/src/main/java/io/jboot/apidoc/annotation/Api.java new file mode 100644 index 0000000000000000000000000000000000000000..1d5c98265ec4550a6d4d8b11357f3d019c50ec29 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/annotation/Api.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Api { + + /** + * 文档标题 + * + * @return + */ + String value(); + + /** + * 文档描述 + * + * @return + */ + String notes() default ""; + + /** + * 生成的文件路径,不配置的情况下,默认为 Controller 的 mapping 转换,例如 mapping 为:/abc/aaa 转为为 abc_aaa。 / 转为为 index。 + * + * @return + */ + String filePath() default ""; + + /** + * 其他类的方法也汇总到此 API 文档里来 + * + * @return + */ + Class[] collect() default {}; + + /** + * 生成文档的排序 + * + * @return + */ + int orderNo() default 0; + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/apidoc/annotation/ApiOper.java b/src/main/java/io/jboot/apidoc/annotation/ApiOper.java new file mode 100644 index 0000000000000000000000000000000000000000..42935b104629df640022ee712feb86147e78b401 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/annotation/ApiOper.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc.annotation; + +import io.jboot.apidoc.ContentType; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface ApiOper { + + /** + * 标题 + * + * @return + */ + String value(); + + /** + * 描述 + * + * @return + */ + String notes() default ""; + + /** + * 参数描述 + * + * @return + */ + String paraNotes() default ""; + + /** + * Http 的 Content-Type + * + * @return + */ + ContentType contentType() default ContentType.DEFAULT; + + /** + * 生成文档的排序 + * + * @return + */ + int orderNo() default 0; + + + /** + * 对于某些没有返回值(void)的方法,其调用其他方法渲染数据时,可以通过其设置顶级的类 比如 Ret 或者 Map 之类的 + * + * @return + */ + Class containerClass() default void.class; + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/apidoc/annotation/ApiPara.java b/src/main/java/io/jboot/apidoc/annotation/ApiPara.java new file mode 100644 index 0000000000000000000000000000000000000000..300e3403a4a9372178d03cf7a099ff1652ec958f --- /dev/null +++ b/src/main/java/io/jboot/apidoc/annotation/ApiPara.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc.annotation; + +import io.jboot.apidoc.HttpMethod; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.PARAMETER}) +public @interface ApiPara { + + /** + * 标题 + * + * @return + */ + String value(); + + /** + * 参数名称 + * + * @return + */ + String name() default ""; + + /** + * 描述 + * + * @return + */ + String notes() default ""; + + /** + * 数据类型 + * + * @return + */ + Class dataType() default void.class; + + /** + * 要求通过哪些方法传入,比如只能通过 post 传入 + * + * @return + */ + HttpMethod[] method() default {}; + + /** + * 是否必填 + * @return + */ + boolean require() default false; +} \ No newline at end of file diff --git a/src/main/java/io/jboot/apidoc/annotation/ApiParas.java b/src/main/java/io/jboot/apidoc/annotation/ApiParas.java new file mode 100644 index 0000000000000000000000000000000000000000..2c455d44ce1f471658056c53a064141d22fd7982 --- /dev/null +++ b/src/main/java/io/jboot/apidoc/annotation/ApiParas.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface ApiParas { + + ApiPara[] value(); + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/cors/EnableCORS.java b/src/main/java/io/jboot/apidoc/annotation/ApiResp.java similarity index 45% rename from src/main/java/io/jboot/web/cors/EnableCORS.java rename to src/main/java/io/jboot/apidoc/annotation/ApiResp.java index c5552494f68794b169c1e1be299cce336174814e..0f7fb09dfc53fb51a6f91fc2170ab67e08d8f1d8 100644 --- a/src/main/java/io/jboot/web/cors/EnableCORS.java +++ b/src/main/java/io/jboot/apidoc/annotation/ApiResp.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,37 +13,50 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.web.cors; +package io.jboot.apidoc.annotation; import java.lang.annotation.*; -/** - * @author Michael Yang 杨福海 (fuhai999@gmail.com) - * @version V1.0 - * @Package io.jboot.web.cors - *

- * detail : https://developer.mozilla.org/en-US/docs/Glossary/CORS - */ @Inherited @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface EnableCORS { - - String allowOrigin() default "*"; - - String allowCredentials() default "true"; - - String allowHeaders() default "Origin,X-Requested-With,Content-Type,Accept,Authorization,Jwt"; - - String allowMethods() default "GET,PUT,POST,DELETE,PATCH,OPTIONS"; - - String exposeHeaders() default ""; - - String requestHeaders() default ""; - - String requestMethod() default ""; - - String origin() default ""; - - String maxAge() default "3600"; -} +@Target({ElementType.METHOD, ElementType.PARAMETER}) +public @interface ApiResp { + + /** + * 字段名称 + * + * @return + */ + String field(); + + /** + * 描述 + * + * @return + */ + String notes(); + + /** + * 数据类型 + * + * @return + */ + Class dataType() default String.class; + + /** + * 泛型类型 + * + * @return + */ + Class[] genericTypes() default {}; + + + /** + * mock 数据 + * + * @return + */ + String mock() default ""; + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/apidoc/annotation/ApiResps.java b/src/main/java/io/jboot/apidoc/annotation/ApiResps.java new file mode 100644 index 0000000000000000000000000000000000000000..b4a3eca38ed4a8f466bfe5dbcdd4b2292948025c --- /dev/null +++ b/src/main/java/io/jboot/apidoc/annotation/ApiResps.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.apidoc.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface ApiResps { + + ApiResp[] value(); + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/app/ApplicationUtil.java b/src/main/java/io/jboot/app/ApplicationUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..f4c1d5a63fe54a514b04d02ee6abfdcd72c46350 --- /dev/null +++ b/src/main/java/io/jboot/app/ApplicationUtil.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.app; + +import io.jboot.app.config.JbootConfigManager; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; + +public class ApplicationUtil { + + static JbootApplicationConfig getAppConfig(String[] args) { + JbootConfigManager.parseArgs(args); + return getConfig(JbootApplicationConfig.class); + } + + + static void printBannerInfo(JbootApplicationConfig appConfig) { + if (appConfig.isBannerEnable()) { + System.out.println(); + System.out.println(Banner.getText(appConfig.getBannerFile())); + System.out.println(); + } + } + + static void printApplicationInfo(JbootApplicationConfig appConfig) { + System.out.println(appConfig.toString()); + } + + private static Boolean runInFatjar; + + public static boolean runInFatjar() { + if (runInFatjar == null) { + runInFatjar = buildRunInFatjar(); + } + return runInFatjar; + } + + + private static boolean buildRunInFatjar() { + URL url = Thread.currentThread().getContextClassLoader().getResource(""); + if (url == null) { + return true; + } + + if ("jar".equalsIgnoreCase(url.getProtocol())) { + return true; + } + + String urlStr = url.toString().toLowerCase(); + if (urlStr.endsWith(".jar!/")) { + return true; + } + + // 在某些情况下 通过 java -jar 运行时,会以 /config/ 结束 + if (urlStr.endsWith("/config/")) { + File urlPath; + try { + //中文目录乱码的问题 + urlPath = new File(URLDecoder.decode(url.getFile(), "UTF-8")); + } catch (UnsupportedEncodingException e) { + urlPath = new File(url.getPath()); + } + return !urlPath.exists() || !urlPath.isDirectory(); + } + + return false; + } + + + static void printClassPath() { + try { + if (runInFatjar()) { + System.out.println("JbootApplication is running in fatjar."); + } else { + String path = ApplicationUtil.class.getResource("/").getPath(); + // 例如: /D:/JAVA/workSpace_idea/... + if (path.indexOf(":") == 2) { + path = path.substring(1); + } + System.out.println("JbootApplication classpath: " + path); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + static T getConfig(Class clazz) { + return JbootConfigManager.me().get(clazz); + } + + static String getConfigValue(String key) { + return JbootConfigManager.me().getConfigValue(key); + } + + + static boolean isDevMode() { + return JbootConfigManager.me().isDevMode(); + } + + +} diff --git a/src/main/java/io/jboot/app/Banner.java b/src/main/java/io/jboot/app/Banner.java index d6c59e32be5aec5b8b25c251ab21584a52da03f3..2b13d654715bacc0f81d4c30d640f7b53e342254 100644 --- a/src/main/java/io/jboot/app/Banner.java +++ b/src/main/java/io/jboot/app/Banner.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/app/HttpContentTypes.java b/src/main/java/io/jboot/app/HttpContentTypes.java index 677d31dfee12e6cc57490a5e08e32e4b4083d61c..6b94b360528cd7c0ec5acd78aeedd6869b449634 100644 --- a/src/main/java/io/jboot/app/HttpContentTypes.java +++ b/src/main/java/io/jboot/app/HttpContentTypes.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2019, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -30,26 +30,32 @@ public class HttpContentTypes { private static Map mappings = new HashMap<>(); static { + /** * 视频相关 */ mappings.put("asf", "video/x-ms-asf"); mappings.put("asx", "video/x-ms-asf"); mappings.put("avi", "video/avi"); - mappings.put("mp4", "video/mpeg4"); - mappings.put("mpeg", "video/mpg"); + mappings.put("flv", "video/x-flv"); + mappings.put("mp4", "video/mp4"); + mappings.put("mpeg", "video/mpeg"); mappings.put("mps", "video/x-mpeg"); mappings.put("mpv", "video/mpg"); + mappings.put("mov", "video/quicktime"); mappings.put("mpa", "video/x-mpg"); mappings.put("mpe", "video/x-mpg"); mappings.put("m4e", "video/mpeg4"); mappings.put("m2v", "video/x-mpeg"); + mappings.put("wmv", "video/x-ms-wmv"); + mappings.put("3gp", "video/3gpp"); + mappings.put("ts", "video/MP2T"); /** * 音频相关 */ - mappings.put("mp3", "audio/mp3"); + mappings.put("mp3", "audio/mpeg"); mappings.put("mp2", "audio/mp2"); mappings.put("m3u", "audio/x-mpegurl"); mappings.put("m3u8", "audio/x-mpegurl"); @@ -59,15 +65,29 @@ public class HttpContentTypes { mappings.put("wav", "audio/wav"); mappings.put("wax", "audio/x-ms-wax"); mappings.put("wma", "audio/x-ms-wma"); + + /** + * 文档相关 + */ + mappings.put("pdf", "application/pdf"); + mappings.put("xml", "application/xml"); + mappings.put("json", "application/json"); + mappings.put("doc", "application/msword"); + mappings.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + mappings.put("xls", "application/vnd.ms-excel"); + mappings.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + mappings.put("pot", "application/vnd.ms-powerpoint"); + mappings.put("ppt", "application/vnd.ms-powerpoint"); + mappings.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"); } /** * 让 undertow 支持音视频格式文件在线播放 */ public static void init(DeploymentInfo di) { - for (Map.Entry entry : mappings.entrySet()){ - di.addMimeMapping(new MimeMapping(entry.getKey(),entry.getValue())); - } + for (Map.Entry entry : mappings.entrySet()) { + di.addMimeMapping(new MimeMapping(entry.getKey(), entry.getValue())); + } } } diff --git a/src/main/java/io/jboot/app/JbootApplication.java b/src/main/java/io/jboot/app/JbootApplication.java index 3f33ff62835da8b642118d615f062bd3d653d1a2..804e656e1b189e5adc7c2c679d9902442ae43786 100644 --- a/src/main/java/io/jboot/app/JbootApplication.java +++ b/src/main/java/io/jboot/app/JbootApplication.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,11 @@ import com.jfinal.server.undertow.WebBuilder; import io.jboot.app.config.JbootConfigManager; import io.jboot.app.undertow.JbootUndertowConfig; import io.jboot.app.undertow.JbootUndertowServer; +import io.jboot.utils.StrUtil; +import io.undertow.Undertow; import javax.servlet.DispatcherType; -import java.net.URISyntaxException; -import java.net.URL; +import java.util.function.Consumer; public class JbootApplication { @@ -42,11 +43,16 @@ public class JbootApplication { public static void start(UndertowServer server) { server.start(); - if (isDevMode()) { + String resourceLoaderEnable = JbootConfigManager.me().getConfigValue("jboot.app.resourceLoaderEnable"); + if (ApplicationUtil.isDevMode() && !"false".equalsIgnoreCase(resourceLoaderEnable)) { new JbootResourceLoader().start(); } } + public static void setBootArg(String key, Object value) { + JbootConfigManager.setBootArg(key, value); + } + /** * 创建 Undertow 服务器,public 用于可以给第三方创建创建着急的 Server * @@ -54,8 +60,8 @@ public class JbootApplication { * @return 返回 UndertowServer */ public static UndertowServer createServer(String[] args) { - JbootApplicationConfig appConfig = getAppConfig(args); - return createServer(appConfig, createUndertowConfig(appConfig), null); + JbootApplicationConfig appConfig = ApplicationUtil.getAppConfig(args); + return createServer(appConfig, createUndertowConfig(appConfig), null, null); } /** @@ -68,40 +74,52 @@ public class JbootApplication { * @return */ public static UndertowServer createServer(String[] args, JbootWebBuilderConfiger configer) { - JbootApplicationConfig appConfig = getAppConfig(args); + JbootApplicationConfig appConfig = ApplicationUtil.getAppConfig(args); return createServer(appConfig, createUndertowConfig(appConfig), configer); } + public static UndertowServer createServer(String[] args, JbootWebBuilderConfiger configer, Consumer builder) { + JbootApplicationConfig appConfig = ApplicationUtil.getAppConfig(args); + return createServer(appConfig, createUndertowConfig(appConfig), configer, builder); + } + + + public static UndertowServer createServer(String[] args, Consumer builder) { + JbootApplicationConfig appConfig = ApplicationUtil.getAppConfig(args); + return createServer(appConfig, createUndertowConfig(appConfig), null, builder); + } + + public static UndertowServer createServer(JbootApplicationConfig appConfig , UndertowConfig undertowConfig , JbootWebBuilderConfiger configer) { + return createServer(appConfig, undertowConfig, configer, null); + } + - printBannerInfo(appConfig); - printApplicationInfo(appConfig); - printClassPath(); + public static UndertowServer createServer(JbootApplicationConfig appConfig + , UndertowConfig undertowConfig + , JbootWebBuilderConfiger configer + , Consumer builder) { + ApplicationUtil.printBannerInfo(appConfig); + ApplicationUtil.printApplicationInfo(appConfig); + ApplicationUtil.printClassPath(); return new JbootUndertowServer(undertowConfig) - .setDevMode(isDevMode()) .configWeb(webBuilder -> { + tryAddContenTypes(webBuilder); tryAddMetricsSupport(webBuilder); tryAddShiroSupport(webBuilder); tryAddWebSocketSupport(webBuilder); if (configer != null) { configer.onConfig(webBuilder); } - }); + }).onStart(builder); } - - - public static JbootApplicationConfig getAppConfig(String[] args) { - JbootConfigManager.me().parseArgs(args); - return getConfig(JbootApplicationConfig.class); - } - public static UndertowConfig createUndertowConfig(JbootApplicationConfig appConfig) { UndertowConfig undertowConfig = new JbootUndertowConfig(appConfig.getJfinalConfig()); undertowConfig.addSystemClassPrefix("io.jboot.app"); @@ -110,9 +128,14 @@ public class JbootApplication { } + private static void tryAddContenTypes(WebBuilder webBuilder) { + HttpContentTypes.init(webBuilder.getDeploymentInfo()); + } + + private static void tryAddMetricsSupport(WebBuilder webBuilder) { - String url = getConfigValue("jboot.metric.url"); - String reporter = getConfigValue("jboot.metric.reporter"); + String url = ApplicationUtil.getConfigValue("jboot.metric.adminServletMapping"); + String reporter = ApplicationUtil.getConfigValue("jboot.metric.reporter"); if (url != null && reporter != null) { webBuilder.addServlet("MetricsAdminServlet", "com.codahale.metrics.servlets.AdminServlet") .addServletMapping("MetricsAdminServlet", url.endsWith("/*") ? url : url + "/*"); @@ -123,21 +146,25 @@ public class JbootApplication { private static void tryAddShiroSupport(WebBuilder webBuilder) { - String iniConfig = getConfigValue("jboot.shiro.ini"); + String iniConfig = ApplicationUtil.getConfigValue("jboot.shiro.ini"); if (iniConfig != null) { - String urlMapping = getConfigValue("jboot.shiro.urlMapping"); + String urlMapping = ApplicationUtil.getConfigValue("jboot.shiro.urlMapping"); if (urlMapping == null) { urlMapping = "/*"; } + String filterClass = StrUtil.defaultIfBlank(ApplicationUtil.getConfigValue("jboot.shiro.filter"), + "io.jboot.support.shiro.JbootShiroFilter"); + webBuilder.addListener("org.apache.shiro.web.env.EnvironmentLoaderListener"); - webBuilder.addFilter("shiro", "io.jboot.support.shiro.JbootShiroFilter") + webBuilder.addFilter("shiro", filterClass) .addFilterUrlMapping("shiro", urlMapping, DispatcherType.REQUEST); - + webBuilder.getDeploymentInfo().addInitParameter("shiroEnvironmentClass", + "io.jboot.support.shiro.JbootShiroWebEnvironment"); } } private static void tryAddWebSocketSupport(WebBuilder webBuilder) { - String websocketEndpoint = getConfigValue("jboot.web.webSocketEndpoint"); + String websocketEndpoint = ApplicationUtil.getConfigValue("jboot.web.webSocketEndpoint"); if (websocketEndpoint != null && websocketEndpoint.trim().length() > 0) { String[] classStrings = websocketEndpoint.split(","); for (String c : classStrings) { @@ -147,48 +174,4 @@ public class JbootApplication { } - private static void printBannerInfo(JbootApplicationConfig appConfig) { - if (appConfig.isBannerEnable()) { - System.out.println(); - System.out.println(Banner.getText(appConfig.getBannerFile())); - System.out.println(); - } - } - - private static void printApplicationInfo(JbootApplicationConfig appConfig) { - System.out.println(appConfig.toString()); - } - - private static void printClassPath() { - try { - URL resourceURL = JbootApplication.class.getResource("/"); - if (resourceURL != null) { - System.out.println("Classpath : " + resourceURL.toURI().getPath()); - } else { - System.out.println("Classpath : application in one jar."); - } - } catch (URISyntaxException e) { - e.printStackTrace(); - } - } - - - private static T getConfig(Class clazz) { - return JbootConfigManager.me().get(clazz); - } - - private static String getConfigValue(String key) { - return JbootConfigManager.me().getConfigValue(key); - } - - - public static void setBootArg(String key, Object value) { - JbootConfigManager.me().setBootArg(key, value); - } - - private static boolean isDevMode() { - return JbootConfigManager.me().isDevMode(); - } - - } diff --git a/src/main/java/io/jboot/app/JbootApplicationConfig.java b/src/main/java/io/jboot/app/JbootApplicationConfig.java index 774d730752cf8c31efdbf4c4f9739a0501cc8b9e..bd35000576d4b5aed4b8e843b301f16fa0c63dc6 100644 --- a/src/main/java/io/jboot/app/JbootApplicationConfig.java +++ b/src/main/java/io/jboot/app/JbootApplicationConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package io.jboot.app; import io.jboot.JbootConsts; +import io.jboot.app.config.JbootConfigManager; import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.utils.StrUtil; @ConfigModel(prefix = "jboot.app") public class JbootApplicationConfig { @@ -27,6 +29,10 @@ public class JbootApplicationConfig { private boolean bannerEnable = true; private String bannerFile = "banner.txt"; private String jfinalConfig = "io.jboot.core.JbootCoreConfig"; + private String listener = "*"; + private String listenerPackage = "*"; + private boolean handle404 = true; + private String proxy; //cglib or javassist public String getMode() { @@ -69,13 +75,66 @@ public class JbootApplicationConfig { this.jfinalConfig = jfinalConfig; } + public String getListener() { + return listener; + } + + public void setListener(String listener) { + this.listener = listener; + } + + public String getListenerPackage() { + return listenerPackage; + } + + public void setListenerPackage(String listenerPackage) { + this.listenerPackage = listenerPackage; + } + + public boolean isHandle404() { + return handle404; + } + + public void setHandle404(boolean handle404) { + this.handle404 = handle404; + } + + public String getProxy() { + if (StrUtil.isBlank(proxy)) { + proxy = initProxy(); + } + return proxy; + } + + + private String initProxy() { + ///cglib javassist + return JdkUtil.isJdk11To19() ? "javassist" : "cglib"; + } + + public void setProxy(String proxy) { + this.proxy = proxy; + } + + private static JbootApplicationConfig instance; + + public static JbootApplicationConfig get() { + if (instance == null) { + instance = JbootConfigManager.me().get(JbootApplicationConfig.class); + } + return instance; + } + @Override public String toString() { return "JbootApplication {" + " name='" + name + '\'' + ", mode='" + mode + '\'' + ", version='" + version + '\'' + - ", jfinalConfig='" + jfinalConfig + '\'' + + ", proxy='" + getProxy() + '\'' + +// ", config='" + jfinalConfig + '\'' + + ", listener='" + listener + '\'' + + ", listenerPackage='" + listenerPackage + '\'' + " }"; } } diff --git a/src/main/java/io/jboot/app/JbootResourceLoader.java b/src/main/java/io/jboot/app/JbootResourceLoader.java index 965e78030189b2157c353868c7c677b18be4f918..cf415699e4343da56e5b42a0ce467e5ec2b92fbc 100644 --- a/src/main/java/io/jboot/app/JbootResourceLoader.java +++ b/src/main/java/io/jboot/app/JbootResourceLoader.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,8 +33,8 @@ public class JbootResourceLoader { private List scanners = new ArrayList<>(); public JbootResourceLoader() { - String configResourcePathName = JbootConfigManager.me().getConfigValue("jboot.app.resourcePathName"); - this.resourcePathName = StrUtil.obtainDefaultIfBlank(configResourcePathName, "webapp"); + String pathName = JbootConfigManager.me().getConfigValue("jboot.app.resourcePathName"); + this.resourcePathName = StrUtil.obtainDefault(pathName, "webapp"); } public JbootResourceLoader(String resourcePathName) { @@ -46,26 +46,28 @@ public class JbootResourceLoader { try { URL url = JbootResourceLoader.class.getClassLoader().getResource(""); - if (url == null) { + if (url == null || url.toString().endsWith(".jar!/")) { return; } String classPath = url.toURI().getPath(); File srcRootPath = new File(classPath, "../..").getCanonicalFile(); - if (new File(srcRootPath.getParent(), "pom.xml").exists()) { + while (new File(srcRootPath.getParent(), "pom.xml").exists()) { srcRootPath = srcRootPath.getParentFile(); } List resourcesDirs = new ArrayList<>(); findResourcesPath(srcRootPath, resourcesDirs); + String targetPath = classPath.endsWith("/config") + ? new File(classPath,"..").getCanonicalPath() : classPath; for (File resourcesDir : resourcesDirs) { - startNewScanner(resourcesDir.getCanonicalFile(), classPath); + startNewScanner(resourcesDir.getCanonicalFile(), targetPath); } - Runtime.getRuntime().addShutdownHook(new Thread(() -> JbootResourceLoader.this.stop())); - System.err.println("JbootResourceLoader started, Watched resource path name : " + resourcePathName); + Runtime.getRuntime().addShutdownHook(new Thread(JbootResourceLoader.this::stop)); + System.err.println("JbootResourceLoader is started, and watching resource dir: " + resourcePathName); } catch (Exception e) { e.printStackTrace(); @@ -73,12 +75,12 @@ public class JbootResourceLoader { } public void stop() { - scanners.forEach(fileScanner -> fileScanner.stop()); - System.out.println("JbootResourceLoader stoped ......"); + scanners.forEach(FileScanner::stop); + System.out.println("JbootResourceLoader has stopped."); } private void findResourcesPath(File root, List resourcesDirs) { - File[] dirs = root.listFiles(pathname -> pathname.isDirectory()); + File[] dirs = root.listFiles(File::isDirectory); if (dirs == null || dirs.length == 0) { return; } @@ -90,7 +92,7 @@ public class JbootResourceLoader { } if (dir.getName().equals(resourcePathName) - && parentFile.getName().equals("main")) { + && "main".equals(parentFile.getName())) { resourcesDirs.add(dir); } else { findResourcesPath(dir, resourcesDirs); @@ -109,21 +111,28 @@ public class JbootResourceLoader { return; } + //忽略掉 windows 和 mac 下的临时文件 + if(file.endsWith("~") || file.endsWith(".DS_Store")){ + return; + } + int indexOf = file.indexOf(path); File target = new File(classPath, resourcePathName + File.separator + file.substring(indexOf + path.length())); - System.err.println("JbootResourceLoader " + action + " : " + target); + System.err.println("JbootResourceLoader " + action + ": " + target); //文件删除 if (FileScanner.ACTION_DELETE.equals(action)) { - target.delete(); + if (!target.delete()){ + System.err.println("JbootResourceLoader can not delete file: " + target); + } } //新增文件 或 修改文件 else { try { FileUtils.copyFile(new File(file), target); } catch (IOException e) { - e.printStackTrace(); + System.err.println("JbootResourceLoader copy file error: " + e.getMessage()); } } } diff --git a/src/main/java/io/jboot/app/JbootSimpleApplication.java b/src/main/java/io/jboot/app/JbootSimpleApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..f5bacdd8adfbd9777305da6641f007b4fac5e859 --- /dev/null +++ b/src/main/java/io/jboot/app/JbootSimpleApplication.java @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.app; + +import com.jfinal.config.Interceptors; +import com.jfinal.config.Plugins; +import com.jfinal.core.JFinal; +import com.jfinal.plugin.IPlugin; +import io.jboot.app.config.JbootConfigManager; +import io.jboot.core.JbootCoreConfig; + +import java.lang.reflect.Method; +import java.text.DecimalFormat; +import java.util.List; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/24 + */ +public class JbootSimpleApplication { + + private static final ReentrantLock LOCK = new ReentrantLock(); + private static final Condition STOP = LOCK.newCondition(); + + public static void main(String[] args) { + run(args); + } + + public static void setBootArg(String key, Object value) { + JbootConfigManager.setBootArg(key, value); + } + + public static void run(String[] args) { + + long startTimeMillis = System.currentTimeMillis(); + + JbootApplicationConfig appConfig = ApplicationUtil.getAppConfig(args); + ApplicationUtil.printBannerInfo(appConfig); + ApplicationUtil.printApplicationInfo(appConfig); + ApplicationUtil.printClassPath(); + + JbootCoreConfig coreConfig = new JbootCoreConfig(); + new SimpleServer(coreConfig, startTimeMillis).start(); + } + + + static class SimpleServer extends Thread { + + private final JbootCoreConfig coreConfig; + private final long startTimeMillis; + private final Plugins plugins = new Plugins(); + private final Interceptors interceptors = new Interceptors(); + + public SimpleServer(JbootCoreConfig coreConfig, long startTimeMillis) { + this.coreConfig = coreConfig; + this.startTimeMillis = startTimeMillis; + + doInitJFinalPathKit(); + doInitCoreConfig(); + } + + + private void doInitJFinalPathKit() { + try { + Class c = JbootSimpleApplication.class.getClassLoader().loadClass("com.jfinal.kit.PathKit"); + Method setWebRootPath = c.getMethod("setWebRootPath", String.class); + String webRootPath = PathKitExt.getWebRootPath(); + setWebRootPath.invoke(null, webRootPath); + + // ------- + Method setRootClassPath = c.getMethod("setRootClassPath", String.class); + String rootClassPath = PathKitExt.getRootClassPath(); + setRootClassPath.invoke(null, rootClassPath); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void doInitCoreConfig() { + + //constants + coreConfig.configConstant(JFinal.me().getConstants()); + + //aop interceptors + coreConfig.configInterceptor(interceptors); + + //plugins + coreConfig.configPlugin(plugins); + startPlugins(); + + //on start + coreConfig.onStart(); + } + + private void startPlugins() { + List pluginList = plugins.getPluginList(); + if (pluginList == null) { + return; + } + + for (IPlugin plugin : pluginList) { + try { + if (plugin.start() == false) { + String message = "Plugin start error: " + plugin.getClass().getName(); + throw new RuntimeException(message); + } + } catch (Exception e) { + String message = "Plugin start error: " + plugin.getClass().getName() + ". \n" + e.getMessage(); + throw new RuntimeException(message, e); + } + } + } + + @Override + public void run() { + String seconds = new DecimalFormat("#.#").format((System.currentTimeMillis() - startTimeMillis) / 1000F); + System.out.println("JbootApplication has started in " + seconds + " seconds. Welcome To The Jboot World (^_^)\n\n"); + + initShutdownHook(); + startAwait(); + } + + + private void initShutdownHook() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\nJbootApplication shutdown, please wait ...... "); + try { + coreConfig.onStop(); + } catch (Exception e) { + System.out.println("JbootApplication shutdown exception: " + e.toString()); + } + System.out.println("JbootApplication has exited, all services stopped."); + try { + LOCK.lock(); + STOP.signal(); + } finally { + LOCK.unlock(); + } + }, "jboot-simple-application-hook")); + } + + + private void startAwait() { + try { + LOCK.lock(); + STOP.await(); + } catch (InterruptedException e) { + System.out.println("JbootApplication has stopped, interrupted by other thread!"); + } finally { + LOCK.unlock(); + } + } + + + } + +} diff --git a/src/main/java/io/jboot/app/JbootWebBuilderConfiger.java b/src/main/java/io/jboot/app/JbootWebBuilderConfiger.java index f6f8bd12e3611dc2a7f09c19394bb9266e57791b..2b999f3fa46d4d1055549c70132bb25d2f26d53d 100644 --- a/src/main/java/io/jboot/app/JbootWebBuilderConfiger.java +++ b/src/main/java/io/jboot/app/JbootWebBuilderConfiger.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/app/JdkUtil.java b/src/main/java/io/jboot/app/JdkUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..8cec5af00416c9a1888e13b78dbf5a357a63b3ba --- /dev/null +++ b/src/main/java/io/jboot/app/JdkUtil.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.app; + +public class JdkUtil { + + public static boolean isJdk1x() { + //1.8.0_261 + String javaVersion = System.getProperty("java.version"); + return javaVersion != null && javaVersion.startsWith("1."); + } + + + public static boolean isJdk11To19() { + //17.0.1 + String javaVersion = System.getProperty("java.version"); + return javaVersion != null && javaVersion.startsWith("1") + && javaVersion.indexOf(".") == 2; + } + + +} diff --git a/src/main/java/io/jboot/app/PathKitExt.java b/src/main/java/io/jboot/app/PathKitExt.java new file mode 100644 index 0000000000000000000000000000000000000000..ec690310d4e0b1152b91b2f72db298c07e630d88 --- /dev/null +++ b/src/main/java/io/jboot/app/PathKitExt.java @@ -0,0 +1,188 @@ +package io.jboot.app; + + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * 参考 JFinal Undertow 的 PathKitExt,目的是在启动的时候 为 PathKit 配置参数 + */ +public class PathKitExt { + + private static String locationPath = null; // 定位路径 + + private static String rootClassPath = null; + private static String webRootPath = null; + + /** + * 1:获取 PathKitExt 类文件所处 jar 包文件所在的目录,注意在 "非部署" 环境中获取到的 + * 通常是 maven 本地库中的某个目录,因为在开发时项目所依赖的 jar 包在 maven 本地库中 + * 这种情况不能使用 + *

+ * 2:PathKitExt 自身在开发时,也就是未打成 jar 包时,获取到的是 APP_BASE/target/classes + * 这种情况多数不必关心,因为 PathKitExt 在使用时必定处于 jar 包之中 + *

+ * 3:获取到的 locationPath 目录用于生成部署时的 config 目录,该值只会在 "部署" 环境下被获取 + * 也用于生成 webRootPath、rootClassPath,这两个值也只会在 "部署" 时被获取 + * 这样就兼容了部署与非部署两种场景 + *

+ * 注意:该路径尾部的 "/" 或 "\\" 已被去除 + */ + public static String getLocationPath() { + if (locationPath != null) { + return locationPath; + } + + try { + // Class clazz = io.undertow.Undertow.class; // 仅测试用 + Class clazz = PathKitExt.class; + String path = clazz.getProtectionDomain().getCodeSource().getLocation().getPath(); + path = java.net.URLDecoder.decode(path, "UTF-8"); + path = path.trim(); + File file = new File(path); + if (file.isFile()) { + path = file.getParent(); + } + + path = removeSlashEnd(path); // 去除尾部 '/' 或 '\' 字符 + locationPath = path; + + return locationPath; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static String getRootClassPath() { + if (rootClassPath == null) { + rootClassPath = buildRootClassPath(); + } + return rootClassPath; + } + + + private static String buildRootClassPath() { + String classPathDirEndsWith_classes = getClassPathDirEndsWith_classes(); + if (classPathDirEndsWith_classes != null) { + return classPathDirEndsWith_classes; + } + + String path = getLocationPath(); + return processRootClassPath(path); + } + + /** + * 获取以 "classes" 结尾的 class path + */ + private static String getClassPathDirEndsWith_classes() { + String[] classPathDirs = getClassPathDirs(); + if (classPathDirs == null || classPathDirs.length == 0) { + return null; + } + + for (String dir : classPathDirs) { + if (dir != null) { + dir = removeSlashEnd(dir.trim()); + if (dir != null && dir.endsWith("classes")) { + return dir; + } + } + } + + return null; + } + + /** + * 1:开发环境 path 会以 classes 结尾 + *

+ * 2:打包以后的部署环境不会以 classes 结尾,约定一个合理的项目打包结构 + * 暂时约定 APP_BASE/config 为 rootClassPath,因为要读取外部配置文件 + */ + private static String processRootClassPath(String path) { + if (path.endsWith("classes")) { + return path; + } + + if (path.endsWith(File.separatorChar + "lib")) { + path = path.substring(0, path.lastIndexOf(File.separatorChar)); + } + + return new File(path + File.separator + "config").getAbsolutePath(); + } + + public static String removeSlashEnd(String path) { + if (path.endsWith(File.separator)) { + return path.substring(0, path.length() - 1); + } else { + return path; + } + } + + // -------------------------------------------------------------------------------------- + + public static String getWebRootPath() { + if (webRootPath == null) { + webRootPath = buildWebRootPath(); + } + return webRootPath; + } + + private static String buildWebRootPath() { + String classPathDirEndsWith_classes = getClassPathDirEndsWith_classes(); + if (classPathDirEndsWith_classes != null) { + return classPathDirEndsWith_classes; + } + + String path = getLocationPath(); + return processWebRootPath(path); + } + + private static String processWebRootPath(String path) { + if (path.endsWith("classes")) { + return path; + } + + if (path.endsWith(File.separatorChar + "lib")) { + path = path.substring(0, path.lastIndexOf(File.separatorChar)); + } + + return new File(path + File.separator + "webapp").getAbsolutePath(); + } + + // --------- + + private static String[] classPathDirs = null; + + public static String[] getClassPathDirs() { + if (classPathDirs == null) { + classPathDirs = buildClassPathDirs(); + } + return classPathDirs; + } + + private static String[] buildClassPathDirs() { + List list = new ArrayList<>(); + String[] classPathArray = System.getProperty("java.class.path").split(File.pathSeparator); + for (String classPath : classPathArray) { + classPath = classPath.trim(); + + if (classPath.startsWith("./")) { + classPath = classPath.substring(2); + } + + File file = new File(classPath); + if (file.exists() && file.isDirectory()) { + // if (!classPath.endsWith("/") && !classPath.endsWith("\\")) { + if (!classPath.endsWith(File.separator)) { + classPath = classPath + File.separator; // append postfix char "/" + } + + list.add(classPath); + } + } + return list.toArray(new String[list.size()]); + } + +} diff --git a/src/main/java/io/jboot/app/config/ConfigPara.java b/src/main/java/io/jboot/app/config/ConfigPara.java new file mode 100644 index 0000000000000000000000000000000000000000..9e5721cf656926745aa7387e456136a2b0810ac9 --- /dev/null +++ b/src/main/java/io/jboot/app/config/ConfigPara.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.app.config; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/24 + */ +public class ConfigPara { + + private int start = 0; + private int end = 0; + private StringBuilder keyStringBuilder = null; + private StringBuilder defaultValueStringBuilder = null; + + + public String getKey() { + if (keyStringBuilder == null) { + return null; + } + return keyStringBuilder.toString(); + } + + public String getDefaultValue() { + if (defaultValueStringBuilder == null) { + return ""; + } + return defaultValueStringBuilder.toString(); + } + + + void appendToKey(char c) { + if (keyStringBuilder == null) { + keyStringBuilder = new StringBuilder(); + } + keyStringBuilder.append(c); + } + + void appendToDefaultValue(char c) { + if (defaultValueStringBuilder == null) { + defaultValueStringBuilder = new StringBuilder(); + } + defaultValueStringBuilder.append(c); + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + public int getEnd() { + return end; + } + + public void setEnd(int end) { + this.end = end; + } + +} diff --git a/src/main/java/io/jboot/app/config/JbootConfigChangeListener.java b/src/main/java/io/jboot/app/config/JbootConfigChangeListener.java index 68a77d9748e1f777b0c7b29e0137d9e2c8505724..6d98aae9cdbed7da9bd56c6a9bb601cbc07b1576 100644 --- a/src/main/java/io/jboot/app/config/JbootConfigChangeListener.java +++ b/src/main/java/io/jboot/app/config/JbootConfigChangeListener.java @@ -1,11 +1,26 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jboot.app.config; /** * @author michael yang (fuhai999@gmail.com) * @Date: 2020/2/8 */ -public interface JbootConfigChangeListener { +public interface JbootConfigChangeListener { - public void onChange(T newObj,T oldObj); + public void onChange(String changeKey, String newValue, String oldValue); } diff --git a/src/main/java/io/jboot/app/config/JbootConfigDecryptor.java b/src/main/java/io/jboot/app/config/JbootConfigDecryptor.java index 504c8ad1b2d93901bd6e252df9d9794090841f33..20592487eb7a11efc4a8aad4e3d9c3ffb5f60a47 100644 --- a/src/main/java/io/jboot/app/config/JbootConfigDecryptor.java +++ b/src/main/java/io/jboot/app/config/JbootConfigDecryptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ package io.jboot.app.config; /** - * Jboot Config 的内容解密器,加密方式由可客户自己编写的加密算法来加密 + * Jboot Config 的内容解密器 + * + * value 值的加密方式,由用户自己编写的加密算法来加密 *

- * 此时,只需要给 JbootConfigManager 配置上 Decryptor 进行解密即可 + * 此时,要正确读取 value 的内容,需要给 JbootConfigManager 配置上 Decryptor 进行解密 */ public interface JbootConfigDecryptor { diff --git a/src/main/java/io/jboot/app/config/JbootConfigKit.java b/src/main/java/io/jboot/app/config/JbootConfigKit.java new file mode 100644 index 0000000000000000000000000000000000000000..6829752d1dea50ec89759969f2e4ab2ede234781 --- /dev/null +++ b/src/main/java/io/jboot/app/config/JbootConfigKit.java @@ -0,0 +1,310 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.app.config; + + +import java.io.File; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class JbootConfigKit { + + + public static T newInstance(Class clazz) { + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static List parseParas(String string) { + if (isBlank(string)) { + return null; + } + + List paras = null; + ConfigPara para = null; + int index = 0; + boolean hasDefaultValue = false; + char[] chars = string.toCharArray(); + for (char c : chars) { + //第一个字符是 '{' 会出现 ArrayIndexOutOfBoundsException 错误 + if (c == '{' && index > 0 && chars[index - 1] == '$' && para == null) { + para = new ConfigPara(); + hasDefaultValue = false; + para.setStart(index - 1); + } else if (c == '}' && para != null) { + para.setEnd(index); + if (paras == null) { + paras = new LinkedList<>(); + } + paras.add(para); + para = null; + } else if (para != null) { + if (c == ':' && !hasDefaultValue) { + hasDefaultValue = true; + } else if (hasDefaultValue) { + para.appendToDefaultValue(c); + } else { + para.appendToKey(c); + } + } + index++; + } + return paras; + } + + + public static String parseValue(String value) { + return parseValue(JbootConfigManager.me(), value); + } + + public static String parseValue(JbootConfigManager manager, String value) { + List paras = parseParas(value); + if (paras == null || paras.size() == 0) { + return value; + } + StringBuilder retBuilder = new StringBuilder(value.length()); + int index = 0; + for (ConfigPara para : paras) { + if (para.getStart() > index) { + retBuilder.append(value, index, para.getStart()); + } + + String configValue = manager.getConfigValue(para.getKey()); + configValue = isNotBlank(configValue) ? configValue : para.getDefaultValue(); + retBuilder.append(configValue); + index = para.getEnd() + 1; + } + + if (index < value.length()) { + retBuilder.append(value, index, value.length()); + } + + return retBuilder.toString(); + } + + + public static List getClassSetMethods(Class clazz) { + List setMethods = new ArrayList<>(); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.getName().startsWith("set") + && method.getName().length() > 3 + && Character.isUpperCase(method.getName().charAt(3)) + && method.getParameterCount() == 1 + && Modifier.isPublic(method.getModifiers()) + && !Modifier.isStatic(method.getModifiers())) { + + setMethods.add(method); + } + } + return setMethods; + } + + public static String firstCharToLowerCase(String str) { + char firstChar = str.charAt(0); + if (firstChar >= 'A' && firstChar <= 'Z') { + char[] arr = str.toCharArray(); + arr[0] += ('a' - 'A'); + return new String(arr); + } + return str; + } + + public static boolean isBlank(String str) { + if (str == null) { + return true; + } + + for (int i = 0, len = str.length(); i < len; i++) { + if (str.charAt(i) > ' ') { + return false; + } + } + return true; + } + + public static boolean isNotBlank(Object str) { + return str != null && !isBlank(str.toString()); + } + + public static boolean areNotBlank(String... strs) { + if (strs == null || strs.length == 0) { + return false; + } + + for (String string : strs) { + if (isBlank(string)) { + return false; + } + } + return true; + } + + + static Properties readProperties(String fileName) { + return readProperties(null,fileName); + } + + static Properties readProperties(String path, String fileName) { + fileName = appendSuffixIfNecessary(fileName); + if (path != null && path.trim().length() > 0) { + return new JbootProp(new File(path, fileName)).getProperties(); + } else { + return new JbootProp(fileName).getProperties(); + } + } + + + static Properties readExternalProperties(String fileName) { + fileName = appendSuffixIfNecessary(fileName); + String jarPath = JbootConfigKit.class.getProtectionDomain().getCodeSource().getLocation().getFile(); + File parentPath = new File(jarPath).getParentFile(); + File externalProperties = new File(parentPath, fileName); + return readPropertiesFile(externalProperties); + } + + + private static String appendSuffixIfNecessary(String fileName) { + fileName = fileName.trim(); + return fileName.toLowerCase().endsWith(".properties") ? fileName : fileName + ".properties"; + } + + + static Properties readPropertiesFile(File propFile) { + if (propFile.exists()) { + return new JbootProp(propFile).getProperties(); + } + return new Properties(); + } + + + public static Object convert(Class convertClass, String s, Type genericType) { + + if (convertClass == String.class || s == null || convertClass == Object.class) { + return s; + } + + if (convertClass == Integer.class || convertClass == int.class) { + return Integer.parseInt(s); + } else if (convertClass == Long.class || convertClass == long.class) { + return Long.parseLong(s); + } else if (convertClass == Double.class || convertClass == double.class) { + return Double.parseDouble(s); + } else if (convertClass == Float.class || convertClass == float.class) { + return Float.parseFloat(s); + } else if (convertClass == Boolean.class || convertClass == boolean.class) { + String value = s.toLowerCase(); + if ("1".equals(value) || "true".equals(value)) { + return Boolean.TRUE; + } else if ("0".equals(value) || "false".equals(value)) { + return Boolean.FALSE; + } else { + throw new RuntimeException("Can not parse to boolean type of value: " + s); + } + } else if (convertClass == java.math.BigDecimal.class) { + return new java.math.BigDecimal(s); + } else if (convertClass == java.math.BigInteger.class) { + return new java.math.BigInteger(s); + } else if (convertClass == byte[].class) { + return s.getBytes(); + } else if (Map.class.isAssignableFrom(convertClass)) { + if (!s.contains(":") || !genericClassCheck(genericType)) { + return null; + } else { + Map map = convertClass == ConcurrentHashMap.class ? new ConcurrentHashMap() : new HashMap(); + String[] strings = s.split(","); + for (String kv : strings) { + int indexOf = kv.indexOf(":"); + if (indexOf > 0 && indexOf < kv.trim().length() - 1) { + map.put(kv.substring(0, indexOf).trim(), kv.substring(indexOf + 1).trim()); + } + } + return map; + } + } else if (List.class.isAssignableFrom(convertClass)) { + if (genericClassCheck(genericType)) { + List list = LinkedList.class == convertClass ? new LinkedList() : new ArrayList(); + String[] strings = s.split(","); + for (String s1 : strings) { + if (s1.trim().length() > 0) { + list.add(s1.trim()); + } + } + return list; + } else { + return null; + } + } else if (Set.class.isAssignableFrom(convertClass)) { + if (genericClassCheck(genericType)) { + Set set = LinkedHashSet.class == convertClass ? new LinkedHashSet() : new HashSet(); + String[] strings = s.split(","); + for (String s1 : strings) { + if (s1.trim().length() > 0) { + set.add(s1.trim()); + } + } + return set; + } else { + return null; + } + } else if (convertClass.isArray() && convertClass.getComponentType() == String.class) { + List list = new LinkedList(); + String[] strings = s.split(","); + if (strings.length > 0) { + for (String s1 : strings) { + if (s1 != null && s1.trim().length() != 0) { + list.add(s1.trim()); + } + } + } + return list.toArray(new String[0]); + } else if (Class.class == convertClass) { + try { + return Class.forName(s, false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + + throw new RuntimeException(convertClass.getName() + " can not be converted, please use other type in your config class!"); + + } + + /** + * 对泛型类型进行检测,只支持 String 类型的泛型,或者不是泛型才会支持 + * + * @param type + * @return + */ + private static boolean genericClassCheck(Type type) { + if (type instanceof ParameterizedType) { + for (Type at : ((ParameterizedType) type).getActualTypeArguments()) { + if (String.class != at) { + return false; + } + } + } + return true; + } + +} diff --git a/src/main/java/io/jboot/app/config/JbootConfigManager.java b/src/main/java/io/jboot/app/config/JbootConfigManager.java index eac9d9598829b40b86a4bbb38141b9216a0b9016..d9c2ebd65f476697a117c0e1b43ffecd8c6122d5 100644 --- a/src/main/java/io/jboot/app/config/JbootConfigManager.java +++ b/src/main/java/io/jboot/app/config/JbootConfigManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,11 @@ */ package io.jboot.app.config; -import com.jfinal.kit.LogKit; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.app.config.support.apollo.ApolloConfigManager; +import io.jboot.app.config.support.nacos.NacosConfigManager; import java.io.File; import java.lang.reflect.Method; @@ -36,12 +39,12 @@ public class JbootConfigManager { //分布式配置 private Map remoteProperties; -// private List readers; + //ConfigObje 缓存 class + prefix : object private Map configCache = new ConcurrentHashMap<>(); - private Map changeListenerMap = new ConcurrentHashMap<>(); - private Map listenerClassMapping = new ConcurrentHashMap<>(); + //监听器 + private Multimap listenersMultimap = ArrayListMultimap.create(); //配置内容解密工具 private JbootConfigDecryptor decryptor; @@ -62,24 +65,67 @@ public class JbootConfigManager { private void init() { + String fileName = getConfigValue(null, "jboot_properties_name"); + if (fileName == null || fileName.length() == 0) { + fileName = "jboot"; + } - File jbootPropertiesFile = new File(Utils.getRootClassPath(), "jboot.properties"); - if (!jbootPropertiesFile.exists()) { - mainProperties = new Properties(); - } else { - mainProperties = new Prop("jboot.properties").getProperties(); + String pathName = getConfigValue(null, "jboot_properties_path"); + mainProperties = JbootConfigKit.readProperties(pathName, fileName); + + + //可以直接在 默认目录下的 jboot.properties 再次指定外部目录 + String newFileName = getConfigValue(mainProperties, "jboot_properties_name"); + if (newFileName != null && newFileName.length() > 0 && "jboot".equals(fileName)) { + fileName = newFileName; + } + + String newPathName = getConfigValue(mainProperties, "jboot_properties_path"); + if (newPathName != null && newPathName.length() > 0 && (pathName == null || pathName.length() == 0)) { + pathName = newPathName; } + + //配置了 pathName,需要再去 path 读取 jboot.properties 文件 + if (pathName != null && pathName.length() > 0) { + Properties newMainProperties = JbootConfigKit.readProperties(pathName, fileName); + mainProperties.putAll(newMainProperties); + } + + String mode = getConfigValue("jboot.app.mode"); + if (JbootConfigKit.isNotBlank(mode)) { + //开始加载 mode properties + //并全部添加覆盖掉掉 main properties + String modeFileName = fileName + "-" + mode; + Properties modeProperties = JbootConfigKit.readProperties(pathName, modeFileName); - if (Utils.isNotBlank(mode)) { - String p = String.format("jboot-%s.properties", mode); - if (new File(Utils.getRootClassPath(), p).exists()) { - mainProperties.putAll(new Prop(p).getProperties()); - } + mainProperties.putAll(modeProperties); } + + + //通过启动参数 --config=./xxx.properties 来指定配置文件启动 + String configFile = getConfigValue(null, "config"); + Properties configFileProperties = null; + if (configFile != null && configFile.startsWith("./")) { + configFileProperties = JbootConfigKit.readExternalProperties(configFile.substring(2)); + } else if (configFile != null) { + configFileProperties = JbootConfigKit.readPropertiesFile(new File(configFile)); + } else { + //通过在 fatjar 的相同目录下,创建 jboot.properties 配置文件来启动 + configFileProperties = JbootConfigKit.readExternalProperties(fileName); + } + + if (configFileProperties != null && !configFileProperties.isEmpty()) { + mainProperties.putAll(configFileProperties); + } + + + NacosConfigManager.me().init(this); + ApolloConfigManager.me().init(this); } + public JbootConfigDecryptor getDecryptor() { return decryptor; } @@ -89,11 +135,11 @@ public class JbootConfigManager { } public T get(Class clazz) { - ConfigModel propertyConfig = clazz.getAnnotation(ConfigModel.class); - if (propertyConfig == null) { + ConfigModel configModel = clazz.getAnnotation(ConfigModel.class); + if (configModel == null) { return get(clazz, null, null); } - return get(clazz, propertyConfig.prefix(), propertyConfig.file()); + return get(clazz, configModel.prefix(), configModel.file()); } @@ -118,12 +164,9 @@ public class JbootConfigManager { Object configObject = configCache.get(clazz.getName() + prefix); if (configObject == null) { - synchronized (clazz) { - if (configObject == null) { - configObject = createConfigObject(clazz, prefix, file); - configCache.put(clazz.getName() + prefix, configObject); - } - } + Object obj = createConfigObject(clazz, prefix, file); + configCache.putIfAbsent(clazz.getName() + prefix, obj); + configObject = configCache.get(clazz.getName() + prefix); } return (T) configObject; @@ -138,11 +181,11 @@ public class JbootConfigManager { * @return */ public T refreshAndGet(Class clazz) { - ConfigModel propertyConfig = clazz.getAnnotation(ConfigModel.class); - if (propertyConfig == null) { + ConfigModel configModel = clazz.getAnnotation(ConfigModel.class); + if (configModel == null) { return refreshAndGet(clazz, null, null); } - return refreshAndGet(clazz, propertyConfig.prefix(), propertyConfig.file()); + return refreshAndGet(clazz, configModel.prefix(), configModel.file()); } @@ -163,24 +206,16 @@ public class JbootConfigManager { return get(clazz, prefix, file); } - private void refreshMainProperties() { - Properties jbootProperties = new Prop("jboot.properties").getProperties(); - if (jbootProperties == null) { - return; - } + private void refreshMainProperties() { - mainProperties.putAll(jbootProperties); + mainProperties.putAll(JbootConfigKit.readProperties("jboot")); - String mode = getConfigValue(jbootProperties, "jboot.app.mode"); - if (Utils.isNotBlank(mode)) { - String p = String.format("jboot-%s.properties", mode); - if (new File(Utils.getRootClassPath(), p).exists()) { - mainProperties.putAll(new Prop(p).getProperties()); - } + String mode = getConfigValue(mainProperties, "jboot.app.mode"); + if (JbootConfigKit.isNotBlank(mode)) { + String modeFileName = "jboot-" + mode; + mainProperties.putAll(JbootConfigKit.readProperties(modeFileName)); } - - } @@ -193,48 +228,39 @@ public class JbootConfigManager { * @param * @return */ - public T createConfigObject(Class clazz, String prefix, String file) { - Object configObject = Utils.newInstance(clazz); - List setMethods = Utils.getClassSetMethods(clazz); - if (setMethods != null) { - for (Method method : setMethods) { - - String key = buildKey(prefix, method); - String value = getConfigValue(key); - - if (Utils.isNotBlank(file) && getClass().getClassLoader().getResource(file) != null) { - try { - Prop prop = new Prop(file); - String filePropValue = getConfigValue(prop.getProperties(), key); - if (Utils.isNotBlank(filePropValue)) { - value = filePropValue; - } - } catch (Throwable ex) { - } + public synchronized T createConfigObject(Class clazz, String prefix, String file) { + T configObject = JbootConfigKit.newInstance(clazz); + for (Method setterMethod : JbootConfigKit.getClassSetMethods(clazz)) { + String key = buildKey(prefix, setterMethod); + String value = getConfigValue(key); + + if (JbootConfigKit.isNotBlank(file)) { + JbootProp prop = new JbootProp(file); + String filePropValue = getConfigValue(prop.getProperties(), key); + if (JbootConfigKit.isNotBlank(filePropValue)) { + value = filePropValue; } + } - try { - if (Utils.isNotBlank(value)) { - Object val = convert(method.getParameterTypes()[0], value); - method.invoke(configObject, val); + if (JbootConfigKit.isNotBlank(value)) { + Object val = JbootConfigKit.convert(setterMethod.getParameterTypes()[0], value, setterMethod.getGenericParameterTypes()[0]); + if (val != null) { + try { + setterMethod.invoke(configObject, val); + } catch (Exception e) { + e.printStackTrace(); } - } catch (Throwable ex) { - ex.printStackTrace(); } } } - return (T) configObject; + return configObject; } - public Object convert(Class type, String s) { - return Utils.convert(type, s); - } - private String buildKey(String prefix, Method method) { - String key = Utils.firstCharToLowerCase(method.getName().substring(3)); - if (Utils.isNotBlank(prefix)) { + String key = JbootConfigKit.firstCharToLowerCase(method.getName().substring(3)); + if (JbootConfigKit.isNotBlank(prefix)) { key = prefix.trim() + "." + key; } return key; @@ -247,13 +273,18 @@ public class JbootConfigManager { public String getConfigValue(Properties properties, String key) { + if (JbootConfigKit.isBlank(key)) { + return null; + } String originalValue = getOriginalConfigValue(properties, key); - return decryptor != null ? decryptor.decrypt(key, originalValue) : originalValue; + String decryptValue = decryptor != null ? decryptor.decrypt(key, originalValue) : originalValue; + String parseValue = JbootConfigKit.parseValue(this, decryptValue); + return parseValue == null || parseValue.trim().length() == 0 ? null : parseValue.trim(); } /** - * 获取值的优先顺序:1、启动配置 2、环境变量 3、properties配置文件 + * 获取值的优先顺序:1、远程配置 2、启动配置 3、环境变量 4、系统属性 5、properties配置文件 * * @param key * @return @@ -265,20 +296,20 @@ public class JbootConfigManager { //优先读取分布式配置内容 if (remoteProperties != null) { value = (String) remoteProperties.get(key); - if (Utils.isNotBlank(value)) { + if (JbootConfigKit.isNotBlank(value)) { return value.trim(); } } //boot arg value = getBootArg(key); - if (Utils.isNotBlank(value)) { + if (JbootConfigKit.isNotBlank(value)) { return value.trim(); } //env value = System.getenv(key); - if (Utils.isNotBlank(value)) { + if (JbootConfigKit.isNotBlank(value)) { return value.trim(); } @@ -287,20 +318,22 @@ public class JbootConfigManager { // 例如:jboot.datasource.url 转换为 JBOOT_DATASOURCE_URL String tempKey = key.toUpperCase().replace('.', '_'); value = System.getenv(tempKey); - if (Utils.isNotBlank(value)) { + if (JbootConfigKit.isNotBlank(value)) { return value.trim(); } //system property value = System.getProperty(key); - if (Utils.isNotBlank(value)) { + if (JbootConfigKit.isNotBlank(value)) { return value.trim(); } //user properties - value = (String) properties.get(key); - if (Utils.isNotBlank(value)) { - return value.trim(); + if (properties != null) { + value = (String) properties.get(key); + if (JbootConfigKit.isNotBlank(value)) { + return value.trim(); + } } return null; @@ -346,86 +379,78 @@ public class JbootConfigManager { } - public void setRemoteProperty(String key, String value) { + public synchronized void setRemoteProperty(String key, String value) { if (remoteProperties == null) { - synchronized (this) { - if (remoteProperties == null) { - remoteProperties = new ConcurrentHashMap(); - } - } + remoteProperties = new ConcurrentHashMap(); } remoteProperties.put(key, value); } - public void setRemoteProperties(Map map) { + + public void removeRemoteProperty(String key) { + if (remoteProperties != null) { + remoteProperties.remove(key); + } + } + + + public synchronized void setRemoteProperties(Map map) { if (remoteProperties == null) { - synchronized (this) { - if (remoteProperties == null) { - remoteProperties = new ConcurrentHashMap(); - } - } + remoteProperties = new ConcurrentHashMap(); } remoteProperties.putAll(map); } - public void addConfigChangeListener(JbootConfigChangeListener listener, Class forClass) { - ConfigModel configModel = forClass.getAnnotation(ConfigModel.class); + + public void addConfigChangeListener(JbootConfigChangeListener listener, Class forClass) { + ConfigModel configModel = (ConfigModel) forClass.getAnnotation(ConfigModel.class); if (configModel == null) { throw new IllegalArgumentException("forClass:" + forClass + " has no @ConfigModel annotation"); } - listenerClassMapping.put(listener, forClass); - String prefix = configModel.prefix(); - List setMethods = Utils.getClassSetMethods(forClass); - if (setMethods != null) { - for (Method method : setMethods) { - String key = buildKey(prefix, method); - changeListenerMap.put(key, listener); + List setterMethods = JbootConfigKit.getClassSetMethods(forClass); + if (setterMethods != null) { + for (Method setterMethod : setterMethods) { + String key = buildKey(prefix, setterMethod); + listenersMultimap.put(key, listener); } } } - public void removeConfigChangeListener(JbootConfigChangeListener listener) { - listenerClassMapping.remove(listener); - for (Iterator> it = changeListenerMap.entrySet().iterator(); it.hasNext(); ) { - Map.Entry item = it.next(); - if (item.getValue().equals(listener)) { - it.remove(); - } + public void addConfigChangeListener(JbootConfigChangeListener listener, String... forKeys) { + if (listener == null) { + throw new NullPointerException("listener must not null."); } - } - public void notifyChangeListeners(Set changedKeys) { - if (changedKeys == null || changedKeys.isEmpty()) { - return; + if (forKeys == null || forKeys.length == 0) { + throw new NullPointerException("forKeys must not null or empty."); } - List listeners = new ArrayList<>(); - - for (String key : changedKeys) { - JbootConfigChangeListener listener = changeListenerMap.get(key); - if (listener != null && !listeners.contains(listener)) { - listeners.add(listener); - } + for (String forKey : forKeys) { + listenersMultimap.put(forKey, listener); } + } - for (JbootConfigChangeListener listener : listeners) { - Class forClass = listenerClassMapping.get(listener); - ConfigModel configModel = forClass.getAnnotation(ConfigModel.class); + public void removeConfigChangeListener(JbootConfigChangeListener listener) { + listenersMultimap.entries().removeIf(entry -> entry.getValue() == listener); + } - Object oldObj = configCache.get(forClass.getName() + configModel.prefix()); - Object newObj = createConfigObject(forClass, configModel.prefix(), null); + public void notifyChangeListeners(String key, String newValue, String oldValue) { + if (key == null) { + return; + } + + Collection listeners = listenersMultimap.get(key); + for (JbootConfigChangeListener listener : listeners) { try { - listener.onChange(newObj, oldObj); + listener.onChange(key, newValue, oldValue); } catch (Throwable ex) { - LogKit.error(ex.toString(), ex); - } finally { - configCache.put(forClass.getName() + configModel.prefix(), newObj); + com.jfinal.kit.LogKit.error(ex.toString(), ex); } } } @@ -436,7 +461,7 @@ public class JbootConfigManager { * * @param args */ - public void parseArgs(String[] args) { + public static void parseArgs(String[] args) { if (args == null || args.length == 0) { return; } @@ -451,11 +476,42 @@ public class JbootConfigManager { } } - public void setBootArg(String key, Object value) { + public static void setBootArg(String key, Object value) { if (argMap == null) { argMap = new HashMap<>(); } - argMap.put(key, value.toString()); + if (key == null) { + return; + } + if (value == null || value.toString().trim().length() == 0) { + argMap.remove(key.trim()); + } else { + argMap.put(key.trim(), value.toString().trim()); + } + } + + public static void setBootProperties(Properties properties) { + Objects.requireNonNull(properties, "properties must not be null"); + properties.forEach((o, o2) -> setBootArg(o.toString(), o2)); + } + + + public static void setBootProperties(String propertiesFilePath) { + File file = new File(propertiesFilePath); + if (file.exists()) { + setBootProperties(file); + } else { + setBootProperties(new JbootProp(propertiesFilePath).getProperties()); + } + } + + + public static void setBootProperties(File propertiesFile) { + if (propertiesFile.exists()) { + setBootProperties(new JbootProp(propertiesFile).getProperties()); + } else { + System.err.println("Warning: properties file not exists: " + propertiesFile); + } } /** @@ -480,15 +536,13 @@ public class JbootConfigManager { public boolean isDevMode() { if (devMode == null) { - synchronized (this) { - if (devMode == null) { - String appMode = getConfigValue("jboot.app.mode"); - devMode = (null == appMode || "".equals(appMode.trim()) || "dev".equals(appMode)); - } - } + String appMode = getConfigValue("jboot.app.mode"); + devMode = (null == appMode || "".equals(appMode.trim()) || "dev".equalsIgnoreCase(appMode.trim())); } return devMode; } - + public void setDevMode(Boolean devMode) { + this.devMode = devMode; + } } diff --git a/src/main/java/io/jboot/app/config/JbootProp.java b/src/main/java/io/jboot/app/config/JbootProp.java new file mode 100644 index 0000000000000000000000000000000000000000..1ab53f4163c45e5ad1f5f9bb873bfd89c2eb75a6 --- /dev/null +++ b/src/main/java/io/jboot/app/config/JbootProp.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.app.config; + +import com.jfinal.kit.LogKit; + +import java.io.*; +import java.net.URL; +import java.util.Properties; + + +class JbootProp { + protected Properties properties = null; + private static final String DEFAULT_ENCODING = "UTF-8"; + + public JbootProp(String fileName) { + this(fileName, DEFAULT_ENCODING); + } + + public JbootProp(String fileName, String encoding) { + properties = new Properties(); + InputStream inputStream = null; + try { + inputStream = getResourceAsStreamByCurrentThread(fileName); + + if (inputStream == null) { + inputStream = getResourceAsStreamByClassloader(fileName); + } + + // 当系统未编译的时候,开发环境下的 resources 目录下的 jboot.properties 文件不会自动被 copy 到 target/classes 目录下 + // 此时,需要主动去探测 resources 目录的文件 + if (inputStream == null) { + URL resourceURL = JbootProp.class.getResource("/"); + if (resourceURL != null) { + String classPath = resourceURL.toURI().getPath(); + + if (removeSlashEnd(classPath).endsWith("classes")) { + + //from classes path + File propFile = new File(classPath, fileName); + if (propFile.exists() && propFile.isFile()) { + inputStream = new FileInputStream(propFile); + } + + //from resources path + else { + File resourcesDir = new File(classPath, "../../src/main/resources"); + propFile = new File(resourcesDir, fileName); + if (propFile.exists() && propFile.isFile()) { + inputStream = new FileInputStream(propFile); + } + } + } + } + } + + if (inputStream != null) { + properties.load(new InputStreamReader(inputStream, encoding)); + } else if (!fileName.contains("-")) { + System.err.println("Warning: Can not load properties file in classpath, file name: " + fileName); + } + } catch (Exception e) { + System.err.println("Warning: Can not load properties file in classpath, file name: " + fileName); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + LogKit.logNothing(e); + } + } + } + } + + private InputStream getResourceAsStreamByCurrentThread(String fileName) { + ClassLoader ret = Thread.currentThread().getContextClassLoader(); + return ret != null ? ret.getResourceAsStream(fileName) : null; + } + + + private InputStream getResourceAsStreamByClassloader(String fileName) { + ClassLoader ret = JbootProp.class.getClassLoader(); + return ret != null ? ret.getResourceAsStream(fileName) : null; + } + + + private static String removeSlashEnd(String path) { + if (path != null && (path.endsWith("/") || path.endsWith("\\"))) { + return path.substring(0, path.length() - 1); + } else { + return path; + } + } + + + public JbootProp(File file) { + properties = new Properties(); + try (InputStream inputStream = new FileInputStream(file)) { + properties.load(new InputStreamReader(inputStream, DEFAULT_ENCODING)); + } catch (Exception e) { + System.err.println("Warning: Can not load properties file: " + file); + } + } + + + public Properties getProperties() { + return properties; + } +} diff --git a/src/main/java/io/jboot/app/config/Utils.java b/src/main/java/io/jboot/app/config/Utils.java deleted file mode 100644 index 2e74219c76bc9d3ce1f9708aa0e4d1299e9785d1..0000000000000000000000000000000000000000 --- a/src/main/java/io/jboot/app/config/Utils.java +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.app.config; - - -import java.io.File; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; - -class Utils { - - - public static boolean isBlank(String string) { - return string == null || string.trim().equals(""); - } - - public static boolean isNotBlank(String string) { - return !isBlank(string); - } - - public static T newInstance(Class clazz) { - try { - Constructor constructor = clazz.getDeclaredConstructor(); - constructor.setAccessible(true); - return (T) constructor.newInstance(); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - - public static List getClassSetMethods(Class clazz) { - List setMethods = new ArrayList<>(); - Method[] methods = clazz.getMethods(); - for (Method method : methods) { - if (method.getName().startsWith("set") - && method.getName().length() > 3 - && method.getParameterCount() == 1) { - - setMethods.add(method); - } - } - return setMethods; - } - - public static String firstCharToLowerCase(String str) { - char firstChar = str.charAt(0); - if (firstChar >= 'A' && firstChar <= 'Z') { - char[] arr = str.toCharArray(); - arr[0] += ('a' - 'A'); - return new String(arr); - } - return str; - } - - - private static String rootClassPath; - - public static String getRootClassPath() { - if (rootClassPath == null) { - try { - String path = getClassLoader().getResource("").toURI().getPath(); - rootClassPath = new File(path).getAbsolutePath(); - } catch (Exception e) { - try { - String path = Utils.class.getProtectionDomain().getCodeSource().getLocation().getPath(); - path = java.net.URLDecoder.decode(path, "UTF-8"); - if (path.endsWith(File.separator)) { - path = path.substring(0, path.length() - 1); - } - /** - * Fix path带有文件名 - */ - if (path.endsWith(".jar")) { - path = path.substring(0, path.lastIndexOf("/") + 1); - } - rootClassPath = path; - } catch (UnsupportedEncodingException e1) { - throw new RuntimeException(e1); - } - } - } - return rootClassPath; - } - - - public static ClassLoader getClassLoader() { - ClassLoader ret = Thread.currentThread().getContextClassLoader(); - return ret != null ? ret : Utils.class.getClassLoader(); - } - - public static void doNothing(Throwable ex) { - } - - public static final Object convert(Class type, String s) { - - if (type == String.class) { - return s; - } - - if (type == Integer.class || type == int.class) { - return Integer.parseInt(s); - } else if (type == Long.class || type == long.class) { - return Long.parseLong(s); - } else if (type == Double.class || type == double.class) { - return Double.parseDouble(s); - } else if (type == Float.class || type == float.class) { - return Float.parseFloat(s); - } else if (type == Boolean.class || type == boolean.class) { - String value = s.toLowerCase(); - if ("1".equals(value) || "true".equals(value)) { - return Boolean.TRUE; - } else if ("0".equals(value) || "false".equals(value)) { - return Boolean.FALSE; - } else { - throw new RuntimeException("Can not parse to boolean type of value: " + s); - } - } else if (type == java.math.BigDecimal.class) { - return new java.math.BigDecimal(s); - } else if (type == java.math.BigInteger.class) { - return new java.math.BigInteger(s); - } else if (type == byte[].class) { - return s.getBytes(); - } - - throw new RuntimeException(type.getName() + " can not be converted, please use other type in your config class!"); - - } - -} diff --git a/src/main/java/io/jboot/app/config/annotation/ConfigModel.java b/src/main/java/io/jboot/app/config/annotation/ConfigModel.java index 6d3e394605572bccf0a4c7c50700631936a54c7f..720f7e408ab64aadad81e45a0a408b10fa82be0e 100644 --- a/src/main/java/io/jboot/app/config/annotation/ConfigModel.java +++ b/src/main/java/io/jboot/app/config/annotation/ConfigModel.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/app/config/support/apollo/ApolloConfigManager.java b/src/main/java/io/jboot/app/config/support/apollo/ApolloConfigManager.java index 07e3ef07550ab5350b222f7311756508edeaea30..5bc1ae10484890a6ed3cbb7a990d6caefe51da37 100644 --- a/src/main/java/io/jboot/app/config/support/apollo/ApolloConfigManager.java +++ b/src/main/java/io/jboot/app/config/support/apollo/ApolloConfigManager.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -17,13 +17,16 @@ package io.jboot.app.config.support.apollo; import com.ctrip.framework.apollo.Config; import com.ctrip.framework.apollo.ConfigService; +import com.ctrip.framework.apollo.enums.PropertyChangeType; import com.ctrip.framework.apollo.model.ConfigChange; -import io.jboot.Jboot; +import io.jboot.app.config.JbootConfigKit; import io.jboot.app.config.JbootConfigManager; -import io.jboot.utils.StrUtil; +import java.util.HashMap; +import java.util.Map; import java.util.Set; + /** * @author michael yang (fuhai999@gmail.com) * @Date: 2020/2/8 @@ -36,38 +39,49 @@ public class ApolloConfigManager { return ME; } - public void init() { + public void init(JbootConfigManager configManager) { - ApolloServerConfig apolloServerConfig = Jboot.config(ApolloServerConfig.class); - if (!apolloServerConfig.isEnable() || !apolloServerConfig.isConfigOk()){ + ApolloServerConfig apolloServerConfig = configManager.get(ApolloServerConfig.class); + if (!apolloServerConfig.isEnable() || !apolloServerConfig.isConfigOk()) { return; } - Config config = getDefaultConfig(); + //apollo 配置 + System.setProperty("app.id", apolloServerConfig.getAppId()); + System.setProperty("apollo.meta", apolloServerConfig.getMeta()); + + + Config config = getDefaultConfig(configManager); Set propNames = config.getPropertyNames(); if (propNames != null && !propNames.isEmpty()) { + Map properties = new HashMap(); for (String name : propNames) { String value = config.getProperty(name, null); - JbootConfigManager.me().setRemoteProperty(name, value); + properties.put(name,value); } + configManager.setRemoteProperties(properties); } config.addChangeListener(changeEvent -> { for (String key : changeEvent.changedKeys()) { ConfigChange change = changeEvent.getChange(key); - JbootConfigManager.me().setRemoteProperty(change.getPropertyName(), change.getNewValue()); + if (change.getChangeType() == PropertyChangeType.DELETED) { + configManager.removeRemoteProperty(change.getPropertyName()); + } else { + configManager.setRemoteProperty(change.getPropertyName(), change.getNewValue()); + } + configManager.notifyChangeListeners(change.getPropertyName(), change.getNewValue(), change.getOldValue()); } - - JbootConfigManager.me().notifyChangeListeners(changeEvent.changedKeys()); }); + } - private Config getDefaultConfig() { - ApolloServerConfig apolloServerConfig = Jboot.config(ApolloServerConfig.class); - if (StrUtil.isNotBlank(apolloServerConfig.getDefaultNamespace())) { - return ConfigService.getConfig(apolloServerConfig.getDefaultNamespace()); + private Config getDefaultConfig(JbootConfigManager configManager) { + ApolloServerConfig apolloServerConfig = configManager.get(ApolloServerConfig.class); + if (JbootConfigKit.isNotBlank(apolloServerConfig.getNamespace())) { + return ConfigService.getConfig(apolloServerConfig.getNamespace()); } else { return ConfigService.getAppConfig(); } diff --git a/src/main/java/io/jboot/app/config/support/apollo/ApolloServerConfig.java b/src/main/java/io/jboot/app/config/support/apollo/ApolloServerConfig.java index 3bbdb7f367ab07ffebb4728d664460735c788899..dc0bdfd337e582d184e6cb87496e3080a3568cb4 100644 --- a/src/main/java/io/jboot/app/config/support/apollo/ApolloServerConfig.java +++ b/src/main/java/io/jboot/app/config/support/apollo/ApolloServerConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ */ package io.jboot.app.config.support.apollo; +import io.jboot.app.config.JbootConfigKit; import io.jboot.app.config.annotation.ConfigModel; -import io.jboot.utils.StrUtil; @ConfigModel(prefix = "jboot.config.apollo") public class ApolloServerConfig { @@ -24,7 +24,7 @@ public class ApolloServerConfig { private boolean enable = false; private String meta; private String appId; - private String defaultNamespace; + private String namespace; public boolean isEnable() { @@ -51,15 +51,15 @@ public class ApolloServerConfig { this.meta = meta; } - public String getDefaultNamespace() { - return defaultNamespace; + public String getNamespace() { + return namespace; } - public void setDefaultNamespace(String defaultNamespace) { - this.defaultNamespace = defaultNamespace; + public void setNamespace(String namespace) { + this.namespace = namespace; } public boolean isConfigOk() { - return StrUtil.areNotEmpty(appId, meta); + return JbootConfigKit.areNotBlank(appId, meta); } } diff --git a/src/main/java/io/jboot/app/config/support/nacos/NacosConfigIniter.java b/src/main/java/io/jboot/app/config/support/nacos/NacosConfigInitializer.java similarity index 68% rename from src/main/java/io/jboot/app/config/support/nacos/NacosConfigIniter.java rename to src/main/java/io/jboot/app/config/support/nacos/NacosConfigInitializer.java index a1ec0c3b72282b94ecc0359a41f71e1f179e3336..58d8396ce314905d55b981c09e54f0822345cd4d 100644 --- a/src/main/java/io/jboot/app/config/support/nacos/NacosConfigIniter.java +++ b/src/main/java/io/jboot/app/config/support/nacos/NacosConfigInitializer.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -17,6 +17,7 @@ package io.jboot.app.config.support.nacos; import com.alibaba.nacos.api.config.ConfigService; import com.alibaba.nacos.api.config.listener.Listener; +import io.jboot.app.config.JbootConfigManager; import java.util.concurrent.Executor; @@ -24,19 +25,20 @@ import java.util.concurrent.Executor; * @author michael yang (fuhai999@gmail.com) * @Date: 2020/2/9 */ -public class NacosConfigIniter { +public class NacosConfigInitializer { private NacosConfigManager manager; + private JbootConfigManager configManager; - public NacosConfigIniter(NacosConfigManager manager) { + public NacosConfigInitializer(NacosConfigManager manager, JbootConfigManager configManager) { this.manager = manager; + this.configManager = configManager; } public void initListener(ConfigService configService, NacosServerConfig config) { try { - configService.addListener(config.getDataId() - , config.getGroup() + configService.addListener(config.getDataId(), config.getGroup() , new Listener() { @Override public Executor getExecutor() { @@ -45,7 +47,7 @@ public class NacosConfigIniter { @Override public void receiveConfigInfo(String configInfo) { - manager.doReceiveConfigInfo(configInfo); + manager.onReceiveConfigInfo(configManager, configInfo); } }); } catch (Exception e) { diff --git a/src/main/java/io/jboot/app/config/support/nacos/NacosConfigManager.java b/src/main/java/io/jboot/app/config/support/nacos/NacosConfigManager.java index 7dd6f2e26278c1165a573087cb550010107befe2..03185c984057e852adbbcfc7aa2d2a08caa80b1d 100644 --- a/src/main/java/io/jboot/app/config/support/nacos/NacosConfigManager.java +++ b/src/main/java/io/jboot/app/config/support/nacos/NacosConfigManager.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -17,16 +17,16 @@ package io.jboot.app.config.support.nacos; import com.alibaba.nacos.api.NacosFactory; import com.alibaba.nacos.api.config.ConfigService; -import io.jboot.Jboot; +import com.jfinal.log.Log; +import io.jboot.app.config.JbootConfigKit; import io.jboot.app.config.JbootConfigManager; -import io.jboot.utils.StrUtil; +import io.jboot.utils.ConfigUtil; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.util.HashSet; +import java.io.StringReader; +import java.util.Map; import java.util.Objects; import java.util.Properties; -import java.util.Set; /** * @author michael yang (fuhai999@gmail.com) @@ -34,6 +34,7 @@ import java.util.Set; */ public class NacosConfigManager { + private static final Log LOG = Log.getLog(NacosConfigManager.class); private static final NacosConfigManager ME = new NacosConfigManager(); public static NacosConfigManager me() { @@ -43,74 +44,78 @@ public class NacosConfigManager { private Properties contentProperties; - public void init() { + /** + * 初始化 nacos 配置监听 + */ + public void init(JbootConfigManager configManager) { + Map configModels = ConfigUtil.getConfigModels(configManager, NacosServerConfig.class); + configModels.forEach((s, nacosConfig) -> initConfig(configManager, nacosConfig)); + } - NacosServerConfig nacosServerConfig = Jboot.config(NacosServerConfig.class); - if (!nacosServerConfig.isEnable() || !nacosServerConfig.isConfigOk()) { + private void initConfig(JbootConfigManager configManager, NacosServerConfig nacosServerConfig) { + if (nacosServerConfig == null + || !nacosServerConfig.isEnable() + || !nacosServerConfig.isConfigOk()) { return; } try { - Properties properties = new Properties(); - properties.put("serverAddr", nacosServerConfig.getServerAddr()); - ConfigService configService = NacosFactory.createConfigService(properties); + ConfigService configService = NacosFactory.createConfigService(nacosServerConfig.toProperties()); String content = configService.getConfig(nacosServerConfig.getDataId() , nacosServerConfig.getGroup(), 3000); - if (StrUtil.isNotBlank(content)) { + if (JbootConfigKit.isNotBlank(content)) { contentProperties = str2Properties(content); if (contentProperties != null) { - JbootConfigManager.me().setRemoteProperties(contentProperties); + configManager.setRemoteProperties(contentProperties); } } - new NacosConfigIniter(this).initListener(configService,nacosServerConfig); + new NacosConfigInitializer(this, configManager).initListener(configService, nacosServerConfig); } catch (Exception e) { - e.printStackTrace(); + + LOG.error(e.toString(), e); } } - - public void doReceiveConfigInfo(String configInfo) { + /** + * 接收到 nacos 服务器消息 + * + * @param configManager + * @param configInfo + */ + public void onReceiveConfigInfo(JbootConfigManager configManager, String configInfo) { Properties properties = str2Properties(configInfo); - Set changedKeys = new HashSet<>(); - if (contentProperties == null) { - contentProperties = properties; - - for (Object key : properties.keySet()) { - changedKeys.add(key.toString()); - } - - JbootConfigManager.me().setRemoteProperties(properties); - JbootConfigManager.me().notifyChangeListeners(changedKeys); - - } else { - - for (Object key : properties.keySet()) { - String newValue = properties.getProperty(key.toString()); - String oldValue = contentProperties.getProperty(key.toString()); - - if (!Objects.equals(newValue, oldValue)) { - changedKeys.add(key.toString()); - contentProperties.put(key, newValue); - JbootConfigManager.me().setRemoteProperty(key.toString(), newValue); + if (properties != null) { + if (contentProperties == null) { + contentProperties = properties; + configManager.setRemoteProperties(properties); + } else { + for (Object key : properties.keySet()) { + String newValue = properties.getProperty(key.toString()); + String oldValue = contentProperties.getProperty(key.toString()); + + if (!Objects.equals(newValue, oldValue)) { + contentProperties.put(key, newValue); + configManager.setRemoteProperty(key.toString(), newValue); + + configManager.notifyChangeListeners(key.toString(), newValue, oldValue); + } } } - - JbootConfigManager.me().notifyChangeListeners(changedKeys); } - } + } private Properties str2Properties(String content) { try { Properties properties = new Properties(); - properties.load(new ByteArrayInputStream(content.getBytes())); + properties.load(new StringReader(content)); return properties; } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/java/io/jboot/app/config/support/nacos/NacosServerConfig.java b/src/main/java/io/jboot/app/config/support/nacos/NacosServerConfig.java index 3a76a1047ba3efa429fde80ea388d2b27d583835..39fc58a94ab8efc2e512772e06192f95e50cd2e1 100644 --- a/src/main/java/io/jboot/app/config/support/nacos/NacosServerConfig.java +++ b/src/main/java/io/jboot/app/config/support/nacos/NacosServerConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,38 @@ package io.jboot.app.config.support.nacos; +import io.jboot.app.config.JbootConfigKit; import io.jboot.app.config.annotation.ConfigModel; -import io.jboot.utils.StrUtil; +import java.util.Properties; + +/** + * @see com.alibaba.nacos.api.PropertyKeyConst + */ @ConfigModel(prefix = "jboot.config.nacos") public class NacosServerConfig { private boolean enable = false; + + private String isUseCloudNamespaceParsing; + private String isUseEndpointParsingRule; + private String endpoint; + private String endpointPort; + private String namespace; + private String username; + private String password; + private String accessKey; + private String secretKey; + private String ramRoleName; private String serverAddr; + private String contextPath; + private String clusterName; + private String encode; + private String configLongPollTimeout; + private String configRetryTime; + private String maxRetry; + private String enableRemoteSyncConfig; + private String dataId; private String group; @@ -35,6 +59,86 @@ public class NacosServerConfig { this.enable = enable; } + public String getIsUseCloudNamespaceParsing() { + return isUseCloudNamespaceParsing; + } + + public void setIsUseCloudNamespaceParsing(String isUseCloudNamespaceParsing) { + this.isUseCloudNamespaceParsing = isUseCloudNamespaceParsing; + } + + public String getIsUseEndpointParsingRule() { + return isUseEndpointParsingRule; + } + + public void setIsUseEndpointParsingRule(String isUseEndpointParsingRule) { + this.isUseEndpointParsingRule = isUseEndpointParsingRule; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getEndpointPort() { + return endpointPort; + } + + public void setEndpointPort(String endpointPort) { + this.endpointPort = endpointPort; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getRamRoleName() { + return ramRoleName; + } + + public void setRamRoleName(String ramRoleName) { + this.ramRoleName = ramRoleName; + } + public String getServerAddr() { return serverAddr; } @@ -43,6 +147,62 @@ public class NacosServerConfig { this.serverAddr = serverAddr; } + public String getContextPath() { + return contextPath; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getEncode() { + return encode; + } + + public void setEncode(String encode) { + this.encode = encode; + } + + public String getConfigLongPollTimeout() { + return configLongPollTimeout; + } + + public void setConfigLongPollTimeout(String configLongPollTimeout) { + this.configLongPollTimeout = configLongPollTimeout; + } + + public String getConfigRetryTime() { + return configRetryTime; + } + + public void setConfigRetryTime(String configRetryTime) { + this.configRetryTime = configRetryTime; + } + + public String getMaxRetry() { + return maxRetry; + } + + public void setMaxRetry(String maxRetry) { + this.maxRetry = maxRetry; + } + + public String getEnableRemoteSyncConfig() { + return enableRemoteSyncConfig; + } + + public void setEnableRemoteSyncConfig(String enableRemoteSyncConfig) { + this.enableRemoteSyncConfig = enableRemoteSyncConfig; + } + public String getDataId() { return dataId; } @@ -59,7 +219,37 @@ public class NacosServerConfig { this.group = group; } + public Properties toProperties() { + Properties properties = new Properties(); + putProperties(properties, "isUseCloudNamespaceParsing", isUseCloudNamespaceParsing); + putProperties(properties, "isUseEndpointParsingRule", isUseEndpointParsingRule); + putProperties(properties, "endpoint", endpoint); + putProperties(properties, "endpointPort", endpointPort); + putProperties(properties, "namespace", namespace); + putProperties(properties, "username", username); + putProperties(properties, "password", password); + putProperties(properties, "accessKey", accessKey); + putProperties(properties, "secretKey", secretKey); + putProperties(properties, "ramRoleName", ramRoleName); + putProperties(properties, "serverAddr", serverAddr); + putProperties(properties, "contextPath", contextPath); + putProperties(properties, "clusterName", clusterName); + putProperties(properties, "encode", encode); + putProperties(properties, "configLongPollTimeout", configLongPollTimeout); + putProperties(properties, "configRetryTime", configRetryTime); + putProperties(properties, "maxRetry", maxRetry); + putProperties(properties, "enableRemoteSyncConfig", enableRemoteSyncConfig); + return properties; + } + + private void putProperties(Properties p, String key, String value) { + if (value != null && value.trim().length() > 0) { + p.put(key, value); + } + } + + public boolean isConfigOk() { - return StrUtil.areNotEmpty(serverAddr, dataId, group); + return JbootConfigKit.areNotBlank(serverAddr, dataId, group); } } diff --git a/src/main/java/io/jboot/app/undertow/JbootHotSwapResolver.java b/src/main/java/io/jboot/app/undertow/JbootHotSwapResolver.java index 78d7e3cb7af5bdeeb20de7144f8a14079c411a50..cdb9dad614127c8b48a45df994c96487d752e73e 100644 --- a/src/main/java/io/jboot/app/undertow/JbootHotSwapResolver.java +++ b/src/main/java/io/jboot/app/undertow/JbootHotSwapResolver.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/app/undertow/JbootUndertowConfig.java b/src/main/java/io/jboot/app/undertow/JbootUndertowConfig.java index 99e5f7092b680d53bf743af95a8c258fc82fcf15..7c1ac10997416f730b21f9b5a078d159fb01cb62 100644 --- a/src/main/java/io/jboot/app/undertow/JbootUndertowConfig.java +++ b/src/main/java/io/jboot/app/undertow/JbootUndertowConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.net.ServerSocket; public class JbootUndertowConfig extends UndertowConfig { + protected static final String DEV_MODE = "undertow.devMode"; protected static final String UNDERTOW_PORT = "undertow.port"; protected static final String UNDERTOW_HOST = "undertow.host"; protected static final String UNDERTOW_RESOURCEPATH = "undertow.resourcePath"; @@ -53,21 +54,41 @@ public class JbootUndertowConfig extends UndertowConfig { .append(new PropExt(JbootConfigManager.me().getProperties())); String port = propExt.get(UNDERTOW_PORT); - Integer availablePort = getAvailablePort(); + + //当不配置端口号时,默认为 8080 if (port == null || port.trim().length() == 0) { propExt.getProperties().put(UNDERTOW_PORT, "8080"); - JbootConfigManager.me().setBootArg(UNDERTOW_PORT, "8080"); - } else if (port.trim().equals("*") && availablePort != null) { - propExt.getProperties().put(UNDERTOW_PORT, availablePort.toString()); - JbootConfigManager.me().setBootArg(UNDERTOW_PORT, availablePort.toString()); + JbootConfigManager.setBootArg(UNDERTOW_PORT, "8080"); + } + + //当配置为 -1 或者 * 时,默认为随机端口号 + else if ((port.trim().equals("*") || port.trim().equals("-1"))) { + Integer availablePort = getAvailablePort(); + if (availablePort != null) { + propExt.getProperties().put(UNDERTOW_PORT, availablePort.toString()); + JbootConfigManager.setBootArg(UNDERTOW_PORT, availablePort.toString()); + } + } + + //当不配置 undertow 开发模式时,默认开发模式为 devMode + String devMode = propExt.get(DEV_MODE); + if (devMode == null || devMode.trim().length() == 0) { + propExt.getProperties().put(DEV_MODE, JbootConfigManager.me().isDevMode()); } + //当不配置 host 时,为其配置默认的 host String host = propExt.get(UNDERTOW_HOST); if (host == null || host.trim().length() == 0) { - propExt.getProperties().put(UNDERTOW_HOST, "0.0.0.0"); - JbootConfigManager.me().setBootArg(UNDERTOW_HOST, "0.0.0.0"); + if (isAppDevMode()) { + propExt.getProperties().put(UNDERTOW_HOST, "localhost"); + JbootConfigManager.setBootArg(UNDERTOW_HOST, "localhost"); + } else { + propExt.getProperties().put(UNDERTOW_HOST, "0.0.0.0"); + JbootConfigManager.setBootArg(UNDERTOW_HOST, "0.0.0.0"); + } } + //当不配置资源路径时,默认为 classpath 下的 webapp String resPath = propExt.get(UNDERTOW_RESOURCEPATH); if (resPath == null || resPath.trim().length() == 0) { propExt.getProperties().put(UNDERTOW_RESOURCEPATH, "classpath:webapp," + this.resourcePath); @@ -81,7 +102,7 @@ public class JbootUndertowConfig extends UndertowConfig { * * @return */ - public static Integer getAvailablePort() { + private static Integer getAvailablePort() { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(0); @@ -98,6 +119,13 @@ public class JbootUndertowConfig extends UndertowConfig { return null; } + + private static boolean isAppDevMode() { + String appMode = JbootConfigManager.me().getConfigValue("jboot.app.mode"); + return (null == appMode || "".equals(appMode.trim()) || "dev".equalsIgnoreCase(appMode.trim())); + } + + @Override public HotSwapResolver getHotSwapResolver() { if (hotSwapResolver == null) { diff --git a/src/main/java/io/jboot/app/undertow/JbootUndertowServer.java b/src/main/java/io/jboot/app/undertow/JbootUndertowServer.java index 771c2f3a35d1eca62da159038a772cfdde0bcff7..d8412c2d4cb4ce7d950b8b86b4e4dab0b526747c 100644 --- a/src/main/java/io/jboot/app/undertow/JbootUndertowServer.java +++ b/src/main/java/io/jboot/app/undertow/JbootUndertowServer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,11 @@ package io.jboot.app.undertow; import com.jfinal.server.undertow.UndertowConfig; import com.jfinal.server.undertow.UndertowServer; +import io.jboot.app.config.JbootConfigManager; +import io.undertow.servlet.Servlets; -import javax.servlet.ServletException; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; public class JbootUndertowServer extends UndertowServer { @@ -27,17 +30,27 @@ public class JbootUndertowServer extends UndertowServer { super(undertowConfig); } + /** + * 添加 自定义 filter 的支持 + */ @Override - protected void init() { - super.init(); - - //让 undertow 支持 音视频在线播放 -// HttpContentTypes.init(deploymentInfo); + protected void configJFinalFilter() { + deploymentInfo.addFilter( + Servlets.filter("jfinal", getJFinalFilter()).addInitParam("configClass", config.getJFinalConfig()) + ).addFilterUrlMapping("jfinal", "/*", DispatcherType.REQUEST); } - @Override - protected void doStop() throws ServletException { - super.doStop(); + + private Class getJFinalFilter() { + try { + String jfinalFilter = JbootConfigManager.me().getConfigValue("undertow.jfinalFilter"); + if (jfinalFilter == null || jfinalFilter.trim().length() == 0){ + jfinalFilter = "com.jfinal.core.JFinalFilter"; + } + return (Class)config.getClassLoader().loadClass(jfinalFilter); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } } } diff --git a/src/main/java/io/jboot/codegen/CodeGenHelpler.java b/src/main/java/io/jboot/codegen/CodeGenHelpler.java index 39ece4102c0fcc6609be6ed0195b27cc01681adb..16afe3fa7de221552fd776cc190a2150bea1ff69 100644 --- a/src/main/java/io/jboot/codegen/CodeGenHelpler.java +++ b/src/main/java/io/jboot/codegen/CodeGenHelpler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,11 @@ import java.util.Set; public class CodeGenHelpler { + public static String getUserDir() { + return System.getProperty("user.dir"); + } + + /** * 获取数据源 * @@ -54,10 +59,28 @@ public class CodeGenHelpler { public static MetaBuilder createMetaBuilder() { - MetaBuilder metaBuilder = new MetaBuilder(getDatasource()); + return createMetaBuilder(getDatasource(), Jboot.config(DataSourceConfig.class, "jboot.datasource").getType()); + } + + + public static MetaBuilder createMetaBuilder(DataSource dataSource) { + return createMetaBuilder(dataSource, DataSourceConfig.TYPE_MYSQL); + } + + + public static MetaBuilder createMetaBuilder(DataSource dataSource, String type) { + return createMetaBuilder(dataSource, type, true); + } + + public static MetaBuilder createMetaBuilder(DataSource dataSource, String type, boolean removeNoPrimaryKeyTable) { + MetaBuilder metaBuilder = removeNoPrimaryKeyTable ? new MetaBuilder(dataSource) : new MetaBuilder(dataSource) { + @Override + protected void removeNoPrimaryKeyTable(List ret) { + //do Nothing... + } + }; metaBuilder.setGenerateRemarks(true); - DataSourceConfig datasourceConfig = Jboot.config(DataSourceConfig.class, "jboot.datasource"); - switch (datasourceConfig.getType()) { + switch (type) { case DataSourceConfig.TYPE_MYSQL: metaBuilder.setDialect(new MysqlDialect()); break; @@ -76,12 +99,15 @@ public class CodeGenHelpler { case DataSourceConfig.TYPE_POSTGRESQL: metaBuilder.setDialect(new PostgreSqlDialect()); break; + case DataSourceConfig.TYPE_INFORMIX: + metaBuilder.setDialect(new InformixDialect()); + break; default: - throw new JbootIllegalConfigException("only support datasource type : mysql、orcale、sqlserver、sqlite、ansisql and postgresql, please check your jboot.properties. "); + throw new JbootIllegalConfigException("Only support datasource type: mysql、orcale、sqlserver、sqlite、ansisql、postgresql and infomix" + + ", please check your jboot.properties. "); } return metaBuilder; - } @@ -97,7 +123,7 @@ public class CodeGenHelpler { Set excludeTableSet = StrUtil.splitToSet(excludeTables.toLowerCase(), ","); for (TableMeta tableMeta : list) { if (excludeTableSet.contains(tableMeta.name.toLowerCase())) { - System.out.println("exclude table : " + tableMeta.name); + System.out.println("exclude table: " + tableMeta.name); continue; } newTableMetaList.add(tableMeta); diff --git a/src/main/java/io/jboot/codegen/GenTester.java b/src/main/java/io/jboot/codegen/GenTester.java deleted file mode 100644 index 5cfbe53d1244ddbcb8a45d25b30941bd62cd4cb7..0000000000000000000000000000000000000000 --- a/src/main/java/io/jboot/codegen/GenTester.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.codegen; - -import com.jfinal.kit.PathKit; -import io.jboot.app.JbootApplication; -import io.jboot.codegen.model.JbootBaseModelGenerator; -import io.jboot.codegen.model.JbootModelGenerator; -import io.jboot.codegen.service.JbootServiceImplGenerator; -import io.jboot.codegen.service.JbootServiceInterfaceGenerator; - -/** - * @author Michael Yang 杨福海 (fuhai999@gmail.com) - * @version V1.0 - * @Package io.jboot.codegen - */ -public class GenTester { - - public static void main(String[] args) { - - JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://127.0.0.1:3306/jbootdemo"); - JbootApplication.setBootArg("jboot.datasource.user", "root"); - - String modelPackage = "io.jboot.codegen.test.model"; - String baseModelPackage = modelPackage + ".base"; - - String modelDir = PathKit.getWebRootPath() + "/src/main/java/" + modelPackage.replace(".", "/"); - String baseModelDir = PathKit.getWebRootPath() + "/src/main/java/" + baseModelPackage.replace(".", "/"); - - System.out.println("start generate..."); - System.out.println("generate dir:" + modelDir); - - - new JbootBaseModelGenerator(baseModelPackage, baseModelDir).setGenerateRemarks(true).generate(); - new JbootModelGenerator(modelPackage, baseModelPackage, modelDir).generate(); - - - String servicePackage = "io.jboot.codegen.test.service"; - new JbootServiceInterfaceGenerator(servicePackage, modelPackage).generate(); - new JbootServiceImplGenerator(servicePackage , modelPackage).setImplName("provider").generate(); - - } -} diff --git a/src/main/java/io/jboot/codegen/model/JbootBaseModelGenerator.java b/src/main/java/io/jboot/codegen/model/JbootBaseModelGenerator.java index 9d58be541931c0f598e1222d2efa6bea59862c22..78cae4b83f25a3d3d71c32be1a65fe24df3ae446 100644 --- a/src/main/java/io/jboot/codegen/model/JbootBaseModelGenerator.java +++ b/src/main/java/io/jboot/codegen/model/JbootBaseModelGenerator.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,12 @@ public class JbootBaseModelGenerator extends BaseModelGenerator { this.metaBuilder = CodeGenHelpler.createMetaBuilder(); } + @Override + protected void initEngine() { + this.getterTypeMap.put("java.math.BigInteger", "getBigInteger"); + super.initEngine(); + } + public void generate() { super.generate(metaBuilder.build()); } @@ -53,6 +59,11 @@ public class JbootBaseModelGenerator extends BaseModelGenerator { metaBuilder.setGenerateRemarks(generateRemarks); return this; } - + public JbootBaseModelGenerator addWhitelist(String... tableNames) { + if (tableNames != null) { + this.metaBuilder.addWhitelist(tableNames); + } + return this; + } } diff --git a/src/main/java/io/jboot/codegen/model/JbootModelGenerator.java b/src/main/java/io/jboot/codegen/model/JbootModelGenerator.java index f20105a8143665266039d4168619e8fb8e57962b..3b9ac67a04c3cd9abe34fb509a1906f6003fcb7f 100644 --- a/src/main/java/io/jboot/codegen/model/JbootModelGenerator.java +++ b/src/main/java/io/jboot/codegen/model/JbootModelGenerator.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,6 +55,11 @@ public class JbootModelGenerator extends ModelGenerator { metaBuilder.setGenerateRemarks(generateRemarks); return this; } - - + //增加白名单功能 + public JbootModelGenerator addWhitelist(String... tableNames) { + if (tableNames != null) { + this.metaBuilder.addWhitelist(tableNames); + } + return this; + } } diff --git a/src/main/java/io/jboot/codegen/model/ModeGenTester.java b/src/main/java/io/jboot/codegen/model/ModeGenTester.java index 3a2ad56bc37b3f2512ab045b34e84dbbaa55cd42..e1ab2ba92a5fb8726f8afa8449931f2baa024e50 100644 --- a/src/main/java/io/jboot/codegen/model/ModeGenTester.java +++ b/src/main/java/io/jboot/codegen/model/ModeGenTester.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package io.jboot.codegen.model; -import com.jfinal.kit.PathKit; import io.jboot.app.JbootApplication; +import io.jboot.codegen.CodeGenHelpler; public class ModeGenTester { @@ -29,8 +29,8 @@ public class ModeGenTester { String modelPackage = "io.jboot.codegen.test"; String baseModelPackage = modelPackage + ".base"; - String modelDir = PathKit.getWebRootPath() + "/src/main/java/" + modelPackage.replace(".", "/"); - String baseModelDir = PathKit.getWebRootPath() + "/src/main/java/" + baseModelPackage.replace(".", "/"); + String modelDir = CodeGenHelpler.getUserDir() + "/src/main/java/" + modelPackage.replace(".", "/"); + String baseModelDir = CodeGenHelpler.getUserDir() + "/src/main/java/" + baseModelPackage.replace(".", "/"); System.out.println("start generate..."); System.out.println("generate dir:" + modelDir); diff --git a/src/main/java/io/jboot/codegen/service/JbootServiceImplGenerator.java b/src/main/java/io/jboot/codegen/service/JbootServiceImplGenerator.java index 3c3447a7966d60b999754ea1b78b6821b7ac7e0f..d1792d153754aa9c9c797bcac7a2d5df31c2069d 100644 --- a/src/main/java/io/jboot/codegen/service/JbootServiceImplGenerator.java +++ b/src/main/java/io/jboot/codegen/service/JbootServiceImplGenerator.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,9 @@ */ package io.jboot.codegen.service; +import com.jfinal.core.JFinal; import com.jfinal.kit.JavaKeyword; import com.jfinal.kit.Kv; -import com.jfinal.kit.PathKit; import com.jfinal.kit.StrKit; import com.jfinal.plugin.activerecord.generator.MetaBuilder; import com.jfinal.plugin.activerecord.generator.TableMeta; @@ -26,8 +26,9 @@ import com.jfinal.template.source.ClassPathSourceFactory; import io.jboot.codegen.CodeGenHelpler; import java.io.File; -import java.io.FileWriter; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,6 +36,7 @@ import java.util.Map; public class JbootServiceImplGenerator { private String basePackage; + private String implPackage; private String modelPackage; private MetaBuilder metaBuilder; @@ -53,9 +55,10 @@ public class JbootServiceImplGenerator { this.outputDir = buildOutPutDir(); } - public JbootServiceImplGenerator(String basePackage,String outputDir ,String modelPackage) { + public JbootServiceImplGenerator(String basePackage,String implPackage,String outputDir ,String modelPackage) { this.basePackage = basePackage; + this.implPackage = implPackage; this.modelPackage = modelPackage; this.template = "io/jboot/codegen/service/service_impl_template.tp"; this.metaBuilder = CodeGenHelpler.createMetaBuilder(); @@ -64,7 +67,7 @@ public class JbootServiceImplGenerator { } private String buildOutPutDir() { - return PathKit.getWebRootPath() + "/src/main/java/" + (basePackage + "." + implName).replace(".", "/"); + return CodeGenHelpler.getUserDir() + "/src/main/java/" + (basePackage + "." + implName).replace(".", "/"); } @@ -86,7 +89,13 @@ public class JbootServiceImplGenerator { return this; } - + public JbootServiceImplGenerator addWhitelist(String... tableNames) { + if (tableNames != null) { + this.metaBuilder.addWhitelist(tableNames); + } + return this; + } + public JbootServiceImplGenerator setGenerateRemarks(boolean generateRemarks) { metaBuilder.setGenerateRemarks(generateRemarks); return this; @@ -115,7 +124,7 @@ public class JbootServiceImplGenerator { protected void genBaseModelContent(TableMeta tableMeta) { - Kv data = Kv.by("serviceImplPackageName", basePackage + "." + implName); + Kv data = Kv.by("serviceImplPackageName", implPackage == null ? (basePackage + "." + implName) : implPackage); // data.set("generateChainSetter", generateChainSetter); data.set("tableMeta", tableMeta); data.set("basePackage", basePackage); @@ -153,13 +162,10 @@ public class JbootServiceImplGenerator { return; } - - FileWriter fw = new FileWriter(target); - try { - fw.write(tableMeta.baseModelContent); - } finally { - fw.close(); + try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(target), JFinal.me().getConstants().getEncoding())) { + osw.write(tableMeta.baseModelContent); } + } diff --git a/src/main/java/io/jboot/codegen/service/JbootServiceInterfaceGenerator.java b/src/main/java/io/jboot/codegen/service/JbootServiceInterfaceGenerator.java index 1df014218610e789e9971501dc1331250059c3c2..4f7d5d6c99a485cc690dba1d1af43eebbac529f2 100644 --- a/src/main/java/io/jboot/codegen/service/JbootServiceInterfaceGenerator.java +++ b/src/main/java/io/jboot/codegen/service/JbootServiceInterfaceGenerator.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ */ package io.jboot.codegen.service; +import com.jfinal.core.JFinal; import com.jfinal.kit.Kv; -import com.jfinal.kit.PathKit; import com.jfinal.kit.StrKit; import com.jfinal.plugin.activerecord.generator.BaseModelGenerator; import com.jfinal.plugin.activerecord.generator.MetaBuilder; @@ -26,36 +26,55 @@ import com.jfinal.template.source.ClassPathSourceFactory; import io.jboot.codegen.CodeGenHelpler; import java.io.File; -import java.io.FileWriter; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStreamWriter; import java.util.List; public class JbootServiceInterfaceGenerator extends BaseModelGenerator { private String modelPacket; private String basePackage; + private String classSuffix = "Service"; + private String classPrefix = ""; private MetaBuilder metaBuilder; public JbootServiceInterfaceGenerator(String basePackage, String modelPacket) { - super(basePackage, PathKit.getWebRootPath() + "/src/main/java/" + basePackage.replace(".", "/")); + super(basePackage, CodeGenHelpler.getUserDir() + "/src/main/java/" + basePackage.replace(".", "/")); this.modelPacket = modelPacket; this.basePackage = basePackage; this.template = "io/jboot/codegen/service/service_template.tp"; this.metaBuilder = CodeGenHelpler.createMetaBuilder(); - } - public JbootServiceInterfaceGenerator(String basePackage,String outputDir, String modelPacket) { + public JbootServiceInterfaceGenerator(String basePackage, String outputDir, String modelPacket) { super(basePackage, outputDir); this.modelPacket = modelPacket; this.basePackage = basePackage; this.template = "io/jboot/codegen/service/service_template.tp"; this.metaBuilder = CodeGenHelpler.createMetaBuilder(); + } + + public String getClassSuffix() { + return classSuffix; + } + + public JbootServiceInterfaceGenerator setClassSuffix(String classSuffix) { + this.classSuffix = classSuffix; + return this; + } + public String getClassPrefix() { + return classPrefix; + } + + public JbootServiceInterfaceGenerator setClassPrefix(String classPrefix) { + this.classPrefix = classPrefix; + return this; } public void generate() { @@ -100,6 +119,8 @@ public class JbootServiceInterfaceGenerator extends BaseModelGenerator { data.set("tableMeta", tableMeta); data.set("modelPacket", modelPacket); data.set("basePackage", basePackage); + data.set("classSuffix", this.classSuffix); + data.set("classPrefix", this.classPrefix); Engine engine = Engine.use("forService"); tableMeta.baseModelContent = engine.getTemplate(template).renderToString(data); @@ -116,20 +137,22 @@ public class JbootServiceInterfaceGenerator extends BaseModelGenerator { dir.mkdirs(); } - String target = baseModelOutputDir + File.separator + tableMeta.modelName + "Service" + ".java"; + String target = baseModelOutputDir + File.separator + getClassPrefix() + tableMeta.modelName + classSuffix + ".java"; File targetFile = new File(target); if (targetFile.exists()) { return; } - FileWriter fw = new FileWriter(target); - try { - fw.write(tableMeta.baseModelContent); - } finally { - fw.close(); + try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(target), JFinal.me().getConstants().getEncoding())) { + osw.write(tableMeta.baseModelContent); } } - + public JbootServiceInterfaceGenerator addWhitelist(String... tableNames) { + if (tableNames != null) { + this.metaBuilder.addWhitelist(tableNames); + } + return this; + } } diff --git a/src/main/java/io/jboot/codegen/service/ServiceGenTester.java b/src/main/java/io/jboot/codegen/service/ServiceGenTester.java index a07c79c249e3f1ee89dbd9a5234789896cf9fef5..aba88b8bbc5b0e39f21866ce116c129751d2421c 100644 --- a/src/main/java/io/jboot/codegen/service/ServiceGenTester.java +++ b/src/main/java/io/jboot/codegen/service/ServiceGenTester.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/codegen/service/service_template.tp b/src/main/java/io/jboot/codegen/service/service_template.tp index 8a2253bf02da0129314bd1cea0b21d3f3a087351..03d434747b7aea275ca12047a5569ff696702d84 100644 --- a/src/main/java/io/jboot/codegen/service/service_template.tp +++ b/src/main/java/io/jboot/codegen/service/service_template.tp @@ -6,7 +6,7 @@ import io.jboot.db.model.Columns; import java.util.List; -public interface #(tableMeta.modelName)Service { +public interface #(classPrefix)#(tableMeta.modelName)#(classSuffix) { /** * 根据主键查找Model diff --git a/src/main/java/io/jboot/components/cache/ActionCache.java b/src/main/java/io/jboot/components/cache/ActionCache.java new file mode 100644 index 0000000000000000000000000000000000000000..a62eae39cef9bb2099af34f1bf2eeb53ba8ad97e --- /dev/null +++ b/src/main/java/io/jboot/components/cache/ActionCache.java @@ -0,0 +1,137 @@ +package io.jboot.components.cache; + + +import com.jfinal.log.Log; +import com.jfinal.plugin.ehcache.IDataLoader; + +import java.util.List; + +public class ActionCache { + + private static final Log LOG = Log.getLog(ActionCache.class); + + private static JbootCache actionCache; + + public static JbootCache setThreadCacheNamePrefix(String cacheNamePrefix) { + return getActionCache().setThreadCacheNamePrefix(cacheNamePrefix); + } + + public static void clearThreadCacheNamePrefix() { + getActionCache().clearThreadCacheNamePrefix(); + } + + static JbootCache getActionCache() { + if (actionCache == null) { + actionCache = JbootCacheManager.me().getCache(AopCacheConfig.getInstance().getUseCacheName()); + } + return actionCache; + } + + public static void setActionCache(JbootCache actionCache) { + ActionCache.actionCache = actionCache; + } + + + public static void put(String cacheName, Object key, Object value) { + try { + getActionCache().put(cacheName, key, value); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + } + + public static void put(String cacheName, Object key, Object value, int liveSeconds) { + try { + getActionCache().put(cacheName, key, value, liveSeconds); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + } + + public static List getKeys(String cacheName) { + try { + return getActionCache().getKeys(cacheName); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + return null; + } + + public static void remove(String cacheName, Object key) { + try { + getActionCache().remove(cacheName, key); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + } + + public static void removeAll(String cacheName) { + try { + getActionCache().removeAll(cacheName); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + } + + public static T get(String cacheName, Object key) { + try { + return getActionCache().get(cacheName, key); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + remove(cacheName, key); + } + return null; + } + + + public static T get(String cacheName, Object key, IDataLoader dataLoader) { + try { + return getActionCache().get(cacheName, key, dataLoader); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + remove(cacheName, key); + } + return null; + } + + + public static T get(String cacheName, Object key, IDataLoader dataLoader, int liveSeconds) { + try { + return getActionCache().get(cacheName, key, dataLoader, liveSeconds); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + remove(cacheName, key); + } + return null; + } + + + public static Integer getTtl(String cacheName, Object key) { + try { + return getActionCache().getTtl(cacheName, key); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + return null; + } + + + public static void setTtl(String cacheName, Object key, int seconds) { + try { + getActionCache().setTtl(cacheName, key, seconds); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + } + + private static final ActionCacheConfig CONFIG = ActionCacheConfig.getInstance(); + + public static void putDataToCache(String cacheName, String cacheKey, Object data, int liveSeconds) { + liveSeconds = liveSeconds > 0 ? liveSeconds : CONFIG.getLiveSeconds(); + if (liveSeconds > 0) { + put(cacheName, cacheKey, data, liveSeconds); + } else { + put(cacheName, cacheKey, data); + } + } +} diff --git a/src/main/java/io/jboot/components/cache/ActionCacheConfig.java b/src/main/java/io/jboot/components/cache/ActionCacheConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..20ea4794cbe10794d232616db928cf3000cbcdfc --- /dev/null +++ b/src/main/java/io/jboot/components/cache/ActionCacheConfig.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.cache; + + +import io.jboot.app.config.JbootConfigManager; +import io.jboot.app.config.annotation.ConfigModel; + + +@ConfigModel(prefix = "jboot.action.cache") +public class ActionCacheConfig { + + // AOP 缓存的默认有效时间,0为永久有效,单位秒, + // 当 @Cacheable 和 @CachePut 注解不配置的时候默认用这个配置 + + private int liveSeconds = 60 * 10; //默认为 10 分钟 + private String useCacheName = "default"; + + public int getLiveSeconds() { + return liveSeconds; + } + + public void setLiveSeconds(int liveSeconds) { + this.liveSeconds = liveSeconds; + } + + public String getUseCacheName() { + return useCacheName; + } + + public void setUseCacheName(String useCacheName) { + this.useCacheName = useCacheName; + } + + + private static ActionCacheConfig me; + + public static ActionCacheConfig getInstance() { + if (me == null) { + me = JbootConfigManager.me().get(ActionCacheConfig.class); + } + return me; + } + +} diff --git a/src/main/java/io/jboot/components/cache/AopCache.java b/src/main/java/io/jboot/components/cache/AopCache.java index c965c1ae11a030236b315293b837f1bf9c2f3e3c..c0acfde69072d2f376aa0356835ab07cb587b88a 100644 --- a/src/main/java/io/jboot/components/cache/AopCache.java +++ b/src/main/java/io/jboot/components/cache/AopCache.java @@ -1,63 +1,144 @@ package io.jboot.components.cache; +import com.jfinal.log.Log; import com.jfinal.plugin.ehcache.IDataLoader; -import io.jboot.Jboot; import java.util.List; public class AopCache { + private static final Log LOG = Log.getLog(AopCache.class); + private static JbootCache aopCache; + public static JbootCache setThreadCacheNamePrefix(String cacheNamePrefix) { + return getAopCache().setThreadCacheNamePrefix(cacheNamePrefix); + } + + public static void clearThreadCacheNamePrefix() { + getAopCache().clearThreadCacheNamePrefix(); + } + static JbootCache getAopCache() { if (aopCache == null) { - synchronized (AopCache.class) { - if (aopCache == null) { - aopCache = JbootCacheManager.me().getCache(Jboot.config(JbootCacheConfig.class).getAopCacheType()); - } - } + aopCache = JbootCacheManager.me().getCache(AopCacheConfig.getInstance().getUseCacheName()); } return aopCache; } - public static T get(String cacheName, Object key) { - return getAopCache().get(cacheName, key); + public static void setAopCache(JbootCache aopCache) { + AopCache.aopCache = aopCache; } + public static void put(String cacheName, Object key, Object value) { - getAopCache().put(cacheName, key, value); + try { + getAopCache().put(cacheName, key, value); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } } public static void put(String cacheName, Object key, Object value, int liveSeconds) { - getAopCache().put(cacheName, key, value, liveSeconds); + try { + getAopCache().put(cacheName, key, value, liveSeconds); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } } public static List getKeys(String cacheName) { - return getAopCache().getKeys(cacheName); + try { + return getAopCache().getKeys(cacheName); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + return null; } public static void remove(String cacheName, Object key) { - getAopCache().remove(cacheName, key); + try { + getAopCache().remove(cacheName, key); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } } public static void removeAll(String cacheName) { - getAopCache().removeAll(cacheName); + try { + getAopCache().removeAll(cacheName); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + } + + public static T get(String cacheName, Object key) { + try { + return getAopCache().get(cacheName, key); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + remove(cacheName, key); + } + return null; } + public static T get(String cacheName, Object key, IDataLoader dataLoader) { - return getAopCache().get(cacheName, key, dataLoader); + try { + return getAopCache().get(cacheName, key, dataLoader); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + remove(cacheName, key); + } + return null; } + public static T get(String cacheName, Object key, IDataLoader dataLoader, int liveSeconds) { - return getAopCache().get(cacheName, key, dataLoader, liveSeconds); + try { + return getAopCache().get(cacheName, key, dataLoader, liveSeconds); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + remove(cacheName, key); + } + return null; } + public static Integer getTtl(String cacheName, Object key) { - return getAopCache().getTtl(cacheName, key); + try { + return getAopCache().getTtl(cacheName, key); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + return null; } + public static void setTtl(String cacheName, Object key, int seconds) { - getAopCache().setTtl(cacheName, key, seconds); + try { + getAopCache().setTtl(cacheName, key, seconds); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + } + + + private static final AopCacheConfig CONFIG = AopCacheConfig.getInstance(); + + public static void putDataToCache(String cacheName, String cacheKey, Object data, int liveSeconds) { + liveSeconds = liveSeconds > 0 ? liveSeconds : CONFIG.getLiveSeconds(); + if (liveSeconds > 0) { + put(cacheName, cacheKey, data, liveSeconds); + } + //永久有效 + else if (liveSeconds == 0){ + put(cacheName, cacheKey, data); + } + // -1 负数,取消 AOP 缓存 + else { + // do nothing + } } } diff --git a/src/main/java/io/jboot/components/cache/AopCacheConfig.java b/src/main/java/io/jboot/components/cache/AopCacheConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..5c6b2679e319c90aff85abcb3a7ffed1c83ae21f --- /dev/null +++ b/src/main/java/io/jboot/components/cache/AopCacheConfig.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.cache; + + +import io.jboot.app.config.JbootConfigManager; +import io.jboot.app.config.annotation.ConfigModel; + + +@ConfigModel(prefix = "jboot.aop.cache") +public class AopCacheConfig { + + // AOP 缓存的默认有效时间,0 为永久有效,单位秒, + // 当 @Cacheable 和 @CachePut 注解不配置的时候默认用这个配置 + + private int liveSeconds = 60 * 10; //默认为 10 分钟 + private String useCacheName = "default"; + + public int getLiveSeconds() { + return liveSeconds; + } + + public void setLiveSeconds(int liveSeconds) { + this.liveSeconds = liveSeconds; + } + + public String getUseCacheName() { + return useCacheName; + } + + public void setUseCacheName(String useCacheName) { + this.useCacheName = useCacheName; + } + + + private static AopCacheConfig me; + + public static AopCacheConfig getInstance() { + if (me == null) { + me = JbootConfigManager.me().get(AopCacheConfig.class); + } + return me; + } + +} diff --git a/src/main/java/io/jboot/components/cache/CachePrinter.java b/src/main/java/io/jboot/components/cache/CachePrinter.java new file mode 100644 index 0000000000000000000000000000000000000000..aa277fc356a89202e11728bed15724a2ec86bb27 --- /dev/null +++ b/src/main/java/io/jboot/components/cache/CachePrinter.java @@ -0,0 +1,9 @@ +package io.jboot.components.cache; + +public interface CachePrinter { + + default void println(String debugInfo){ + System.out.println(debugInfo+"\n"); + } + +} diff --git a/src/main/java/io/jboot/components/cache/CacheTime.java b/src/main/java/io/jboot/components/cache/CacheTime.java index 6b1086174a9f6068a4b667b3ed731f988fd2eb14..8a93723e922f90f5ccae5290aa82da3b3f2a7fb6 100644 --- a/src/main/java/io/jboot/components/cache/CacheTime.java +++ b/src/main/java/io/jboot/components/cache/CacheTime.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2019, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, diff --git a/src/main/java/io/jboot/components/cache/JbootCache.java b/src/main/java/io/jboot/components/cache/JbootCache.java index 504ba47794337c9c716c0dd765d331ffbfc178a7..0549266988974d0274d07c275328bd61ffc7a045 100644 --- a/src/main/java/io/jboot/components/cache/JbootCache.java +++ b/src/main/java/io/jboot/components/cache/JbootCache.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,28 +22,59 @@ import java.util.List; public interface JbootCache extends com.jfinal.plugin.activerecord.cache.ICache { + + + //设置当前 线程 的缓存前缀 + JbootCache setThreadCacheNamePrefix(String cacheNamePrefix); + + //清除当前 线程 的缓存前缀 + void clearThreadCacheNamePrefix(); + + //配置哪些缓存名称可以忽略线程前缀的影响 + boolean addThreadCacheNamePrefixIngore(String cacheName); + + default void addThreadCacheNamePrefixIngores(String... cacheNames) { + for (String cacheName : cacheNames) { + addThreadCacheNamePrefixIngore(cacheName); + } + } + + //移除对 addThreadCacheNamePrefixIngore 的配置 + boolean removeThreadCacheNamePrefixIngore(String cacheName); + + + JbootCacheConfig getConfig(); + @Override - public T get(String cacheName, Object key); + T get(String cacheName, Object key); @Override - public void put(String cacheName, Object key, Object value); + void put(String cacheName, Object key, Object value); - public void put(String cacheName, Object key, Object value, int liveSeconds); + void put(String cacheName, Object key, Object value, int liveSeconds); - public List getKeys(String cacheName); @Override - public void remove(String cacheName, Object key); + void remove(String cacheName, Object key); @Override - public void removeAll(String cacheName); + void removeAll(String cacheName); + + T get(String cacheName, Object key, IDataLoader dataLoader); + + T get(String cacheName, Object key, IDataLoader dataLoader, int liveSeconds); + + Integer getTtl(String cacheName, Object key); + + void setTtl(String cacheName, Object key, int seconds); + + void refresh(String cacheName, Object key); - public T get(String cacheName, Object key, IDataLoader dataLoader); + void refresh(String cacheName); - public T get(String cacheName, Object key, IDataLoader dataLoader, int liveSeconds); - public Integer getTtl(String cacheName, Object key); + List getNames(); - public void setTtl(String cacheName, Object key, int seconds); + List getKeys(String cacheName); } diff --git a/src/main/java/io/jboot/components/cache/JbootCacheBase.java b/src/main/java/io/jboot/components/cache/JbootCacheBase.java index 9d10e83e2c528e085fd161e5fd07dd14b6cdf69c..cf07b5404723382b1f0656a7b2ed7073aeeebdf7 100644 --- a/src/main/java/io/jboot/components/cache/JbootCacheBase.java +++ b/src/main/java/io/jboot/components/cache/JbootCacheBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,88 @@ package io.jboot.components.cache; +import io.jboot.utils.StrUtil; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + public abstract class JbootCacheBase implements JbootCache { + protected JbootCacheConfig config; + + public JbootCacheBase(JbootCacheConfig config) { + this.config = config; + } + + private Set ignoreThreadCacheNames = ConcurrentHashMap.newKeySet(); + private ThreadLocal CACHE_NAME_PREFIX_TL = new ThreadLocal<>(); + + @Override + public JbootCacheConfig getConfig() { + return config; + } + + @Override + public JbootCache setThreadCacheNamePrefix(String cacheNamePrefix) { + if (StrUtil.isNotBlank(cacheNamePrefix)) { + CACHE_NAME_PREFIX_TL.set(cacheNamePrefix); + } else { + CACHE_NAME_PREFIX_TL.remove(); + } + return this; + } + + @Override + public void clearThreadCacheNamePrefix() { + CACHE_NAME_PREFIX_TL.remove(); + } + + + @Override + public boolean addThreadCacheNamePrefixIngore(String cacheName) { + return ignoreThreadCacheNames.add(cacheName); + } + + @Override + public boolean removeThreadCacheNamePrefixIngore(String cacheName) { + return ignoreThreadCacheNames.remove(cacheName); + } + + + /** + * 构建缓存名称 + * + * @param cacheName + * @return + */ + protected String buildCacheName(String cacheName) { + + String cacheNamePrefix = null; + + if (!ignoreThreadCacheNames.contains(cacheName)) { + cacheNamePrefix = CACHE_NAME_PREFIX_TL.get(); + } + + if (StrUtil.isBlank(cacheNamePrefix)) { + cacheNamePrefix = config.getDefaultCachePrefix(); + } + + return StrUtil.isNotBlank(cacheNamePrefix) ? (cacheNamePrefix + ":" + cacheName) : cacheName; + } + + + @Override + public void refresh(String cacheName, Object key) { + + } + + @Override + public void refresh(String cacheName) { + + } + + + protected void println(String debugInfo) { + JbootCacheManager.me().getPrinter().println(debugInfo); + } } diff --git a/src/main/java/io/jboot/components/cache/JbootCacheConfig.java b/src/main/java/io/jboot/components/cache/JbootCacheConfig.java index 7efcf2a85e4408f39c85b3330bb1ee5dae374dec..ce2f2d1a7617fe86ea584413fec1f0492e83d521 100644 --- a/src/main/java/io/jboot/components/cache/JbootCacheConfig.java +++ b/src/main/java/io/jboot/components/cache/JbootCacheConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,11 @@ package io.jboot.components.cache; +import com.google.common.collect.Sets; +import io.jboot.Jboot; import io.jboot.app.config.annotation.ConfigModel; -import io.jboot.utils.StrUtil; + +import java.util.Set; @ConfigModel(prefix = "jboot.cache") @@ -31,13 +34,31 @@ public class JbootCacheConfig { public static final String TYPE_CAREDIS = "caredis"; public static final String TYPE_NONE = "none"; + public static final Set TYPES = Sets.newHashSet(TYPE_EHCACHE, TYPE_REDIS, TYPE_EHREDIS + , TYPE_J2CACHE, TYPE_CAFFEINE, TYPE_CAREDIS, TYPE_NONE); + + private String name = "default"; + private String type = TYPE_CAFFEINE; + private String typeName; + + private String defaultCachePrefix; + private Boolean devMode = false; - private String type = TYPE_EHCACHE; + //只启用一级缓存,针对分布式场景下,redis 有关闭了 scan keys 等指令的时候 + //可以使用 redis 的消息机制做缓存同步 + private boolean useFirstLevelOnly = false; + + private String cacheSyncMqChannel; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } - // AOP 缓存的默认有效时间,0为永久有效,单位秒, - // 当 @Cacheable 和 @CachePut 注解不配置的时候默认用这个配置 - private int aopCacheLiveSeconds = 0; - private String aopCacheType; public String getType() { return type; @@ -47,22 +68,43 @@ public class JbootCacheConfig { this.type = type; } - public int getAopCacheLiveSeconds() { - return aopCacheLiveSeconds; + public String getTypeName() { + return typeName; + } + + public void setTypeName(String typeName) { + this.typeName = typeName; + } + + public String getDefaultCachePrefix() { + return defaultCachePrefix; + } + + public void setDefaultCachePrefix(String defaultCachePrefix) { + this.defaultCachePrefix = defaultCachePrefix; + } + + public boolean isDevMode() { + return devMode == null ? Jboot.isDevMode() : devMode; + } + + public void setDevMode(Boolean devMode) { + this.devMode = devMode; + } + + public boolean isUseFirstLevelOnly() { + return useFirstLevelOnly; } - public void setAopCacheLiveSeconds(int aopCacheLiveSeconds) { - this.aopCacheLiveSeconds = aopCacheLiveSeconds; + public void setUseFirstLevelOnly(boolean useFirstLevelOnly) { + this.useFirstLevelOnly = useFirstLevelOnly; } - public String getAopCacheType() { - if (StrUtil.isBlank(aopCacheType)){ - aopCacheType = getType(); - } - return aopCacheType; + public String getCacheSyncMqChannel() { + return cacheSyncMqChannel; } - public void setAopCacheType(String aopCacheType) { - this.aopCacheType = aopCacheType; + public void setCacheSyncMqChannel(String cacheSyncMqChannel) { + this.cacheSyncMqChannel = cacheSyncMqChannel; } } diff --git a/src/main/java/io/jboot/components/cache/JbootCacheManager.java b/src/main/java/io/jboot/components/cache/JbootCacheManager.java index 205e0d483801ee93107c040ddbee773e6771e257..bb782f25c56f009beda17d3a342176bbbaaeeaa3 100644 --- a/src/main/java/io/jboot/components/cache/JbootCacheManager.java +++ b/src/main/java/io/jboot/components/cache/JbootCacheManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,8 @@ import io.jboot.components.cache.j2cache.J2cacheImpl; import io.jboot.components.cache.none.NoneCacheImpl; import io.jboot.components.cache.redis.JbootRedisCacheImpl; import io.jboot.core.spi.JbootSpiLoader; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.ConfigUtil; import io.jboot.utils.StrUtil; import java.util.Map; @@ -38,58 +40,78 @@ public class JbootCacheManager { } private Map cacheMap = new ConcurrentHashMap<>(); - private JbootCacheConfig config = Jboot.config(JbootCacheConfig.class); + private CachePrinter printer = new CachePrinter() { + @Override + public void println(String debugInfo) { + CachePrinter.super.println(debugInfo); + } + }; public static JbootCacheManager me() { return me; } public JbootCache getCache() { - return getCache(config.getType()); + return getCache("default"); } - public JbootCache getCache(String type) { - if (StrUtil.isBlank(type)) { - throw new IllegalArgumentException("type must not be null or blank."); + public JbootCache getCache(String name) { + if (StrUtil.isBlank(name)) { + throw new IllegalArgumentException("Cache name must not be null or blank."); } - JbootCache cache = cacheMap.get(type); - if (cache != null) { - return cache; - } + JbootCache cache = cacheMap.get(name); + + if (cache == null) { + Map configModels = ConfigUtil.getConfigModels(JbootCacheConfig.class); + JbootCacheConfig.TYPES.forEach(configModels::remove); + + configModels.putIfAbsent("default", Jboot.config(JbootCacheConfig.class)); + + if (!configModels.containsKey(name)) { + throw new JbootIllegalConfigException("Please config \"jboot.cache." + name + ".type\" in your jboot.properties."); + } - synchronized (type.intern()) { - if (cache == null) { - JbootCacheConfig cacheConfig = new JbootCacheConfig(); - cacheConfig.setType(type); - cache = buildCache(cacheConfig); - cacheMap.put(type, cache); + JbootCacheConfig cacheConfig = configModels.get(name); + JbootCache newCache = buildCache(cacheConfig); + if (newCache != null) { + cacheMap.putIfAbsent(name, newCache); } + + cache = cacheMap.get(name); } return cache; } - private JbootCache buildCache(JbootCacheConfig config) { + private synchronized JbootCache buildCache(JbootCacheConfig config) { switch (config.getType()) { case JbootCacheConfig.TYPE_EHCACHE: - return new JbootEhcacheImpl(); + return new JbootEhcacheImpl(config); case JbootCacheConfig.TYPE_REDIS: - return new JbootRedisCacheImpl(); + return new JbootRedisCacheImpl(config); case JbootCacheConfig.TYPE_EHREDIS: - return new JbootEhredisCacheImpl(); + return new JbootEhredisCacheImpl(config); case JbootCacheConfig.TYPE_J2CACHE: - return new J2cacheImpl(); + return new J2cacheImpl(config); case JbootCacheConfig.TYPE_CAFFEINE: - return new CaffeineCacheImpl(); + return new CaffeineCacheImpl(config); case JbootCacheConfig.TYPE_CAREDIS: - return new JbootCaredisCacheImpl(); + return new JbootCaredisCacheImpl(config); case JbootCacheConfig.TYPE_NONE: - return new NoneCacheImpl(); + return new NoneCacheImpl(config); default: - return JbootSpiLoader.load(JbootCache.class, config.getType()); + return JbootSpiLoader.load(JbootCache.class, config.getType(), config); } } + + public CachePrinter getPrinter() { + return printer; + } + + public void setPrinter(CachePrinter printer) { + this.printer = printer; + } } diff --git a/src/main/java/io/jboot/components/cache/annotation/CacheEvict.java b/src/main/java/io/jboot/components/cache/annotation/CacheEvict.java index 43e3a3389f8804870340bb6b2eb5ce5d21651720..78da5da59338bb8b14e9896ab37172fb260723c7 100644 --- a/src/main/java/io/jboot/components/cache/annotation/CacheEvict.java +++ b/src/main/java/io/jboot/components/cache/annotation/CacheEvict.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/cache/annotation/CachePut.java b/src/main/java/io/jboot/components/cache/annotation/CachePut.java index c55d57aff2e8a6772f66a8eedc945e1cce3f1151..35f7dd80fb4a1b66affc79b41aa04a40e250c7f9 100644 --- a/src/main/java/io/jboot/components/cache/annotation/CachePut.java +++ b/src/main/java/io/jboot/components/cache/annotation/CachePut.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/cache/annotation/Cacheable.java b/src/main/java/io/jboot/components/cache/annotation/Cacheable.java index aedaf912a3eac7f13698672ffbd9cc10a5b93ba8..63cc111546cc12e1658947ecf7c9718cd38cde69 100644 --- a/src/main/java/io/jboot/components/cache/annotation/Cacheable.java +++ b/src/main/java/io/jboot/components/cache/annotation/Cacheable.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/cache/annotation/CachesEvict.java b/src/main/java/io/jboot/components/cache/annotation/CachesEvict.java index 60d3b384a9fcdb6c307cc4be47736258b108df10..edc4d9dd1c006cb943263009cd4ccb44773c662d 100644 --- a/src/main/java/io/jboot/components/cache/annotation/CachesEvict.java +++ b/src/main/java/io/jboot/components/cache/annotation/CachesEvict.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheBuilder.java b/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheBuilder.java index c300c53c1b88e3cae1db373e4aa6678ab3e663f7..6a0db1049811ddca0232637c2dd364575474b59b 100644 --- a/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheBuilder.java +++ b/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheBuilder.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,5 +20,5 @@ import com.github.benmanes.caffeine.cache.Cache; public interface CaffeineCacheBuilder { - public Cache build(); + Cache build(String cacheName); } diff --git a/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheImpl.java b/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheImpl.java index 153b6e2a89f91596094a7132dda3f8a522b962cb..351eb84b70debe2121f412586cf2e026d7cbef5c 100644 --- a/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheImpl.java +++ b/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package io.jboot.components.cache.caffeine; import com.github.benmanes.caffeine.cache.Cache; import com.jfinal.plugin.ehcache.IDataLoader; import io.jboot.components.cache.JbootCacheBase; +import io.jboot.components.cache.JbootCacheConfig; import java.util.ArrayList; import java.util.List; @@ -29,16 +30,23 @@ public class CaffeineCacheImpl extends JbootCacheBase { private Map cacheMap = new ConcurrentHashMap<>(); + public CaffeineCacheImpl(JbootCacheConfig config) { + super(config); + } + protected Cache getCacheOnly(String cacheName) { + cacheName = buildCacheName(cacheName); return cacheMap.get(cacheName); } protected Cache getCache(String cacheName) { + cacheName = buildCacheName(cacheName); Cache cache = cacheMap.get(cacheName); if (cache == null) { synchronized (CaffeineCacheImpl.class) { + cache = cacheMap.get(cacheName); if (cache == null) { - cache = createCacheBuilder().build(); + cache = createCacheBuilder().build(cacheName); cacheMap.put(cacheName,cache); } } @@ -63,35 +71,53 @@ public class CaffeineCacheImpl extends JbootCacheBase { cache.invalidate(key); return null; } - return (T) data.getValue(); + + T cacheData = (T) data.getValue(); + if (config.isDevMode()) { + println("CaffeineCache GET: cacheName[" + buildCacheName(cacheName) + "] cacheKey[" + key + "] value:" + cacheData); + } + return cacheData; } @Override public void put(String cacheName, Object key, Object value) { putData(getCache(cacheName), key, new CaffeineCacheObject(value)); + if (config.isDevMode()) { + println("CaffeineCache PUT: cacheName[" + buildCacheName(cacheName) + "] cacheKey[" + key + "] value:" + value); + } } @Override public void put(String cacheName, Object key, Object value, int liveSeconds) { putData(getCache(cacheName), key, new CaffeineCacheObject(value, liveSeconds)); + if (config.isDevMode()) { + println("CaffeineCache PUT: cacheName[" + buildCacheName(cacheName) + "] cacheKey[" + key + "] value:" + value); + } } - @Override - public List getKeys(String cacheName) { - Cache cache = getCacheOnly(cacheName); - return cache == null ? null : new ArrayList(cache.asMap().keySet()); - } + @Override public void remove(String cacheName, Object key) { Cache cache = getCacheOnly(cacheName); - if (cache != null) cache.invalidate(key); + if (cache != null) { + cache.invalidate(key); + } + if (config.isDevMode()) { + println("CaffeineCache REMOVE: cacheName[" + buildCacheName(cacheName) + "] cacheKey[" + key + "]"); + } } @Override public void removeAll(String cacheName) { Cache cache = getCacheOnly(cacheName); - if (cache != null) cache.invalidateAll(); + if (cache != null) { + cache.invalidateAll(); + } + cacheMap.remove(buildCacheName(cacheName)); + if (config.isDevMode()) { + println("CaffeineCache REMOVEALL: cacheName[" + buildCacheName(cacheName) + "]"); + } } @Override @@ -103,10 +129,17 @@ public class CaffeineCacheImpl extends JbootCacheBase { if (newValue != null) { data = new CaffeineCacheObject(newValue); putData(cache, key, data); + if (config.isDevMode()) { + println("CaffeineCache PUT: cacheName[" + buildCacheName(cacheName) + "] cacheKey[" + key + "] value:" + newValue); + } } return (T) newValue; } else { - return (T) data.getValue(); + Object cacheData = data.getValue(); + if (config.isDevMode()) { + println("CaffeineCache GET: cacheName[" + buildCacheName(cacheName) + "] cacheKey[" + key + "] value:" + cacheData); + } + return (T) cacheData; } } @@ -119,20 +152,31 @@ public class CaffeineCacheImpl extends JbootCacheBase { if (newValue != null) { data = new CaffeineCacheObject(newValue, liveSeconds); putData(cache, key, data); + if (config.isDevMode()) { + println("CaffeineCache PUT: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + newValue); + } } return (T) newValue; } else { - return (T) data.getValue(); + Object cacheData = data.getValue(); + if (config.isDevMode()) { + println("CaffeineCache GET: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + cacheData); + } + return (T) cacheData; } } @Override public Integer getTtl(String cacheName, Object key) { Cache cache = getCacheOnly(cacheName); - if (cache == null) return null; + if (cache == null) { + return null; + } CaffeineCacheObject data = (CaffeineCacheObject) cache.getIfPresent(key); - if (data == null) return null; + if (data == null) { + return null; + } return data.getTtl(); } @@ -140,13 +184,33 @@ public class CaffeineCacheImpl extends JbootCacheBase { @Override public void setTtl(String cacheName, Object key, int seconds) { Cache cache = getCacheOnly(cacheName); - if (cache == null) return; + if (cache == null) { + return; + } CaffeineCacheObject data = (CaffeineCacheObject) cache.getIfPresent(key); - if (data == null) return; + if (data == null) { + return; + } data.setLiveSeconds(seconds); putData(cache, key, data); + + if (config.isDevMode()) { + println("CaffeineCache SETTTL: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] seconds:" + seconds); + } + } + + + @Override + public List getNames() { + return new ArrayList(cacheMap.keySet()); + } + + @Override + public List getKeys(String cacheName) { + Cache cache = getCacheOnly(cacheName); + return cache == null ? null : new ArrayList(cache.asMap().keySet()); } diff --git a/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheObject.java b/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheObject.java index 7125649fbb7dd23aeab81bd6d8cc93e14165a297..3e11d8e06e8775418e85c9c1c6e7d1b1dae5b011 100644 --- a/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheObject.java +++ b/src/main/java/io/jboot/components/cache/caffeine/CaffeineCacheObject.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,10 +53,7 @@ public class CaffeineCacheObject implements Serializable { return -1; } - long timeMillis = cachetime - DateUtils.addSeconds(new Date(), -liveSeconds) - .getTime(); - - System.out.println("timeMillis:" + timeMillis); + long timeMillis = cachetime - DateUtils.addSeconds(new Date(), -liveSeconds).getTime(); if (timeMillis > 0) { return (int) (timeMillis / 1000); diff --git a/src/main/java/io/jboot/components/cache/caffeine/DefaultCaffeineCacheBuilder.java b/src/main/java/io/jboot/components/cache/caffeine/DefaultCaffeineCacheBuilder.java index 57110601bf96a6ea899b666b85b1e83805a67d61..51df59d29fdd92aa81425ef017a13c382aebdc42 100644 --- a/src/main/java/io/jboot/components/cache/caffeine/DefaultCaffeineCacheBuilder.java +++ b/src/main/java/io/jboot/components/cache/caffeine/DefaultCaffeineCacheBuilder.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import java.util.concurrent.TimeUnit; public class DefaultCaffeineCacheBuilder implements CaffeineCacheBuilder{ @Override - public Cache build(){ + public Cache build(String cacheName){ return Caffeine.newBuilder() .expireAfterWrite(24, TimeUnit.HOURS) .build(); diff --git a/src/main/java/io/jboot/components/cache/caredis/JbootCaredisCacheImpl.java b/src/main/java/io/jboot/components/cache/caredis/JbootCaredisCacheImpl.java index 9690fa3da58dd9263ab90581efa0f11c2b7c3b9a..3f2e41c9355697a2f58888192d983bbbbe0bd877 100644 --- a/src/main/java/io/jboot/components/cache/caredis/JbootCaredisCacheImpl.java +++ b/src/main/java/io/jboot/components/cache/caredis/JbootCaredisCacheImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.jfinal.plugin.ehcache.IDataLoader; import io.jboot.Jboot; import io.jboot.components.cache.JbootCacheBase; +import io.jboot.components.cache.JbootCacheConfig; import io.jboot.components.cache.caffeine.CaffeineCacheImpl; import io.jboot.components.cache.redis.JbootRedisCacheImpl; import io.jboot.components.serializer.JbootSerializer; @@ -53,13 +54,19 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { .build(); - public JbootCaredisCacheImpl() { - this.caffeineCacheImpl = new CaffeineCacheImpl(); - this.redisCacheImpl = new JbootRedisCacheImpl(); + public JbootCaredisCacheImpl(JbootCacheConfig config) { + super(config); + this.caffeineCacheImpl = new CaffeineCacheImpl(config); + this.redisCacheImpl = new JbootRedisCacheImpl(config); this.clientId = StrUtil.uuid(); this.serializer = Jboot.getSerializer(); - this.redis = redisCacheImpl.getRedis(); + //在某些场景下,多个应用使用同一个 redis 实例,此时可以通过配置 cacheSyncMqChannel 来解决缓存冲突的问题 + if (StrUtil.isNotBlank(config.getCacheSyncMqChannel())){ + this.channel = config.getCacheSyncMqChannel(); + } + + this.redis = redisCacheImpl.getRedis(); this.redis.subscribe(new BinaryJedisPubSub() { @Override public void onMessage(byte[] channel, byte[] message) { @@ -69,27 +76,10 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { } - @Override - public List getKeys(String cacheName) { - List list = keysCache.getIfPresent(cacheName); - if (list == null) { - list = redisCacheImpl.getKeys(cacheName); - if (list == null) { - synchronized (cacheName.intern()) { - if (list == null) { - list = new ArrayList(); - } - } - } - keysCache.put(cacheName, list); - } - return list; - } - @Override public T get(String cacheName, Object key) { T value = caffeineCacheImpl.get(cacheName, key); - if (value == null) { + if (value == null && !config.isUseFirstLevelOnly()) { value = redisCacheImpl.get(cacheName, key); if (value != null) { Integer ttl = redisCacheImpl.getTtl(cacheName, key); @@ -107,7 +97,9 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { public void put(String cacheName, Object key, Object value) { try { caffeineCacheImpl.put(cacheName, key, value); - redisCacheImpl.put(cacheName, key, value); + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.put(cacheName, key, value); + } } finally { publishMessage(JbootCaredisMessage.ACTION_PUT, cacheName, key); } @@ -122,7 +114,9 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { } try { caffeineCacheImpl.put(cacheName, key, value, liveSeconds); - redisCacheImpl.put(cacheName, key, value, liveSeconds); + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.put(cacheName, key, value, liveSeconds); + } } finally { publishMessage(JbootCaredisMessage.ACTION_PUT, cacheName, key); } @@ -132,7 +126,9 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { public void remove(String cacheName, Object key) { try { caffeineCacheImpl.remove(cacheName, key); - redisCacheImpl.remove(cacheName, key); + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.remove(cacheName, key); + } } finally { publishMessage(JbootCaredisMessage.ACTION_REMOVE, cacheName, key); } @@ -142,7 +138,9 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { public void removeAll(String cacheName) { try { caffeineCacheImpl.removeAll(cacheName); - redisCacheImpl.removeAll(cacheName); + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.removeAll(cacheName); + } } finally { publishMessage(JbootCaredisMessage.ACTION_REMOVE_ALL, cacheName, null); } @@ -183,7 +181,7 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { @Override public Integer getTtl(String cacheName, Object key) { Integer ttl = caffeineCacheImpl.getTtl(cacheName, key); - if (ttl == null) { + if (ttl == null && !config.isUseFirstLevelOnly()) { ttl = redisCacheImpl.getTtl(cacheName, key); } return ttl; @@ -194,11 +192,50 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { public void setTtl(String cacheName, Object key, int seconds) { try { caffeineCacheImpl.setTtl(cacheName, key, seconds); - redisCacheImpl.setTtl(cacheName, key, seconds); + + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.setTtl(cacheName, key, seconds); + } } finally { publishMessage(JbootCaredisMessage.ACTION_REMOVE, cacheName, key); } + } + + @Override + public void refresh(String cacheName, Object key) { + publishMessage(JbootCaredisMessage.ACTION_REMOVE, cacheName, key); + } + + + @Override + public void refresh(String cacheName) { + publishMessage(JbootCaredisMessage.ACTION_REMOVE_ALL, cacheName, null); + } + + + @Override + public List getNames() { + return config.isUseFirstLevelOnly() ? null : redisCacheImpl.getNames(); + } + + + @Override + public List getKeys(String cacheName) { + List list = keysCache.getIfPresent(cacheName); + if (list != null) { + return list; + } + + if (!config.isUseFirstLevelOnly()) { + list = redisCacheImpl.getKeys(cacheName); + if (list == null) { + list = new ArrayList(); + } + keysCache.put(cacheName, list); + } + + return list; } @@ -215,9 +252,8 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { public void onMessage(String channel, Object obj) { JbootCaredisMessage message = (JbootCaredisMessage) obj; - /** - * 不处理自己发送的消息 - */ + + //不处理自己发送的消息 if (clientId.equals(message.getClientId())) { return; } @@ -226,8 +262,6 @@ public class JbootCaredisCacheImpl extends JbootCacheBase { switch (message.getAction()) { case JbootCaredisMessage.ACTION_PUT: - caffeineCacheImpl.remove(message.getCacheName(), message.getKey()); - break; case JbootCaredisMessage.ACTION_REMOVE: caffeineCacheImpl.remove(message.getCacheName(), message.getKey()); break; diff --git a/src/main/java/io/jboot/components/cache/caredis/JbootCaredisMessage.java b/src/main/java/io/jboot/components/cache/caredis/JbootCaredisMessage.java index 954ebf0fd6afb0dc24997a60f6f2c3fb7105edf5..6422e578ede008e009ad44133eeed78591d67d9e 100644 --- a/src/main/java/io/jboot/components/cache/caredis/JbootCaredisMessage.java +++ b/src/main/java/io/jboot/components/cache/caredis/JbootCaredisMessage.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/cache/ehcache/JbootEhCacheConfig.java b/src/main/java/io/jboot/components/cache/ehcache/JbootEhCacheConfig.java index 4eeed51705f0bdf653386c9331e8c43c5d3f148b..afa5a4c85baf9fd0e21c4fdd177d848946c56530 100644 --- a/src/main/java/io/jboot/components/cache/ehcache/JbootEhCacheConfig.java +++ b/src/main/java/io/jboot/components/cache/ehcache/JbootEhCacheConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/cache/ehcache/JbootEhcacheImpl.java b/src/main/java/io/jboot/components/cache/ehcache/JbootEhcacheImpl.java index c64675123c9176febe2d000785cbfa1f328fcee1..8c97f164376efa33271c87104a299d0ada2cf101 100644 --- a/src/main/java/io/jboot/components/cache/ehcache/JbootEhcacheImpl.java +++ b/src/main/java/io/jboot/components/cache/ehcache/JbootEhcacheImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,17 @@ package io.jboot.components.cache.ehcache; import com.jfinal.kit.PathKit; -import com.jfinal.log.Log; import com.jfinal.plugin.ehcache.IDataLoader; import io.jboot.Jboot; import io.jboot.components.cache.JbootCacheBase; +import io.jboot.components.cache.JbootCacheConfig; import io.jboot.utils.StrUtil; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; import net.sf.ehcache.event.CacheEventListener; +import java.util.Arrays; import java.util.List; @@ -34,26 +35,22 @@ public class JbootEhcacheImpl extends JbootCacheBase { private CacheManager cacheManager; private static Object locker = new Object(); - private static final Log log = Log.getLog(JbootEhcacheImpl.class); - private CacheEventListener cacheEventListener; - public JbootEhcacheImpl() { - JbootEhCacheConfig config = Jboot.config(JbootEhCacheConfig.class); - if (StrUtil.isBlank(config.getConfigFileName())) { + public JbootEhcacheImpl(JbootCacheConfig config) { + super(config); + JbootEhCacheConfig ehconfig = Jboot.config(JbootEhCacheConfig.class); + if (StrUtil.isBlank(ehconfig.getConfigFileName())) { cacheManager = CacheManager.create(); } else { - String configPath = config.getConfigFileName(); - if (!configPath.startsWith("/")){ - configPath = PathKit.getRootClassPath()+"/"+configPath; + String configPath = ehconfig.getConfigFileName(); + if (!configPath.startsWith("/")) { + configPath = PathKit.getRootClassPath() + "/" + configPath; } cacheManager = CacheManager.create(configPath); } } - public JbootEhcacheImpl(CacheManager cacheManager) { - this.cacheManager = cacheManager; - } public CacheEventListener getCacheEventListener() { return cacheEventListener; @@ -64,12 +61,12 @@ public class JbootEhcacheImpl extends JbootCacheBase { } public Cache getOrAddCache(String cacheName) { + cacheName = buildCacheName(cacheName); Cache cache = cacheManager.getCache(cacheName); if (cache == null) { synchronized (locker) { cache = cacheManager.getCache(cacheName); if (cache == null) { - log.warn("Could not find cache config [" + cacheName + "], using default."); cacheManager.addCacheIfAbsent(cacheName); cache = cacheManager.getCache(cacheName); if (cacheEventListener != null) { @@ -81,20 +78,29 @@ public class JbootEhcacheImpl extends JbootCacheBase { return cache; } - @Override - public List getKeys(String cacheName) { - return getOrAddCache(cacheName).getKeys(); - } @Override public T get(String cacheName, Object key) { Element element = getOrAddCache(cacheName).get(key); - return element != null ? (T) element.getObjectValue() : null; + if (element == null) { + return null; + } + + Object objectValue = element.getObjectValue(); + if (config.isDevMode()) { + println("Ehcache GET: cacheName[" + buildCacheName(cacheName) + "] cacheKey[" + key + "] value:" + objectValue); + } + return (T) objectValue; + } @Override public void put(String cacheName, Object key, Object value) { getOrAddCache(cacheName).put(new Element(key, value)); + + if (config.isDevMode()) { + println("Ehcache PUT: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + value); + } } @Override @@ -106,16 +112,29 @@ public class JbootEhcacheImpl extends JbootCacheBase { Element element = new Element(key, value); element.setTimeToLive(liveSeconds); getOrAddCache(cacheName).put(element); + + if (config.isDevMode()) { + println("Ehcache PUT: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + value); + } } @Override public void remove(String cacheName, Object key) { getOrAddCache(cacheName).remove(key); + + if (config.isDevMode()) { + println("Ehcache REMOVE: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"]"); + } } @Override public void removeAll(String cacheName) { getOrAddCache(cacheName).removeAll(); + cacheManager.removeCache(cacheName); + + if (config.isDevMode()) { + println("Ehcache REMOVEALL: cacheName[" +buildCacheName(cacheName)+ "]"); + } } @Override @@ -125,6 +144,10 @@ public class JbootEhcacheImpl extends JbootCacheBase { data = dataLoader.load(); put(cacheName, key, data); } + + if (config.isDevMode()) { + println("Ehcache GET: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + data); + } return (T) data; } @@ -138,6 +161,11 @@ public class JbootEhcacheImpl extends JbootCacheBase { data = dataLoader.load(); put(cacheName, key, data, liveSeconds); } + + if (config.isDevMode()) { + println("Ehcache GET: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + data); + } + return (T) data; } @@ -157,8 +185,23 @@ public class JbootEhcacheImpl extends JbootCacheBase { element.setTimeToLive(seconds); getOrAddCache(cacheName).put(element); + + if (config.isDevMode()) { + println("Ehcache SETTTL: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] seconds:" + seconds); + } } + @Override + public List getNames() { + return Arrays.asList(cacheManager.getCacheNames()); + } + + @Override + public List getKeys(String cacheName) { + return getOrAddCache(cacheName).getKeys(); + } + + public CacheManager getCacheManager() { return cacheManager; } diff --git a/src/main/java/io/jboot/components/cache/ehredis/JbootEhredisCacheImpl.java b/src/main/java/io/jboot/components/cache/ehredis/JbootEhredisCacheImpl.java index d773e0f4320c877236a50b22febd4ff31f3fba47..604b81ec215d6651c7628da9f25aab0fd7d9eb72 100644 --- a/src/main/java/io/jboot/components/cache/ehredis/JbootEhredisCacheImpl.java +++ b/src/main/java/io/jboot/components/cache/ehredis/JbootEhredisCacheImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,12 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.jfinal.plugin.ehcache.IDataLoader; import io.jboot.Jboot; -import io.jboot.support.redis.JbootRedis; import io.jboot.components.cache.JbootCacheBase; +import io.jboot.components.cache.JbootCacheConfig; import io.jboot.components.cache.ehcache.JbootEhcacheImpl; import io.jboot.components.cache.redis.JbootRedisCacheImpl; import io.jboot.components.serializer.JbootSerializer; +import io.jboot.support.redis.JbootRedis; import io.jboot.utils.StrUtil; import net.sf.ehcache.CacheException; import net.sf.ehcache.Ehcache; @@ -57,14 +58,19 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL .build(key -> null); - public JbootEhredisCacheImpl() { - this.ehcacheImpl = new JbootEhcacheImpl(); + public JbootEhredisCacheImpl(JbootCacheConfig config) { + super(config); + this.ehcacheImpl = new JbootEhcacheImpl(config); this.ehcacheImpl.setCacheEventListener(this); - this.redisCacheImpl = new JbootRedisCacheImpl(); + this.redisCacheImpl = new JbootRedisCacheImpl(config); this.clientId = StrUtil.uuid(); this.serializer = Jboot.getSerializer(); + if (StrUtil.isNotBlank(config.getCacheSyncMqChannel())){ + this.channel = config.getCacheSyncMqChannel(); + } + this.redis = redisCacheImpl.getRedis(); this.redis.subscribe(new BinaryJedisPubSub() { @Override @@ -75,23 +81,10 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL } - @Override - public List getKeys(String cacheName) { - List list = keysCache.getIfPresent(cacheName); - if (list == null) { - list = redisCacheImpl.getKeys(cacheName); - if (list == null) { - list = new ArrayList(); - } - keysCache.put(cacheName, list); - } - return list; - } - @Override public T get(String cacheName, Object key) { T value = ehcacheImpl.get(cacheName, key); - if (value == null) { + if (value == null && !config.isUseFirstLevelOnly()) { value = redisCacheImpl.get(cacheName, key); if (value != null) { Integer ttl = redisCacheImpl.getTtl(cacheName, key); @@ -109,7 +102,9 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL public void put(String cacheName, Object key, Object value) { try { ehcacheImpl.put(cacheName, key, value); - redisCacheImpl.put(cacheName, key, value); + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.put(cacheName, key, value); + } } finally { publishMessage(JbootEhredisMessage.ACTION_PUT, cacheName, key); } @@ -124,7 +119,10 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL } try { ehcacheImpl.put(cacheName, key, value, liveSeconds); - redisCacheImpl.put(cacheName, key, value, liveSeconds); + + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.put(cacheName, key, value, liveSeconds); + } } finally { publishMessage(JbootEhredisMessage.ACTION_PUT, cacheName, key); } @@ -134,7 +132,10 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL public void remove(String cacheName, Object key) { try { ehcacheImpl.remove(cacheName, key); - redisCacheImpl.remove(cacheName, key); + + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.remove(cacheName, key); + } } finally { publishMessage(JbootEhredisMessage.ACTION_REMOVE, cacheName, key); } @@ -144,7 +145,10 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL public void removeAll(String cacheName) { try { ehcacheImpl.removeAll(cacheName); - redisCacheImpl.removeAll(cacheName); + + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.removeAll(cacheName); + } } finally { publishMessage(JbootEhredisMessage.ACTION_REMOVE_ALL, cacheName, null); } @@ -185,7 +189,7 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL @Override public Integer getTtl(String cacheName, Object key) { Integer ttl = ehcacheImpl.getTtl(cacheName, key); - if (ttl == null) { + if (ttl == null && !config.isUseFirstLevelOnly()) { ttl = redisCacheImpl.getTtl(cacheName, key); } return ttl; @@ -196,11 +200,13 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL public void setTtl(String cacheName, Object key, int seconds) { try { ehcacheImpl.setTtl(cacheName, key, seconds); - redisCacheImpl.setTtl(cacheName, key, seconds); + + if (!config.isUseFirstLevelOnly()) { + redisCacheImpl.setTtl(cacheName, key, seconds); + } } finally { publishMessage(JbootEhredisMessage.ACTION_REMOVE, cacheName, key); } - } @@ -214,12 +220,42 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL keysCache.invalidate(cacheName); } + + @Override + public void refresh(String cacheName, Object key) { + publishMessage(JbootEhredisMessage.ACTION_REMOVE, cacheName, key); + } + + + @Override + public void refresh(String cacheName) { + publishMessage(JbootEhredisMessage.ACTION_REMOVE_ALL, cacheName, null); + } + + @Override + public List getNames() { + return config.isUseFirstLevelOnly() ? null : redisCacheImpl.getNames(); + } + + @Override + public List getKeys(String cacheName) { + List list = keysCache.getIfPresent(cacheName); + if (list == null && !config.isUseFirstLevelOnly()) { + list = redisCacheImpl.getKeys(cacheName); + if (list == null) { + list = new ArrayList(); + } + keysCache.put(cacheName, list); + } + return list; + } + + public void onMessage(String channel, Object obj) { JbootEhredisMessage message = (JbootEhredisMessage) obj; - /** - * 不处理自己发送的消息 - */ + + //不处理自己发送的消息 if (clientId.equals(message.getClientId())) { return; } @@ -228,8 +264,6 @@ public class JbootEhredisCacheImpl extends JbootCacheBase implements CacheEventL switch (message.getAction()) { case JbootEhredisMessage.ACTION_PUT: - ehcacheImpl.remove(message.getCacheName(), message.getKey()); - break; case JbootEhredisMessage.ACTION_REMOVE: ehcacheImpl.remove(message.getCacheName(), message.getKey()); break; diff --git a/src/main/java/io/jboot/components/cache/ehredis/JbootEhredisMessage.java b/src/main/java/io/jboot/components/cache/ehredis/JbootEhredisMessage.java index 683213d62515e41654eab4b29dec9934541f96c1..1630b27ce86104c197f09a5c2932c938a071007b 100644 --- a/src/main/java/io/jboot/components/cache/ehredis/JbootEhredisMessage.java +++ b/src/main/java/io/jboot/components/cache/ehredis/JbootEhredisMessage.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/cache/interceptor/ActionCachedContent.java b/src/main/java/io/jboot/components/cache/interceptor/ActionCachedContent.java new file mode 100644 index 0000000000000000000000000000000000000000..4d62b476d3ced1c230cc4f7d6bc2e4baccdef01a --- /dev/null +++ b/src/main/java/io/jboot/components/cache/interceptor/ActionCachedContent.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.cache.interceptor; + +import com.jfinal.render.*; +import io.jboot.web.render.JbootRender; +import io.jboot.web.render.JbootTemplateRender; +import io.jboot.web.render.JbootXmlRender; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class ActionCachedContent implements Serializable { + + private static final int RENDER_DEFAULT = 0; + private static final int RENDER_TEMPLATE = 1; + private static final int RENDER_XML = 2; + private static final int RENDER_JSON = 3; + private static final int RENDER_TEXT = 4; + + private static IRenderFactory renderFactory = RenderManager.me().getRenderFactory(); + private static final Set ignoreAttrs = new HashSet<>(); + + + private Map headers; + private Map attrs; + + private String contentType; + private Integer renderType; + private String viewOrText; + private Map otherPara = null; + + public static Set getIgnoreAttrs() { + return ignoreAttrs; + } + + public static void addIgnoreAttr(String attrName) { + ignoreAttrs.add(attrName); + } + + public ActionCachedContent(Render render) { + if (render == null) { + throw new IllegalArgumentException("Render can not be null."); + } + + // xml + if (render instanceof JbootXmlRender) { + renderType = RENDER_XML; + contentType = ((JbootXmlRender) render).getContentType(); + viewOrText = render.getView(); + } + // template + else if (render instanceof JbootTemplateRender) { + renderType = RENDER_TEMPLATE; + contentType = ((JbootTemplateRender) render).getContentType(); + viewOrText = render.getView(); + } + // default + else if (render instanceof JbootRender) { + renderType = RENDER_DEFAULT; + contentType = ((JbootRender) render).getContentType(); + viewOrText = render.getView(); + } + // text + else if (render instanceof TextRender) { + renderType = RENDER_TEXT; + contentType = ((TextRender) render).getContentType(); + viewOrText = ((TextRender) render).getText(); + } + // json + else if (render instanceof JsonRender) { + renderType = RENDER_JSON; + otherPara = new HashMap<>(); + + JsonRender jsonRender = (JsonRender) render; + otherPara.put("jsonText", jsonRender.getJsonText()); + otherPara.put("attrs", jsonRender.getAttrs()); + otherPara.put("forIE", jsonRender.getForIE()); + } else { + throw new IllegalArgumentException("@Cacheable Can not support the render of the type: " + render.getClass().getName()); + } + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public Map getAttrs() { + return attrs; + } + + public void setAttrs(Map attrs) { + this.attrs = attrs; + if (this.attrs != null) { + for (String ignoreAttr : ignoreAttrs) { + this.attrs.remove(ignoreAttr); + } + } + } + + public void addAttr(String key, Object value) { + if (ignoreAttrs.contains(key)) { + return; + } + if (this.attrs == null) { + this.attrs = new HashMap<>(); + } + this.attrs.put(key, value); + } + + public String getViewOrText() { + return viewOrText; + } + + public void setViewOrText(String viewOrText) { + this.viewOrText = viewOrText; + } + + public Integer getRenderType() { + return renderType; + } + + public void setRenderType(Integer renderType) { + this.renderType = renderType; + } + + public Map getOtherPara() { + return otherPara; + } + + public void setOtherPara(Map otherPara) { + this.otherPara = otherPara; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public void addHeader(String key, String value) { + if (this.headers == null) { + this.headers = new HashMap<>(); + } + this.headers.put(key, value); + } + + + public Render createRender() { + switch (renderType) { + case RENDER_DEFAULT: + return renderFactory.getRender(viewOrText); + case RENDER_TEMPLATE: + return renderFactory.getTemplateRender(viewOrText); + case RENDER_JSON: + JsonRender jsonRender; + if (otherPara.get("jsonText") != null) { + jsonRender = (JsonRender) renderFactory.getJsonRender((String) otherPara.get("jsonText")); + } else if (otherPara.get("attrs") != null) { + jsonRender = (JsonRender) renderFactory.getJsonRender((String[]) otherPara.get("attrs")); + } else { + jsonRender = (JsonRender) renderFactory.getJsonRender(); + } + if (Boolean.TRUE.equals(otherPara.get("forIE"))) { + jsonRender.forIE(); + } + return jsonRender; + case RENDER_TEXT: + return renderFactory.getTextRender(viewOrText, contentType); + case RENDER_XML: + return renderFactory.getXmlRender(viewOrText); + default: + throw new IllegalStateException("@Cacheable can not support the renderType of the value: " + renderType); + } + } +} diff --git a/src/main/java/io/jboot/components/cache/interceptor/CPI.java b/src/main/java/io/jboot/components/cache/interceptor/CPI.java new file mode 100644 index 0000000000000000000000000000000000000000..0b5b1c6a4dc6c3d495f349f77fab5d320766dff1 --- /dev/null +++ b/src/main/java/io/jboot/components/cache/interceptor/CPI.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.cache.interceptor; + +import com.jfinal.template.Engine; + +public class CPI { + + public static Engine getCacheRenderEngine(){ + return Utils.ENGINE; + } + +} diff --git a/src/main/java/io/jboot/components/cache/interceptor/JbootCacheEvictInterceptor.java b/src/main/java/io/jboot/components/cache/interceptor/CacheEvictInterceptor.java similarity index 75% rename from src/main/java/io/jboot/components/cache/interceptor/JbootCacheEvictInterceptor.java rename to src/main/java/io/jboot/components/cache/interceptor/CacheEvictInterceptor.java index 3af70ddda2716dfef82aff4a40cba308639afc73..52b6d6967d5c864a4130fc1c93868e2ebab4611e 100644 --- a/src/main/java/io/jboot/components/cache/interceptor/JbootCacheEvictInterceptor.java +++ b/src/main/java/io/jboot/components/cache/interceptor/CacheEvictInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,29 +25,28 @@ import java.lang.reflect.Method; /** * 清除缓存操作的拦截器 */ -public class JbootCacheEvictInterceptor implements Interceptor { +public class CacheEvictInterceptor implements Interceptor { @Override public void intercept(Invocation inv) { Method method = inv.getMethod(); - CacheEvict cacheEvict = method.getAnnotation(CacheEvict.class); if (cacheEvict == null) { inv.invoke(); return; } - Class targetClass = inv.getTarget().getClass(); + Class targetClass = inv.getTarget().getClass(); if (cacheEvict.beforeInvocation()) { - Utils.doCacheEvict(inv.getArgs(), targetClass, method, cacheEvict); + Utils.removeCache(inv.getArgs(), targetClass, method, cacheEvict, inv.isActionInvocation()); } inv.invoke(); if (!cacheEvict.beforeInvocation()) { - Utils.doCacheEvict(inv.getArgs(), targetClass, method, cacheEvict); + Utils.removeCache(inv.getArgs(), targetClass, method, cacheEvict, inv.isActionInvocation()); } } } diff --git a/src/main/java/io/jboot/components/cache/interceptor/CacheInterceptorBuilder.java b/src/main/java/io/jboot/components/cache/interceptor/CacheInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..8ee0a521ca1c00e851b78723db9dbd9cc4c79560 --- /dev/null +++ b/src/main/java/io/jboot/components/cache/interceptor/CacheInterceptorBuilder.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.cache.interceptor; + +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.components.cache.annotation.CacheEvict; +import io.jboot.components.cache.annotation.CachePut; +import io.jboot.components.cache.annotation.Cacheable; +import io.jboot.components.cache.annotation.CachesEvict; + +import java.lang.reflect.Method; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +@AutoLoad +public class CacheInterceptorBuilder implements InterceptorBuilder { + + // 缓存拦截器的权重 + // 其他拦截器器的默认权重是 1+ + public static final int INTERCEPTOR_WEIGHT = 100; + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.hasAnnotation(method, CacheEvict.class)) { + interceptors.add(CacheEvictInterceptor.class, INTERCEPTOR_WEIGHT); + } + + if (Util.hasAnnotation(method, Cacheable.class)) { + interceptors.add(CacheableInterceptor.class, INTERCEPTOR_WEIGHT); + } + + if (Util.hasAnnotation(method, CachePut.class)) { + interceptors.add(CachePutInterceptor.class, INTERCEPTOR_WEIGHT); + } + + if (Util.hasAnnotation(method, CachesEvict.class)) { + interceptors.add(CachesEvictInterceptor.class, INTERCEPTOR_WEIGHT); + } + } + + +} diff --git a/src/main/java/io/jboot/components/cache/interceptor/JbootCachePutInterceptor.java b/src/main/java/io/jboot/components/cache/interceptor/CachePutInterceptor.java similarity index 49% rename from src/main/java/io/jboot/components/cache/interceptor/JbootCachePutInterceptor.java rename to src/main/java/io/jboot/components/cache/interceptor/CachePutInterceptor.java index 02b7f5edea5f171afb3308805ebb8d4be9bffefc..b47a6694bcad335c40fb0240cfd6c43f0805a91f 100644 --- a/src/main/java/io/jboot/components/cache/interceptor/JbootCachePutInterceptor.java +++ b/src/main/java/io/jboot/components/cache/interceptor/CachePutInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ package io.jboot.components.cache.interceptor; import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; +import com.jfinal.core.Controller; +import io.jboot.components.cache.AopCache; import io.jboot.components.cache.annotation.CachePut; import io.jboot.utils.AnnotationUtil; @@ -26,20 +28,49 @@ import java.lang.reflect.Method; /** * 缓存设置拦截器 */ -public class JbootCachePutInterceptor implements Interceptor { +public class CachePutInterceptor implements Interceptor { @Override public void intercept(Invocation inv) { - //先执行,之后再保存数据 - inv.invoke(); - Method method = inv.getMethod(); CachePut cachePut = method.getAnnotation(CachePut.class); - if (cachePut == null) { + if (cachePut == null || (inv.isActionInvocation() && !CacheableInterceptor.isActionCacheEnable())) { + inv.invoke(); + return; + } + + if (inv.isActionInvocation()) { + forController(inv, method, cachePut); + } else { + forService(inv, method, cachePut); + } + } + + + private void forController(Invocation inv, Method method, CachePut cachePut) { + String unless = AnnotationUtil.get(cachePut.unless()); + if (Utils.isUnless(unless, method, inv.getArgs())) { return; } + Class targetClass = inv.getTarget().getClass(); + String cacheName = AnnotationUtil.get(cachePut.name()); + Utils.ensureCacheNameNotBlank(method, cacheName); + String cacheKey = Utils.buildCacheKey(AnnotationUtil.get(cachePut.key()), targetClass, method, inv.getArgs()); + + Controller controller = inv.getController(); + + inv.invoke(); + + CacheableInterceptor.cacheActionContent(cacheName, cacheKey, cachePut.liveSeconds(), inv , method); + } + + + private void forService(Invocation inv, Method method, CachePut cachePut) { + + inv.invoke(); + Object data = inv.getReturnValue(); String unless = AnnotationUtil.get(cachePut.unless()); @@ -47,12 +78,12 @@ public class JbootCachePutInterceptor implements Interceptor { return; } - Class targetClass = inv.getTarget().getClass(); + Class targetClass = inv.getTarget().getClass(); String cacheName = AnnotationUtil.get(cachePut.name()); - Utils.ensureCachenameAvailable(method, targetClass, cacheName); + Utils.ensureCacheNameNotBlank(method, cacheName); String cacheKey = Utils.buildCacheKey(AnnotationUtil.get(cachePut.key()), targetClass, method, inv.getArgs()); - Utils.putDataToCache(cachePut.liveSeconds(),cacheName,cacheKey,data); + AopCache.putDataToCache(cacheName, cacheKey, data, cachePut.liveSeconds()); } diff --git a/src/main/java/io/jboot/components/cache/interceptor/CacheableInterceptor.java b/src/main/java/io/jboot/components/cache/interceptor/CacheableInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..8f5aa44a812f0ae39a505380f790785146ad62d0 --- /dev/null +++ b/src/main/java/io/jboot/components/cache/interceptor/CacheableInterceptor.java @@ -0,0 +1,319 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.cache.interceptor; + + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.core.Action; +import com.jfinal.core.CPI; +import com.jfinal.core.Controller; +import com.jfinal.plugin.activerecord.Page; +import com.jfinal.render.Render; +import com.jfinal.render.RenderManager; +import io.jboot.components.cache.AopCache; +import io.jboot.components.cache.annotation.Cacheable; +import io.jboot.db.model.JbootModel; +import io.jboot.exception.JbootException; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.ModelUtil; +import io.jboot.utils.StrUtil; +import io.jboot.web.render.JbootRenderFactory; +import io.jboot.web.render.JbootReturnValueRender; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.*; + +/** + * 缓存操作的拦截器 + * + * @author michael yang + */ +public class CacheableInterceptor implements Interceptor { + + private static final String NULL_VALUE = "NULL_VALUE"; + public static final String IGNORE_CACHED_ATTRS = "__ignore_cached_attrs"; + + //是否开启 Controller 的 Action 缓存 + //可用在 dev 模式下关闭,生产环境开启的场景,方便调试数据 + private static boolean actionCacheEnable = true; + private static String actionCacheRefreshKey; + private static String actionCacheRefreshValue = "1"; + + public static boolean isActionCacheEnable() { + return actionCacheEnable; + } + + public static void setActionCacheEnable(boolean actionCacheEnable) { + CacheableInterceptor.actionCacheEnable = actionCacheEnable; + } + + public static String getActionCacheRefreshKey() { + return actionCacheRefreshKey; + } + + public static void setActionCacheRefreshKey(String actionCacheRefreshKey) { + CacheableInterceptor.actionCacheRefreshKey = actionCacheRefreshKey; + } + + public static String getActionCacheRefreshValue() { + return actionCacheRefreshValue; + } + + public static void setActionCacheRefreshValue(String actionCacheRefreshValue) { + if (actionCacheRefreshValue == null || actionCacheRefreshValue.trim().length() == 0){ + throw new NullPointerException("actionCacheRefresValue can not be null or empty."); + } + CacheableInterceptor.actionCacheRefreshValue = actionCacheRefreshValue; + } + + @Override + public void intercept(Invocation inv) { + + Method method = inv.getMethod(); + Cacheable cacheable = method.getAnnotation(Cacheable.class); + if (cacheable == null || (inv.isActionInvocation() && !actionCacheEnable)) { + inv.invoke(); + return; + } + + if (inv.isActionInvocation()) { + forController(inv, method, cacheable); + } else { + forService(inv, method, cacheable); + } + } + + + private void forController(Invocation inv, Method method, Cacheable cacheable) { + String unlessString = AnnotationUtil.get(cacheable.unless()); + if (Utils.isUnless(unlessString, method, inv.getArgs())) { + inv.invoke(); + return; + } + + Class targetClass = inv.getTarget().getClass(); + String cacheName = AnnotationUtil.get(cacheable.name()); + Utils.ensureCacheNameNotBlank(method, cacheName); + String cacheKey = Utils.buildCacheKey(AnnotationUtil.get(cacheable.key()), targetClass, method, inv.getArgs()); + + Controller controller = inv.getController(); + + //刷新当前页面缓存 + if (StrUtil.isNotBlank(actionCacheRefreshKey) + && actionCacheRefreshValue.equals(inv.getController().getPara(actionCacheRefreshKey))){ + inv.invoke(); + cacheActionContent(cacheName, cacheKey, cacheable.liveSeconds(), inv, method); + return; + } + + ActionCachedContent actionCachedContent = AopCache.get(cacheName, cacheKey); + if (actionCachedContent != null) { + renderActionCachedContent(controller, actionCachedContent); + return; + } + + inv.invoke(); + cacheActionContent(cacheName, cacheKey, cacheable.liveSeconds(), inv, method); + } + + + /** + * 对 action 内容进行缓存 + * + * @param cacheName + * @param cacheKey + * @param liveSeconds + * @param inv + * @param method + */ + public static void cacheActionContent(String cacheName, String cacheKey, int liveSeconds, Invocation inv, Method method) { + + Render render = getControllerRender(inv, method); + + if (render == null){ + return; + } + + ActionCachedContent cachedContent = new ActionCachedContent(render); + + Controller controller = inv.getController(); + + // 忽略的缓存配置 + Set ignoreCachedAttrs = controller.getAttr(IGNORE_CACHED_ATTRS); + if (ignoreCachedAttrs != null) { + ignoreCachedAttrs.add(IGNORE_CACHED_ATTRS); + } + + HttpServletRequest request = controller.getRequest(); + for (Enumeration names = request.getAttributeNames(); names.hasMoreElements(); ) { + String name = names.nextElement(); + if (ignoreCachedAttrs == null || !ignoreCachedAttrs.contains(name)) { + cachedContent.addAttr(name, request.getAttribute(name)); + } + } + + HttpServletResponse response = controller.getResponse(); + Collection headerNames = response.getHeaderNames(); + headerNames.forEach(name -> cachedContent.addHeader(name, response.getHeader(name))); + + AopCache.putDataToCache(cacheName, cacheKey, cachedContent, liveSeconds); + } + + + protected static final RenderManager renderManager = RenderManager.me(); + + private static Render getControllerRender(Invocation inv, Method method) { + Render render = inv.getController().getRender(); + if (render == null && void.class != method.getReturnType() + && renderManager.getRenderFactory() instanceof JbootRenderFactory) { + + JbootRenderFactory factory = (JbootRenderFactory) renderManager.getRenderFactory(); + JbootReturnValueRender returnValueRender = factory.getReturnValueRender(inv.getReturnValue()); + + //有可能为 null,比如 Forward 的情况 + render = returnValueRender.getRealRender(); + + }else if (render == null) { + Action action = CPI.getAction(inv.getController()); + render = renderManager.getRenderFactory().getDefaultRender(action.getViewPath() + action.getMethodName()); + } + + return render; + } + + + /** + * 渲染缓存的 ActionCachedContent + * + * @param controller + * @param actionCachedContent + */ + private void renderActionCachedContent(Controller controller, ActionCachedContent actionCachedContent) { + Map cachedAttrs = actionCachedContent.getAttrs(); + if (cachedAttrs != null) { + HttpServletRequest request = controller.getRequest(); + Set existAttrNames = getRequestAttrNames(request); + cachedAttrs.forEach((cachedAttrName, value) -> { + if (!existAttrNames.contains(cachedAttrName)) { + request.setAttribute(cachedAttrName, value); + } + }); + } + + + Map headers = actionCachedContent.getHeaders(); + if (headers != null) { + HttpServletResponse response = controller.getResponse(); + headers.forEach((name, value) -> { + String existHeaderValue = response.getHeader(name); + if (existHeaderValue != null) { + value = existHeaderValue; + } + response.setHeader(name, value); + }); + } + + controller.render(actionCachedContent.createRender()); + } + + + private Set getRequestAttrNames(HttpServletRequest request) { + Set ret = new HashSet<>(); + for (Enumeration attrNames = request.getAttributeNames(); attrNames.hasMoreElements(); ) { + ret.add(attrNames.nextElement()); + } + return ret; + } + + /** + * Service 层的 Cacheable 使用 + * + * @param inv + * @param method + * @param cacheable + */ + private void forService(Invocation inv, Method method, Cacheable cacheable) { + String unlessString = AnnotationUtil.get(cacheable.unless()); + if (Utils.isUnless(unlessString, method, inv.getArgs())) { + inv.invoke(); + return; + } + + Class targetClass = inv.getTarget().getClass(); + String cacheName = AnnotationUtil.get(cacheable.name()); + Utils.ensureCacheNameNotBlank(method, cacheName); + String cacheKey = Utils.buildCacheKey(AnnotationUtil.get(cacheable.key()), targetClass, method, inv.getArgs()); + + Object data = AopCache.get(cacheName, cacheKey); + if (data != null) { + if (NULL_VALUE.equals(data)) { + inv.setReturnValue(null); + } else if (cacheable.returnCopyEnable()) { + inv.setReturnValue(getCopyObject(inv, data)); + } else { + inv.setReturnValue(data); + } + } else { + inv.invoke(); + data = inv.getReturnValue(); + if (data != null) { + + AopCache.putDataToCache(cacheName, cacheKey, data, cacheable.liveSeconds()); + + //当启用返回 copy 值的时候,返回的内容应该是一个进行copy之后的值 + if (cacheable.returnCopyEnable()) { + inv.setReturnValue(getCopyObject(inv, data)); + } + + } else if (cacheable.nullCacheEnable()) { + AopCache.putDataToCache(cacheName, cacheKey, NULL_VALUE, cacheable.liveSeconds()); + } + } + } + + + private Object getCopyObject(Invocation inv, Object data) { + if (data instanceof List) { + return ModelUtil.copy((List) data); + } else if (data instanceof Set) { + return ModelUtil.copy((Set) data); + } else if (data instanceof Page) { + return ModelUtil.copy((Page) data); + } else if (data instanceof JbootModel) { + return ModelUtil.copy((JbootModel) data); + } else if (data.getClass().isArray() + && JbootModel.class.isAssignableFrom(data.getClass().getComponentType())) { + return ModelUtil.copy((M[]) data); + } else { + throw newException(null, inv, data); + } + } + + + private JbootException newException(Exception ex, Invocation inv, Object data) { + String msg = "Can not copy data for type [" + data.getClass().getName() + "] in method :" + + ClassUtil.buildMethodString(inv.getMethod()) + + " , can not use @Cacheable(returnCopyEnable=true) annotation"; + + return ex == null ? new JbootException(msg) : new JbootException(msg, ex); + } + + +} diff --git a/src/main/java/io/jboot/components/cache/interceptor/JbootCachesEvictInterceptor.java b/src/main/java/io/jboot/components/cache/interceptor/CachesEvictInterceptor.java similarity index 81% rename from src/main/java/io/jboot/components/cache/interceptor/JbootCachesEvictInterceptor.java rename to src/main/java/io/jboot/components/cache/interceptor/CachesEvictInterceptor.java index d5bec562dfc7155324419165abfe9922106cf892..08c8552de06ccc3084281b19a414970007fd4f87 100644 --- a/src/main/java/io/jboot/components/cache/interceptor/JbootCachesEvictInterceptor.java +++ b/src/main/java/io/jboot/components/cache/interceptor/CachesEvictInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,8 @@ import java.util.List; /** * 清除缓存操作的拦截器 */ -public class JbootCachesEvictInterceptor implements Interceptor { - private static final Log LOG = Log.getLog(JbootCachesEvictInterceptor.class); +public class CachesEvictInterceptor implements Interceptor { + private static final Log LOG = Log.getLog(CachesEvictInterceptor.class); @Override @@ -62,20 +62,21 @@ public class JbootCachesEvictInterceptor implements Interceptor { } } - Class targetClass = inv.getTarget().getClass(); + Class targetClass = inv.getTarget().getClass(); try { - doCachesEvict(inv.getArgs(), targetClass, method, beforeInvocations); + doCachesEvict(inv.getArgs(), targetClass, method, beforeInvocations, inv.isActionInvocation()); inv.invoke(); } finally { - doCachesEvict(inv.getArgs(), targetClass, method, afterInvocations); + doCachesEvict(inv.getArgs(), targetClass, method, afterInvocations, inv.isActionInvocation()); } } private void doCachesEvict(Object[] arguments - , Class targetClass + , Class targetClass , Method method - , List cacheEvicts) { + , List cacheEvicts + , boolean isAction) { if (cacheEvicts == null || cacheEvicts.isEmpty()) { return; @@ -83,7 +84,7 @@ public class JbootCachesEvictInterceptor implements Interceptor { for (CacheEvict evict : cacheEvicts) { try { - Utils.doCacheEvict(arguments, targetClass, method, evict); + Utils.removeCache(arguments, targetClass, method, evict, isAction); } catch (Exception ex) { LOG.error(ex.toString(), ex); } diff --git a/src/main/java/io/jboot/components/cache/interceptor/JbootCacheInterceptor.java b/src/main/java/io/jboot/components/cache/interceptor/JbootCacheInterceptor.java deleted file mode 100644 index b79e58a4453d832f23212b98d8f678be9e174efb..0000000000000000000000000000000000000000 --- a/src/main/java/io/jboot/components/cache/interceptor/JbootCacheInterceptor.java +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.components.cache.interceptor; - - -import com.jfinal.aop.Interceptor; -import com.jfinal.aop.Invocation; -import com.jfinal.plugin.activerecord.Page; -import io.jboot.components.cache.AopCache; -import io.jboot.components.cache.annotation.Cacheable; -import io.jboot.db.model.JbootModel; -import io.jboot.exception.JbootException; -import io.jboot.utils.AnnotationUtil; -import io.jboot.utils.ClassUtil; -import io.jboot.utils.ModelCopier; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.Set; - -/** - * 缓存操作的拦截器 - * - * @author michael yang - */ -public class JbootCacheInterceptor implements Interceptor { - - private static final String NULL_VALUE = "NULL_VALUE"; - - @Override - public void intercept(Invocation inv) { - - Method method = inv.getMethod(); - Cacheable cacheable = method.getAnnotation(Cacheable.class); - if (cacheable == null) { - inv.invoke(); - return; - } - - String unlessString = AnnotationUtil.get(cacheable.unless()); - if (Utils.isUnless(unlessString, method, inv.getArgs())) { - inv.invoke(); - return; - } - - Class targetClass = inv.getTarget().getClass(); - String cacheName = AnnotationUtil.get(cacheable.name()); - Utils.ensureCachenameAvailable(method, targetClass, cacheName); - String cacheKey = Utils.buildCacheKey(AnnotationUtil.get(cacheable.key()), targetClass, method, inv.getArgs()); - - Object data = AopCache.get(cacheName, cacheKey); - if (data != null) { - if (NULL_VALUE.equals(data)) { - inv.setReturnValue(null); - } else if (cacheable.returnCopyEnable()) { - inv.setReturnValue(getCopyObject(inv, data)); - } else { - inv.setReturnValue(data); - } - } else { - inv.invoke(); - data = inv.getReturnValue(); - if (data != null) { - - Utils.putDataToCache(cacheable.liveSeconds(), cacheName, cacheKey, data); - - //当启用返回 copy 值的时候,返回的内容应该是一个进行copy之后的值 - if (cacheable.returnCopyEnable()) { - inv.setReturnValue(getCopyObject(inv, data)); - } - - } else if (cacheable.nullCacheEnable()) { - Utils.putDataToCache(cacheable.liveSeconds(), cacheName, cacheKey, NULL_VALUE); - } - } - } - - - private Object getCopyObject(Invocation inv, Object data) { - if (data instanceof List) { - return ModelCopier.copy((List) data); - } else if (data instanceof Set) { - return ModelCopier.copy((Set) data); - } else if (data instanceof Page) { - return ModelCopier.copy((Page) data); - } else if (data instanceof JbootModel) { - return ModelCopier.copy((JbootModel) data); - } else if (data.getClass().isArray() - && JbootModel.class.isAssignableFrom(data.getClass().getComponentType())) { - return ModelCopier.copy((M[]) data); - } else { - throw newException(null, inv, data); - } - } - - - private JbootException newException(Exception ex, Invocation inv, Object data) { - String msg = "can not copy data for type [" + data.getClass().getName() + "] in method :" - + ClassUtil.buildMethodString(inv.getMethod()) - + " , can not use @Cacheable(returnCopyEnable=true) annotation"; - - return ex == null ? new JbootException(msg) : new JbootException(msg, ex); - } - - -} diff --git a/src/main/java/io/jboot/components/cache/interceptor/ParaDirective.java b/src/main/java/io/jboot/components/cache/interceptor/ParaDirective.java new file mode 100644 index 0000000000000000000000000000000000000000..aadaf6b273f978a8f7c42f5c701940a5eb44f73c --- /dev/null +++ b/src/main/java/io/jboot/components/cache/interceptor/ParaDirective.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.cache.interceptor; + +import com.jfinal.core.Controller; +import com.jfinal.kit.LogKit; +import com.jfinal.template.Env; +import com.jfinal.template.io.Writer; +import com.jfinal.template.stat.Scope; +import io.jboot.utils.StrUtil; +import io.jboot.web.controller.JbootControllerContext; +import io.jboot.web.directive.base.JbootDirectiveBase; + +import java.io.IOException; + +public class ParaDirective extends JbootDirectiveBase { + + @Override + public void onRender(Env env, Scope scope, Writer writer) { + + Controller controller = JbootControllerContext.get(); + if (controller == null) { + throw new IllegalStateException("#para(...) directive only use for controller." + getLocation()); + } + + String key = getPara(0, scope); + String defaultValue = getPara(1, scope); + + if (StrUtil.isBlank(key)) { + throw new IllegalArgumentException("#para(...) argument must not be empty." + getLocation()); + } + + String value = controller.getPara(key); + if (StrUtil.isBlank(value)) { + value = StrUtil.isNotBlank(defaultValue) ? defaultValue : ""; + } + + try { + writer.write(value); + } catch (IOException e) { + LogKit.error(e.toString(), e); + } + } + + public static Object para(String key) { + return para(key, null); + } + + public static Object para(String key, Object defaultValue) { + Controller controller = JbootControllerContext.get(); + if (controller == null) { + throw new IllegalStateException("para(...) method only use for controller."); + } + + String value = controller.get(key); + + if (StrUtil.isNumeric(value)) { + return Long.valueOf(value); + } + + if (StrUtil.isDecimal(value)) { + return Double.parseDouble(value); + } + + return StrUtil.isNotBlank(value) ? value : defaultValue; + } +} + diff --git a/src/main/java/io/jboot/components/cache/interceptor/Utils.java b/src/main/java/io/jboot/components/cache/interceptor/Utils.java index 1dc5fbeef0938bbc8d167bd0399a349d68293b47..5e83feed648373a77d02295807d25b5a4eeb2594 100644 --- a/src/main/java/io/jboot/components/cache/interceptor/Utils.java +++ b/src/main/java/io/jboot/components/cache/interceptor/Utils.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,30 +15,33 @@ */ package io.jboot.components.cache.interceptor; -import com.jfinal.log.Log; import com.jfinal.template.Engine; -import io.jboot.Jboot; +import io.jboot.components.cache.ActionCache; import io.jboot.components.cache.AopCache; -import io.jboot.components.cache.JbootCacheConfig; import io.jboot.components.cache.annotation.CacheEvict; +import io.jboot.db.model.Columns; import io.jboot.exception.JbootException; -import io.jboot.utils.AnnotationUtil; -import io.jboot.utils.ArrayUtil; -import io.jboot.utils.ClassUtil; -import io.jboot.utils.StrUtil; +import io.jboot.utils.*; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collection; import java.util.HashMap; import java.util.Map; class Utils { + static final Engine ENGINE = new Engine("JbootCacheRenderEngine"); - static final Log LOG = Log.getLog(Utils.class); - static final Engine ENGINE = new Engine("JbootCacheRender"); + static { + ENGINE.addDirective("para", ParaDirective.class); + ENGINE.addSharedStaticMethod(ParaDirective.class); + } /** * use jfinal engine render text @@ -49,80 +52,81 @@ class Utils { * @return */ static String engineRender(String template, Method method, Object[] arguments) { - Map datas = new HashMap(); + Map datas = new HashMap<>(); int x = 0; for (Parameter p : method.getParameters()) { if (!p.isNamePresent()) { // 必须通过添加 -parameters 进行编译,才可以获取 Parameter 的编译前的名字 - throw new RuntimeException(" Maven or IDE config is error. see http://www.jfinal.com/doc/3-3 "); + throw new RuntimeException(" Maven or IDE config is error. see https://jfinal.com/doc/3-3 "); } datas.put(p.getName(), arguments[x++]); } return ENGINE.getTemplateByString(template).renderToString(datas); - } - static String buildCacheKey(String key, Class clazz, Method method, Object[] arguments) { - + static String buildCacheKey(String key, Class clazz, Method method, Object[] arguments) { clazz = ClassUtil.getUsefulClass(clazz); if (StrUtil.isNotBlank(key)) { return renderKey(key, method, arguments); } - StringBuilder keyBuilder = new StringBuilder(clazz.getName()); - keyBuilder.append("#").append(method.getName()); + StringBuilder keyBuilder = new StringBuilder(clazz.getSimpleName()); + keyBuilder.append('.').append(method.getName()); if (ArrayUtil.isNullOrEmpty(arguments)) { - return keyBuilder.toString(); + return keyBuilder.append("()").toString(); } - Class[] paramTypes = method.getParameterTypes(); + Class[] paramTypes = method.getParameterTypes(); int index = 0; for (Object argument : arguments) { - String argStr = converteToString(argument); - ensureArgumentNotNull(argStr, clazz, method); - keyBuilder - .append(paramTypes[index++].getClass().getName()) - .append(":") - .append(argStr) - .append("-"); - } + String argString = convertToString(argument, method); + if (index == 0) { + keyBuilder.append("("); + } else { + keyBuilder.append(", "); + } + keyBuilder.append(paramTypes[index++].getSimpleName()) + .append(':') + .append(argString); - //remove last chat '-' - return keyBuilder.deleteCharAt(keyBuilder.length() - 1).toString(); + if (index == arguments.length) { + keyBuilder.append(")"); + } + } + return keyBuilder.toString(); } private static String renderKey(String key, Method method, Object[] arguments) { - if (!key.contains("#(") || !key.contains(")")) { - return key; + int indexOfStartFlag = key.indexOf("#"); + if (indexOfStartFlag > -1) { + int indexOfEndFlag = key.indexOf(")"); + if (indexOfEndFlag > indexOfStartFlag) { + return engineRender(key, method, arguments); + } } - return engineRender(key, method, arguments); + return key; } - public static void ensureArgumentNotNull(String argument, Class clazz, Method method) { - if (argument == null) { - throw new JbootException("not support empty key for annotation @Cacheable, @CacheEvict or @CachePut " + - "at method[" + ClassUtil.buildMethodString(method) + "], " + - "please config key properties in @Cacheable, @CacheEvict or @CachePut annotation."); - } - } - public static void ensureCachenameAvailable(Method method, Class targetClass, String cacheName) { + public static void ensureCacheNameNotBlank(Method method, String cacheName) { if (StrUtil.isBlank(cacheName)) { - throw new JbootException(String.format("CacheEvict.name() must not empty in method [%s].", - ClassUtil.buildMethodString(method))); + throw new IllegalStateException("Cache Name must not empty or blank in method: " + + ClassUtil.buildMethodString(method)); } } - static boolean isPrimitive(Class clazz) { + static boolean isSupportClass(Class clazz) { return clazz == String.class || clazz == Integer.class || clazz == int.class + || clazz == Short.class + || clazz == short.class || clazz == Long.class || clazz == long.class || clazz == Double.class @@ -131,47 +135,106 @@ class Utils { || clazz == float.class || clazz == Boolean.class || clazz == boolean.class + || clazz == char.class || clazz == BigDecimal.class || clazz == BigInteger.class || clazz == java.util.Date.class || clazz == java.sql.Date.class || clazz == java.sql.Timestamp.class - || clazz == java.sql.Time.class; + || clazz == java.sql.Time.class + || clazz == LocalDate.class + || clazz == LocalDateTime.class + || clazz == LocalTime.class + || clazz.isArray() + || Collection.class.isAssignableFrom(clazz) + || clazz == Columns.class + ; } - static String converteToString(Object object) { + static String convertToString(Object object, Method method) { if (object == null) { return "null"; } - if (!isPrimitive(object.getClass())) { - return null; + + if (!isSupportClass(object.getClass())) { + String msg = "Unsupport empty key for annotation @Cacheable, @CacheEvict or @CachePut " + + "at method [" + ClassUtil.buildMethodString(method) + "], " + + "please config key in the annotation."; + throw new IllegalArgumentException(msg); + } + + if (object.getClass().isArray()) { + StringBuilder ret = new StringBuilder(); + Object[] values = (Object[]) object; + int index = 0; + for (Object value : values) { + if (index == 0) { + ret.append('['); + } + ret.append(convertToString(value, method)); + if (++index != values.length) { + ret.append(','); + } else { + ret.append(']'); + } + } + return ret.toString(); + } + + if (object instanceof Collection) { + Collection c = (Collection) object; + StringBuilder ret = new StringBuilder(); + int index = 0; + for (Object o : c) { + if (index == 0) { + ret.append('['); + } + ret.append(convertToString(o, method)); + if (++index != c.size()) { + ret.append(','); + } else { + ret.append(']'); + } + } + return ret.toString(); } if (object instanceof java.util.Date) { return String.valueOf(((java.util.Date) object).getTime()); } + if (object instanceof LocalDateTime) { + return String.valueOf(DateUtil.toDate((LocalDateTime) object).getTime()); + } + + if (object instanceof LocalDate) { + return String.valueOf(DateUtil.toDate((LocalDate) object).getTime()); + } + + if (object instanceof LocalTime) { + return String.valueOf(DateUtil.toDate((LocalTime) object).getTime()); + } + + if (object instanceof Columns) { + return ((Columns) object).getCacheKey(); + } + return String.valueOf(object); } static boolean isUnless(String unlessString, Method method, Object[] arguments) { - if (StrUtil.isBlank(unlessString)) { return false; } - String template = new StringBuilder("#(") - .append(unlessString) - .append(")") - .toString(); - - return "true".equals(engineRender(template,method, arguments)); + unlessString = "#(" + unlessString + ")"; + return "true".equals(engineRender(unlessString, method, arguments)); } - static void doCacheEvict(Object[] arguments, Class targetClass, Method method, CacheEvict evict) { + static void removeCache(Object[] arguments, Class targetClass, Method method, CacheEvict evict, boolean isAction) { String unless = AnnotationUtil.get(evict.unless()); if (Utils.isUnless(unless, method, arguments)) { return; @@ -185,25 +248,21 @@ class Utils { String cacheKey = AnnotationUtil.get(evict.key()); - if (StrUtil.isBlank(cacheKey) || "*".equals(cacheKey)) { - AopCache.removeAll(cacheName); + if (StrUtil.isBlank(cacheKey) || "*".equals(cacheKey.trim())) { + if (isAction) { + ActionCache.removeAll(cacheName); + } else { + AopCache.removeAll(cacheName); + } } else { cacheKey = Utils.buildCacheKey(cacheKey, targetClass, method, arguments); - AopCache.remove(cacheName, cacheKey); + if (isAction) { + ActionCache.remove(cacheName, cacheKey); + } else { + AopCache.remove(cacheName, cacheKey); + } } } - private static final JbootCacheConfig CONFIG = Jboot.config(JbootCacheConfig.class); - - static void putDataToCache(int liveSeconds, String cacheName, String cacheKey, Object data) { - liveSeconds = liveSeconds > 0 - ? liveSeconds - : CONFIG.getAopCacheLiveSeconds(); - if (liveSeconds > 0) { - AopCache.put(cacheName, cacheKey, data, liveSeconds); - } else { - AopCache.put(cacheName, cacheKey, data); - } - } } diff --git a/src/main/java/io/jboot/components/cache/j2cache/J2cacheImpl.java b/src/main/java/io/jboot/components/cache/j2cache/J2cacheImpl.java index 7f9da59ae849dd3258d6f28daaedbc982f8064b1..6fe5c98805c046666560f84ba450be1e384f147f 100644 --- a/src/main/java/io/jboot/components/cache/j2cache/J2cacheImpl.java +++ b/src/main/java/io/jboot/components/cache/j2cache/J2cacheImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,53 +15,87 @@ */ package io.jboot.components.cache.j2cache; +import com.jfinal.log.Log; import com.jfinal.plugin.ehcache.IDataLoader; -import io.jboot.components.cache.JbootCache; +import io.jboot.components.cache.JbootCacheBase; +import io.jboot.components.cache.JbootCacheConfig; import io.jboot.exception.JbootException; +import net.oschina.j2cache.CacheChannel; import net.oschina.j2cache.CacheObject; import net.oschina.j2cache.J2Cache; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.core.cache.j2cache */ -public class J2cacheImpl implements JbootCache { +public class J2cacheImpl extends JbootCacheBase { + + private static final Log LOG = Log.getLog(J2cacheImpl.class); + + public J2cacheImpl(JbootCacheConfig config) { + super(config); + } @Override public T get(String cacheName, Object key) { + cacheName = buildCacheName(cacheName); CacheObject cacheObject = J2Cache.getChannel().get(cacheName, key.toString(), false); - return cacheObject != null ? (T) cacheObject.getValue() : null; + if (cacheObject != null) { + Object value = cacheObject.getValue(); + if (config.isDevMode()) { + println("J2cache GET: cacheName[" +cacheName+ "] cacheKey["+key+"] value:" + value); + } + return (T) value; + } else { + return null; + } } @Override public void put(String cacheName, Object key, Object value) { + cacheName = buildCacheName(cacheName); J2Cache.getChannel().set(cacheName, key.toString(), value); + + if (config.isDevMode()) { + println("J2cache PUT: cacheName[" +cacheName+ "] cacheKey["+key+"] value:" + value); + } } @Override public void put(String cacheName, Object key, Object value, int liveSeconds) { + cacheName = buildCacheName(cacheName); J2Cache.getChannel().set(cacheName, key.toString(), value, liveSeconds); - } - @Override - public List getKeys(String cacheName) { - Collection keys = J2Cache.getChannel().keys(cacheName); - return keys != null ? new ArrayList(keys) : null; + if (config.isDevMode()) { + println("J2cache PUT: cacheName[" +cacheName+ "] cacheKey["+key+"] value:" + value); + } } + @Override public void remove(String cacheName, Object key) { + cacheName = buildCacheName(cacheName); J2Cache.getChannel().evict(cacheName, key.toString()); + + if (config.isDevMode()) { + println("J2cache REMOVE: cacheName[" +cacheName+ "] cacheKey["+key+"]"); + } } @Override public void removeAll(String cacheName) { + cacheName = buildCacheName(cacheName); J2Cache.getChannel().clear(cacheName); + + if (config.isDevMode()) { + println("J2cache REMOVEALL: cacheName[" +cacheName+ "]"); + } } @Override @@ -73,6 +107,11 @@ public class J2cacheImpl implements JbootCache { put(cacheName, key, value); } } + + if (config.isDevMode()) { + println("J2cache GET: cacheName[" + buildCacheName(cacheName) + "] cacheKey[" + key + "] value:" + value); + } + return (T) value; } @@ -87,9 +126,89 @@ public class J2cacheImpl implements JbootCache { data = dataLoader.load(); put(cacheName, key, data, liveSeconds); } + + if (config.isDevMode()) { + println("J2cache GET: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + data); + } return (T) data; } + private Method sendEvictCmdMethod; + + @Override + public synchronized void refresh(String cacheName, Object key) { + cacheName = buildCacheName(cacheName); + if (sendEvictCmdMethod == null) { + sendEvictCmdMethod = getSendEvictCmdMethod(); + } + try { + if (sendEvictCmdMethod != null) { + sendEvictCmdMethod.invoke(J2Cache.getChannel(), cacheName, key); + } + } catch (Exception e) { + LOG.error("refresh error!", e); + } + } + + + private Method sendClearCmdMethod; + + @Override + public synchronized void refresh(String cacheName) { + cacheName = buildCacheName(cacheName); + if (sendClearCmdMethod == null) { + sendClearCmdMethod = getSendClearCmdMethod(); + } + try { + if (sendClearCmdMethod != null) { + sendClearCmdMethod.invoke(J2Cache.getChannel(), cacheName); + } + } catch (Exception e) { + LOG.error("refresh error!", e); + } + } + + @Override + public List getNames() { + Collection regions = J2Cache.getChannel().getL1Provider().regions(); + return regions != null && !regions.isEmpty() + ? regions.stream().map(CacheChannel.Region::getName).collect(Collectors.toList()) + : null; + } + + + @Override + public List getKeys(String cacheName) { + cacheName = buildCacheName(cacheName); + Collection keys = J2Cache.getChannel().keys(cacheName); + return keys != null ? new ArrayList(keys) : null; + } + + + private Method getSendEvictCmdMethod() { + try { + Method method = CacheChannel.class.getDeclaredMethod("sendEvictCmd", String.class, String[].class); + method.setAccessible(true); + return method; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + + private Method getSendClearCmdMethod() { + try { + Method method = CacheChannel.class.getDeclaredMethod("sendClearCmd", String.class); + method.setAccessible(true); + return method; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + @Override public Integer getTtl(String cacheName, Object key) { throw new JbootException("getTtl not support in j2cache"); diff --git a/src/main/java/io/jboot/components/cache/none/NoneCacheImpl.java b/src/main/java/io/jboot/components/cache/none/NoneCacheImpl.java index 77f00dd1fd3a403390614247f06e63c5da95b2d8..9e4e86cea139ab5dc84a73dc56ace42b539d292c 100644 --- a/src/main/java/io/jboot/components/cache/none/NoneCacheImpl.java +++ b/src/main/java/io/jboot/components/cache/none/NoneCacheImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package io.jboot.components.cache.none; import com.jfinal.plugin.ehcache.IDataLoader; import io.jboot.components.cache.JbootCache; +import io.jboot.components.cache.JbootCacheConfig; import java.util.List; @@ -25,6 +26,38 @@ import java.util.List; * noneCache 存在的目的:方便通过配置文件的方式关闭缓存功能 */ public class NoneCacheImpl implements JbootCache { + + private JbootCacheConfig config; + + public NoneCacheImpl(JbootCacheConfig config) { + this.config = config; + } + + @Override + public JbootCache setThreadCacheNamePrefix(String cacheNamePrefix) { + return this; + } + + @Override + public void clearThreadCacheNamePrefix() { + + } + + @Override + public boolean addThreadCacheNamePrefixIngore(String cacheName) { + return true; + } + + @Override + public boolean removeThreadCacheNamePrefixIngore(String cacheName) { + return true; + } + + @Override + public JbootCacheConfig getConfig() { + return config; + } + @Override public T get(String cacheName, Object key) { return null; @@ -40,11 +73,6 @@ public class NoneCacheImpl implements JbootCache { //do nothing } - @Override - public List getKeys(String cacheName) { - return null; - } - @Override public void remove(String cacheName, Object key) { //do nothing @@ -74,4 +102,24 @@ public class NoneCacheImpl implements JbootCache { public void setTtl(String cacheName, Object key, int seconds) { //do nothing } + + @Override + public void refresh(String cacheName, Object key) { + + } + + @Override + public void refresh(String cacheName) { + + } + + @Override + public List getNames() { + return null; + } + + @Override + public List getKeys(String cacheName) { + return null; + } } diff --git a/src/main/java/io/jboot/components/cache/redis/JbootRedisCacheConfig.java b/src/main/java/io/jboot/components/cache/redis/JbootRedisCacheConfig.java index d7d63b12649962ccdcbeab2fdb4c2ed72b17fb38..05ad4d43aa1c358af7088e0bf7553acb9ac1d386 100644 --- a/src/main/java/io/jboot/components/cache/redis/JbootRedisCacheConfig.java +++ b/src/main/java/io/jboot/components/cache/redis/JbootRedisCacheConfig.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -25,5 +25,16 @@ import io.jboot.support.redis.JbootRedisConfig; @ConfigModel(prefix = "jboot.cache.redis") public class JbootRedisCacheConfig extends JbootRedisConfig { + /** + * 全局的key前缀,所有缓存的key都会自动添加该前缀 + */ + private String globalKeyPrefix; + public String getGlobalKeyPrefix() { + return globalKeyPrefix; + } + + public void setGlobalKeyPrefix(String globalKeyPrefix) { + this.globalKeyPrefix = globalKeyPrefix; + } } diff --git a/src/main/java/io/jboot/components/cache/redis/JbootRedisCacheImpl.java b/src/main/java/io/jboot/components/cache/redis/JbootRedisCacheImpl.java index af5528e66e0d4d054c30d811b631c00736e3bbe3..956db724aa18baa3c10a56e58ffdecfa22909556 100644 --- a/src/main/java/io/jboot/components/cache/redis/JbootRedisCacheImpl.java +++ b/src/main/java/io/jboot/components/cache/redis/JbootRedisCacheImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,13 @@ package io.jboot.components.cache.redis; import com.jfinal.plugin.ehcache.IDataLoader; import io.jboot.Jboot; -import io.jboot.support.redis.JbootRedis; -import io.jboot.support.redis.JbootRedisManager; import io.jboot.components.cache.JbootCacheBase; +import io.jboot.components.cache.JbootCacheConfig; import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.support.redis.JbootRedis; +import io.jboot.support.redis.JbootRedisManager; +import io.jboot.support.redis.RedisScanResult; +import io.jboot.utils.StrUtil; import java.util.ArrayList; import java.util.List; @@ -31,39 +34,63 @@ public class JbootRedisCacheImpl extends JbootCacheBase { private JbootRedis redis; + private JbootRedisCacheConfig cacheConfig; + private String redisCacheNamesKey = "jboot_cache_names"; + private String globalKeyPrefix = ""; + + + public JbootRedisCacheImpl(JbootCacheConfig config) { + super(config); + + cacheConfig = Jboot.config(JbootRedisCacheConfig.class); - public JbootRedisCacheImpl() { - JbootRedisCacheConfig redisConfig = Jboot.config(JbootRedisCacheConfig.class); - if (redisConfig.isConfigOk()) { - redis = JbootRedisManager.me().getRedis(redisConfig); + if (StrUtil.isNotBlank(cacheConfig.getGlobalKeyPrefix())) { + globalKeyPrefix = cacheConfig.getGlobalKeyPrefix() + ":"; + redisCacheNamesKey = globalKeyPrefix + redisCacheNamesKey; + } + + + if (cacheConfig.isConfigOk()) { + redis = JbootRedisManager.me().getRedis(cacheConfig); } else { redis = Jboot.getRedis(); } if (redis == null) { - throw new JbootIllegalConfigException("can not get redis, please check your jboot.properties , please correct config jboot.cache.redis.host or jboot.redis.host "); + throw new JbootIllegalConfigException("Can not get redis component in JbootRedisCacheImpl, Please check your jboot.properties " + + "and config jboot.cache.redis.host or jboot.redis.host correct."); } } @Override public T get(String cacheName, Object key) { - return redis.get(buildKey(cacheName, key)); + T value = redis.get(buildKey(cacheName, key)); + + if (config.isDevMode()) { + println("RedisCache GET: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + value); + } + return value; } @Override public void put(String cacheName, Object key, Object value) { if (value == null) { - // if value is null : java.lang.NullPointerException: null at redis.clients.jedis.Protocol.sendCommand(Protocol.java:99) + remove(cacheName, key); return; } redis.set(buildKey(cacheName, key), value); + redis.sadd(buildCacheName(redisCacheNamesKey), cacheName); + + if (config.isDevMode()) { + println("RedisCache PUT: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + value); + } } @Override public void put(String cacheName, Object key, Object value, int liveSeconds) { if (value == null) { - // if value is null : java.lang.NullPointerException: null at redis.clients.jedis.Protocol.sendCommand(Protocol.java:99) + remove(cacheName, key); return; } if (liveSeconds <= 0) { @@ -72,34 +99,48 @@ public class JbootRedisCacheImpl extends JbootCacheBase { } redis.setex(buildKey(cacheName, key), liveSeconds, value); - } + redis.sadd(buildCacheName(redisCacheNamesKey), cacheName); - @Override - public List getKeys(String cacheName) { - Set keyset = redis.keys(cacheName + ":*"); - if (keyset == null || keyset.size() == 0) { - return null; - } - List keys = new ArrayList<>(keyset); - for (int i = 0; i < keys.size(); i++) { - keys.set(i, keys.get(i).substring(cacheName.length() + 3)); + if (config.isDevMode()) { + println("RedisCache PUT: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + value); } - return keys; } @Override public void remove(String cacheName, Object key) { redis.del(buildKey(cacheName, key)); + + if (config.isDevMode()) { + println("RedisCache REMOVE: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"]"); + } } @Override public void removeAll(String cacheName) { - String[] keys = new String[]{}; - keys = redis.keys(cacheName + ":*").toArray(keys); - if (keys != null && keys.length > 0) { - redis.del(keys); + String cursor = "0"; + int scanCount = 1000; + boolean continueState = true; + String scanName = globalKeyPrefix + buildCacheName(cacheName); + do { + RedisScanResult redisScanResult = redis.scan(scanName + ":*", cursor, scanCount); + List scanKeys = redisScanResult.getResults(); + cursor = redisScanResult.getCursor(); + + if (scanKeys != null && scanKeys.size() > 0) { + redis.del(scanKeys.toArray(new String[scanKeys.size()])); + } + + if (redisScanResult.isCompleteIteration()) { + continueState = false; + } + } while (continueState); + + redis.srem(buildCacheName(redisCacheNamesKey), cacheName); + + if (config.isDevMode()) { + println("RedisCache REMOVEALL: cacheName[" +buildCacheName(cacheName)+ "]"); } } @@ -111,22 +152,31 @@ public class JbootRedisCacheImpl extends JbootCacheBase { data = dataLoader.load(); put(cacheName, key, data); } + + if (config.isDevMode()) { + println("RedisCache GET: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + data); + } + return (T) data; } private String buildKey(String cacheName, Object key) { - if (key instanceof Number) - return String.format("%s:I:%s", cacheName, key); - else { - Class keyClass = key.getClass(); - if (String.class.equals(keyClass) || - StringBuffer.class.equals(keyClass) || - StringBuilder.class.equals(keyClass)) { - return String.format("%s:S:%s", cacheName, key); - } + cacheName = buildCacheName(cacheName); + StringBuilder keyBuilder = new StringBuilder(globalKeyPrefix) + .append(cacheName).append(":"); + + if (key instanceof String) { + keyBuilder.append("S"); + } else if (key instanceof Number) { + keyBuilder.append("I"); + } else if (key == null) { + keyBuilder.append("S"); + key = "null"; + } else { + keyBuilder.append("O"); } - return String.format("%s:O:%s", cacheName, key); + return keyBuilder.append(":").append(key).toString(); } @Override @@ -140,6 +190,10 @@ public class JbootRedisCacheImpl extends JbootCacheBase { data = dataLoader.load(); put(cacheName, key, data, liveSeconds); } + + if (config.isDevMode()) { + println("RedisCache GET: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] value:" + data); + } return (T) data; } @@ -150,9 +204,49 @@ public class JbootRedisCacheImpl extends JbootCacheBase { return ttl != null ? ttl.intValue() : null; } + @Override public void setTtl(String cacheName, Object key, int seconds) { redis.expire(buildKey(cacheName, key), seconds); + + if (config.isDevMode()) { + println("RedisCache SETTTL: cacheName[" +buildCacheName(cacheName)+ "] cacheKey["+key+"] seconds:" + seconds); + } + } + + + @Override + public List getNames() { + String key = buildCacheName(redisCacheNamesKey); + Set set = redis.smembers(key); + return set == null ? null : new ArrayList(set); + } + + + @Override + public List getKeys(String cacheName) { + cacheName = globalKeyPrefix + buildCacheName(cacheName); + List keys = new ArrayList<>(); + String cursor = "0"; + int scanCount = 1000; + boolean continueState = true; + do { + RedisScanResult redisScanResult = redis.scan(cacheName + ":*", cursor, scanCount); + List scanKeys = redisScanResult.getResults(); + cursor = redisScanResult.getCursor(); + + if (scanKeys != null && scanKeys.size() > 0) { + for (String key : scanKeys) { + keys.add(key.substring(cacheName.length() + 3)); + } + } + + if (redisScanResult.isCompleteIteration()) { + continueState = false; + } + } while (continueState); + + return keys; } public JbootRedis getRedis() { diff --git a/src/main/java/io/jboot/components/cache/support/JbootCaptchaCache.java b/src/main/java/io/jboot/components/cache/support/JbootCaptchaCache.java new file mode 100644 index 0000000000000000000000000000000000000000..4a614996062766ae6cb2025a7bb709662d408e21 --- /dev/null +++ b/src/main/java/io/jboot/components/cache/support/JbootCaptchaCache.java @@ -0,0 +1,39 @@ +package io.jboot.components.cache.support; + +import com.jfinal.captcha.Captcha; +import com.jfinal.captcha.ICaptchaCache; +import io.jboot.Jboot; +import io.jboot.components.cache.JbootCacheManager; +import io.jboot.utils.StrUtil; + +public class JbootCaptchaCache implements ICaptchaCache { + + public static final String CACHE_NAME = "jboot_captchas"; + + public JbootCaptchaCache() { + JbootCacheManager.me().getCache().addThreadCacheNamePrefixIngore(CACHE_NAME); + } + + @Override + public void put(Captcha captcha) { + Jboot.getCache().put(CACHE_NAME, captcha.getKey(), captcha, (int) ((captcha.getExpireAt() - System.currentTimeMillis()) / 1000)); + } + + @Override + public Captcha get(String key) { + return StrUtil.isBlank(key) ? null : Jboot.getCache().get(CACHE_NAME, key); + } + + @Override + public void remove(String key) { + if (StrUtil.isNotBlank(key)) { + Jboot.getCache().remove(CACHE_NAME, key); + } + } + + @Override + public void removeAll() { + Jboot.getCache().removeAll(CACHE_NAME); + } + +} diff --git a/src/main/java/io/jboot/components/cache/support/JbootTokenCache.java b/src/main/java/io/jboot/components/cache/support/JbootTokenCache.java new file mode 100644 index 0000000000000000000000000000000000000000..510f7417025967cf92c54f3503e018257ccc289c --- /dev/null +++ b/src/main/java/io/jboot/components/cache/support/JbootTokenCache.java @@ -0,0 +1,40 @@ +package io.jboot.components.cache.support; + +import com.jfinal.token.ITokenCache; +import com.jfinal.token.Token; +import io.jboot.Jboot; +import io.jboot.components.cache.JbootCacheManager; + +import java.util.List; + +public class JbootTokenCache implements ITokenCache { + + static final String CACHE_NAME = "jboot_tokens"; + + public JbootTokenCache() { + JbootCacheManager.me().getCache().addThreadCacheNamePrefixIngore(CACHE_NAME); + } + + @Override + public void put(Token token) { + Jboot.getCache().put(CACHE_NAME, token.getId(), token, (int) ((token.getExpirationTime() - System.currentTimeMillis()) / 1000)); + } + + @Override + public void remove(Token token) { + Jboot.getCache().remove(CACHE_NAME, token.getId()); + } + + @Override + public boolean contains(Token token) { + return Jboot.getCache().get(CACHE_NAME, token.getId()) != null; + } + + @Override + public List getAll() { + // 此处直接 return null 即可 + // 因为 JFinal 调用此方法的目的是为了去清除过期的 Token + // 但是,通过 Jboot 缓存,配置上过期时间时,其在过期的时候自动进行清除了,不再需要 JFinal 进行再次清除 + return null; + } +} diff --git a/src/main/java/io/jboot/components/cache/support/WechatAccessTokenCache.java b/src/main/java/io/jboot/components/cache/support/WechatAccessTokenCache.java new file mode 100644 index 0000000000000000000000000000000000000000..c015192c9df30e8e23bda28a3ade43e9f416b7a1 --- /dev/null +++ b/src/main/java/io/jboot/components/cache/support/WechatAccessTokenCache.java @@ -0,0 +1,36 @@ +package io.jboot.components.cache.support; + +import com.jfinal.weixin.sdk.cache.IAccessTokenCache; +import io.jboot.Jboot; +import io.jboot.components.cache.JbootCacheManager; + +public class WechatAccessTokenCache implements IAccessTokenCache { + + static final String CACHE_NAME = "wechat_access_tokens"; + + public WechatAccessTokenCache() { + JbootCacheManager.me().getCache() + .addThreadCacheNamePrefixIngore(CACHE_NAME); + } + + + @Override + public String get(String key) { + return Jboot.getCache().get(CACHE_NAME, key); + } + + + @Override + public void set(String key, String value) { + // 微信相关 token 的有效期之多 2 个小时 + // 如果设置为 7200,则有一定几率出现如下错误 + // {"errcode":40001,"errmsg":"invalid credential, access_token is invalid or not latest rid: **"} + Jboot.getCache().put(CACHE_NAME, key, value,7000); + } + + + @Override + public void remove(String key) { + Jboot.getCache().remove(CACHE_NAME, key); + } +} diff --git a/src/main/java/io/jboot/components/event/JbootEvent.java b/src/main/java/io/jboot/components/event/JbootEvent.java index 6e150b96cd073e1114649e1d5426b60dfccd15ee..960d68ffed862cce04242acfe38f4619825a32a6 100644 --- a/src/main/java/io/jboot/components/event/JbootEvent.java +++ b/src/main/java/io/jboot/components/event/JbootEvent.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/event/JbootEventListener.java b/src/main/java/io/jboot/components/event/JbootEventListener.java index 6e4ce76936910e70b5a4ebe3f1ff891e7ef7251d..fe80b17a516c01ab4d24f7accc2e9ad714f7e933 100644 --- a/src/main/java/io/jboot/components/event/JbootEventListener.java +++ b/src/main/java/io/jboot/components/event/JbootEventListener.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -17,6 +17,6 @@ package io.jboot.components.event; public interface JbootEventListener { - public void onEvent(JbootEvent event); + void onEvent(JbootEvent event); } diff --git a/src/main/java/io/jboot/components/event/JbootEventManager.java b/src/main/java/io/jboot/components/event/JbootEventManager.java index 43405fc94101585639edba44fb7e32f059f235e1..272595dc2c7f2349f39103b3bfa59cc19f77645c 100644 --- a/src/main/java/io/jboot/components/event/JbootEventManager.java +++ b/src/main/java/io/jboot/components/event/JbootEventManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,27 +27,25 @@ import java.util.concurrent.*; public class JbootEventManager { - private final ExecutorService threadPool; + private static final Log LOG = Log.getLog(JbootEventManager.class); + private static JbootEventManager manager = new JbootEventManager(); + private final Map> asyncListenerMap; private final Map> listenerMap; - private static final Log log = Log.getLog(JbootEventManager.class); - private static JbootEventManager manager; - public JbootEventManager() { - threadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new SynchronousQueue<>(), new NamedThreadFactory("jbootevent")); + private ExecutorService threadPool; + + + private JbootEventManager() { asyncListenerMap = new ConcurrentHashMap<>(); listenerMap = new ConcurrentHashMap<>(); + threadPool = NamedThreadPools.newFixedThreadPool("jboot-event"); initListeners(); } public static JbootEventManager me() { - if (manager == null) { - manager = ClassUtil.singleton(JbootEventManager.class); - } return manager; } @@ -67,7 +65,7 @@ public class JbootEventManager { deleteListner(asyncListenerMap, listenerClass); if (Jboot.isDevMode()) { - log.debug(String.format("listener[%s]-->>unRegisterListener.", listenerClass)); + LOG.debug(String.format("listener[%s]-->>unRegisterListener.", listenerClass)); } } @@ -100,7 +98,7 @@ public class JbootEventManager { String[] actions = AnnotationUtil.get(listenerAnnotation.action()); if (actions == null) { - log.warn("listenerClass[" + listenerAnnotation + "] register fail, because action is null or blank."); + LOG.warn("listenerClass[" + listenerAnnotation + "] register fail, because action is null or blank."); return; } @@ -154,7 +152,7 @@ public class JbootEventManager { } if (Jboot.isDevMode()) { - log.debug(String.format("listener[%s]-->>registered.", eventListener)); + LOG.debug(String.format("listener[%s]-->>registered.", eventListener)); } } @@ -179,7 +177,7 @@ public class JbootEventManager { return false; } - public void pulish(final JbootEvent event) { + public void publish(final JbootEvent event) { String action = event.getAction(); List syncListeners = listenerMap.get(action); @@ -197,12 +195,9 @@ public class JbootEventManager { private void invokeListeners(final JbootEvent event, List syncListeners) { for (final JbootEventListener listener : syncListeners) { try { - if (Jboot.isDevMode()) { - log.debug(String.format("listener[%s]-->>onEvent(%s)", listener, event)); - } listener.onEvent(event); } catch (Throwable e) { - log.error(String.format("listener[%s] onEvent is error! ", listener.getClass()), e); + LOG.error(String.format("listener[%s] onEvent is error! ", listener.getClass()), e); } } } @@ -211,15 +206,19 @@ public class JbootEventManager { for (final JbootEventListener listener : listeners) { threadPool.execute(() -> { try { - if (Jboot.isDevMode()) { - log.debug(String.format("listener[%s]-->>onEvent(%s) in async", listener, event)); - } listener.onEvent(event); } catch (Throwable e) { - log.error(String.format("listener[%s] onEvent is error! ", listener.getClass()), e); + LOG.error(String.format("listener[%s] onEvent is error! ", listener.getClass()), e); } }); } } + public ExecutorService getThreadPool() { + return threadPool; + } + + public void setThreadPool(ExecutorService threadPool) { + this.threadPool = threadPool; + } } diff --git a/src/main/java/io/jboot/components/event/annotation/EventConfig.java b/src/main/java/io/jboot/components/event/annotation/EventConfig.java index 11c85bc5e82c6497710c4374f10a1a25cdab69bf..9266631c71573658460e45364121c6ffce9b9d28 100644 --- a/src/main/java/io/jboot/components/event/annotation/EventConfig.java +++ b/src/main/java/io/jboot/components/event/annotation/EventConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package io.jboot.components.event.annotation; import java.lang.annotation.*; +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented diff --git a/src/main/java/io/jboot/components/gateway/GatewayErrorRender.java b/src/main/java/io/jboot/components/gateway/GatewayErrorRender.java new file mode 100644 index 0000000000000000000000000000000000000000..b4408320ceb952e2bcc4b42229430af6ef1d5b59 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/GatewayErrorRender.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +import com.jfinal.kit.Ret; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public interface GatewayErrorRender { + + Ret noneHealthUrl = Ret.fail().set("errorCode", 1).set("message", "No healthy url in Gateway."); + Ret sentinelBlockedError = Ret.fail().set("errorCode", 3).set("message", "Blocked by Sentinel (flow limiting) in Jboot."); + + + void renderError(Exception error, Ret errorMessage, JbootGatewayConfig config, HttpServletRequest request, HttpServletResponse response); +} diff --git a/src/main/java/io/jboot/components/gateway/GatewayHttpProxy.java b/src/main/java/io/jboot/components/gateway/GatewayHttpProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..c1e1d54bc770e84e23670038791f3d2f0cde7af9 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/GatewayHttpProxy.java @@ -0,0 +1,383 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +import com.jfinal.kit.LogKit; +import com.jfinal.log.Log; +import io.jboot.exception.JbootException; +import io.jboot.utils.StrUtil; + +import javax.net.ssl.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.zip.GZIPInputStream; + +public class GatewayHttpProxy { + + private static final Log LOG = Log.getLog(GatewayHttpProxy.class); + + private int readTimeOut = 10000; //10s + private int connectTimeOut = 5000; //5s + private int retries = 2; + private String contentType = JbootGatewayConfig.DEFAULT_PROXY_CONTENT_TYPE; + + + private boolean instanceFollowRedirects = false; + private boolean useCaches = false; + + private Map headers; + + private Exception exception; + + + public GatewayHttpProxy() { + } + + + public GatewayHttpProxy(JbootGatewayConfig config) { + this.readTimeOut = config.getProxyReadTimeout(); + this.connectTimeOut = config.getProxyConnectTimeout(); + this.retries = config.getProxyRetries(); + this.contentType = config.getProxyContentType(); + } + + + public void sendRequest(String url, HttpServletRequest req, HttpServletResponse resp) { + int triesCount = Math.max(retries, 0); + Exception exception = null; + + do { + try { + exception = null; + doSendRequest(url, req, resp); + } catch (Exception ex) { + exception = ex; + } + } while (exception != null && triesCount-- > 0); + + if (exception != null) { + this.exception = exception; + LOG.error(exception.toString(), exception); + } + } + + + protected void doSendRequest(String url, HttpServletRequest req, HttpServletResponse resp) throws Exception { + + HttpURLConnection conn = null; + try { + conn = getConnection(url); + + /** + * 配置 HttpURLConnection 的 http 请求头 + */ + configConnection(conn, req); + + + // get 请求 + if ("get".equalsIgnoreCase(req.getMethod())) { + conn.connect(); + } + // post 请求 + else { + conn.setDoOutput(true); + conn.setDoInput(true); + copyRequestStreamToConnection(req, conn); + } + + + /** + * 配置 HttpServletResponse 的 http 响应头 + */ + configResponse(resp, conn); + + /** + * 复制链接的 inputStream 流到 Response + */ + copyConnStreamToResponse(conn, resp); + + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + + protected void copyRequestStreamToConnection(HttpServletRequest req, HttpURLConnection conn) throws IOException { + OutputStream outStream = null; + InputStream inStream = null; + try { + outStream = conn.getOutputStream(); + inStream = req.getInputStream(); + int len; + byte[] buffer = new byte[1024]; + while ((len = inStream.read(buffer)) != -1) { + outStream.write(buffer, 0, len); + } + } finally { + quetlyClose(outStream, inStream); + } + } + + + protected void copyConnStreamToResponse(HttpURLConnection conn, HttpServletResponse resp) throws IOException { + if (resp.isCommitted()) { + return; + } + + InputStream inStream = null; + OutputStream outStream = null; + try { + inStream = getInputStream(conn); + outStream = resp.getOutputStream(); + byte[] buffer = new byte[1024]; + for (int len; (len = inStream.read(buffer)) != -1; ) { + outStream.write(buffer, 0, len); + } +// outStream.flush(); + } finally { + quetlyClose(inStream); + } + } + + + protected void quetlyClose(Closeable... closeables) { + for (Closeable closeable : closeables) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + LogKit.logNothing(e); + } + } + } + } + + + protected void configResponse(HttpServletResponse resp, HttpURLConnection conn) throws IOException { + + if (resp.isCommitted()) { + return; + } + + resp.setStatus(conn.getResponseCode()); + + //conn 是否已经指定了 contentType,如果指定了,就用 conn 的,否则就用自己配置的 + boolean isContentTypeSetted = false; + + Map> headerFields = conn.getHeaderFields(); + if (headerFields != null && !headerFields.isEmpty()) { + Set headerNames = headerFields.keySet(); + for (String headerName : headerNames) { + //需要排除 Content-Encoding,因为 Server 可能已经使用 gzip 压缩,但是此代理已经对 gzip 内容进行解压了 + if (StrUtil.isBlank(headerName) || "Content-Encoding".equalsIgnoreCase(headerName)) { + continue; + } + + String headerFieldValue = conn.getHeaderField(headerName); + if (StrUtil.isNotBlank(headerFieldValue)) { + resp.setHeader(headerName, headerFieldValue); + if ("Content-Type".equalsIgnoreCase(headerName)) { + isContentTypeSetted = true; + } + } + } + } + + //conn 没有 Content-Type,需要设置为手动配置的内容 + if (!isContentTypeSetted) { + resp.setContentType(contentType); + } + } + + protected InputStream getInputStream(HttpURLConnection conn) throws IOException { + InputStream stream = conn.getResponseCode() >= 400 + ? conn.getErrorStream() + : conn.getInputStream(); + + if ("gzip".equalsIgnoreCase(conn.getContentEncoding())) { + return new GZIPInputStream(stream); + } else { + return stream; + } + } + + + protected void configConnection(HttpURLConnection conn, HttpServletRequest req) throws ProtocolException { + + conn.setReadTimeout(readTimeOut); + conn.setConnectTimeout(connectTimeOut); + conn.setInstanceFollowRedirects(instanceFollowRedirects); + conn.setUseCaches(useCaches); + + conn.setRequestMethod(req.getMethod()); + + Enumeration headerNames = req.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + if (StrUtil.isNotBlank(headerName)) { + String headerFieldValue = req.getHeader(headerName); + if (StrUtil.isNotBlank(headerFieldValue)) { + conn.setRequestProperty(headerName, headerFieldValue); + } + } + } + + if (this.headers != null) { + for (Map.Entry entry : this.headers.entrySet()) { + conn.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + } + + protected HttpURLConnection getConnection(String urlString) { + try { + if (urlString.toLowerCase().startsWith("https")) { + return getHttpsConnection(urlString); + } else { + return getHttpConnection(urlString); + } + } catch (Throwable ex) { + throw new JbootException(ex); + } + } + + protected HttpURLConnection getHttpConnection(String urlString) throws Exception { + URL url = new URL(urlString); + return (HttpURLConnection) url.openConnection(); + } + + protected HttpsURLConnection getHttpsConnection(String urlString) throws Exception { + + SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); + TrustManager[] tm = {trustAnyTrustManager}; + sslContext.init(null, tm, null); + SSLSocketFactory ssf = sslContext.getSocketFactory(); + + URL url = new URL(urlString); + HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + conn.setHostnameVerifier(hnv); + conn.setSSLSocketFactory(ssf); + return conn; + } + + protected static X509TrustManager trustAnyTrustManager = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + protected static HostnameVerifier hnv = (hostname, session) -> true; + + + public Exception getException() { + return exception; + } + + public void setException(Exception exception) { + this.exception = exception; + } + + public int getReadTimeOut() { + return readTimeOut; + } + + public void setReadTimeOut(int readTimeOut) { + this.readTimeOut = readTimeOut; + } + + public int getConnectTimeOut() { + return connectTimeOut; + } + + public void setConnectTimeOut(int connectTimeOut) { + this.connectTimeOut = connectTimeOut; + } + + public int getRetries() { + return retries; + } + + public void setRetries(int retries) { + this.retries = retries; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public boolean isInstanceFollowRedirects() { + return instanceFollowRedirects; + } + + public void setInstanceFollowRedirects(boolean instanceFollowRedirects) { + this.instanceFollowRedirects = instanceFollowRedirects; + } + + public boolean isUseCaches() { + return useCaches; + } + + public void setUseCaches(boolean useCaches) { + this.useCaches = useCaches; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public GatewayHttpProxy addHeader(String key, String value) { + if (this.headers == null) { + this.headers = new HashMap<>(); + } + this.headers.put(key, value); + return this; + } + + public GatewayHttpProxy addHeaders(Map headers) { + if (this.headers == null) { + this.headers = new HashMap<>(); + } + this.headers.putAll(headers); + return this; + } + + +} diff --git a/src/main/java/io/jboot/components/gateway/GatewayInterceptor.java b/src/main/java/io/jboot/components/gateway/GatewayInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..289545ccc51678d123b5f955760535bf2d49148a --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/GatewayInterceptor.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +/** + * GatewayInterceptor. + */ +public interface GatewayInterceptor { + + void intercept(GatewayInvocation inv); + +} + diff --git a/src/main/java/io/jboot/components/gateway/GatewayInvocation.java b/src/main/java/io/jboot/components/gateway/GatewayInvocation.java new file mode 100644 index 0000000000000000000000000000000000000000..b87abd8330699574fa1f395910641fabf2b865d7 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/GatewayInvocation.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +import com.jfinal.kit.Ret; +import io.jboot.Jboot; +import io.jboot.utils.StrUtil; +import io.jboot.web.render.JbootJsonRender; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.ConnectException; + + +public class GatewayInvocation { + + private JbootGatewayConfig config; + private GatewayInterceptor[] inters; + private HttpServletRequest request; + private HttpServletResponse response; + private GatewayHttpProxy proxy; + private String proxyUrl; + + //是否跳过错误渲染,如果跳过,那么则由拦截器通过 getResponse() 自行渲染 + private boolean skipExceptionRender = false; + + private static boolean devMode = Jboot.isDevMode(); + + private int index = 0; + + + public GatewayInvocation(JbootGatewayConfig config, HttpServletRequest request, HttpServletResponse response) { + this.config = config; + this.request = request; + this.response = response; + this.inters = config.getGatewayInterceptors(); + this.proxy = new GatewayHttpProxy(config); + this.proxyUrl = buildProxyUrl(config, request); + } + + + public void invoke() { + if (inters.length == 0) { + doInvoke(); + return; + } + if (index < inters.length) { + inters[index++].intercept(this); + } else if (index++ >= inters.length) { + doInvoke(); + } + } + + + protected void doInvoke() { + if (StrUtil.isBlank(proxyUrl)) { + renderError(null, GatewayErrorRender.noneHealthUrl, config, request, response); + return; + } + + if (devMode) { + System.out.println("Jboot Gateway >>> " + proxyUrl); + } + + //启用 Sentinel 限流 + if (config.isSentinelEnable()) { + new GatewaySentinelProcesser().process(proxy, proxyUrl, config, request, response, skipExceptionRender); + }else { + + //未启用 Sentinel 的情况 + proxy.sendRequest(proxyUrl, request, response); + + Exception exception = proxy.getException(); + if (exception != null && !skipExceptionRender) { + if (exception instanceof ConnectException) { + Ret connectionError = Ret.fail().set("errorCode", 2).set("message", "Can not connect to target server: " + proxyUrl); + renderError(exception, connectionError, config, request, response); + } else { + Ret ret = Ret.fail().set("errorCode", 9).set("message", exception.getMessage()); + renderError(exception, ret, config, request, response); + } + } + } + + } + + + private static void renderError(Exception error, Ret errorMessage, JbootGatewayConfig config, HttpServletRequest request, HttpServletResponse response) { + GatewayErrorRender errorRender = JbootGatewayManager.me().getGatewayErrorRender(); + if (errorRender != null) { + errorRender.renderError(error, errorMessage, config, request, response); + } else { + new JbootJsonRender(errorMessage).setContext(request, response).render(); + } + } + + + private static String buildProxyUrl(JbootGatewayConfig config, HttpServletRequest request) { + //配置负载均衡策略 + GatewayLoadBalanceStrategy lbs = config.buildLoadBalanceStrategy(); + + //通过负载均衡策略获取 URL 地址 + String url = lbs.getUrl(config, request); + if (StrUtil.isBlank(url)) { + return null; + } + + StringBuilder sb = new StringBuilder(url); + if (StrUtil.isNotBlank(request.getRequestURI())) { + sb.append(request.getRequestURI()); + } + + if (StrUtil.isNotBlank(request.getQueryString())) { + sb.append("?").append(request.getQueryString()); + } + + return sb.toString(); + } + + + public JbootGatewayConfig getConfig() { + return config; + } + + public void setConfig(JbootGatewayConfig config) { + this.config = config; + } + + public GatewayInterceptor[] getInters() { + return inters; + } + + public void setInters(GatewayInterceptor[] inters) { + this.inters = inters; + } + + public HttpServletRequest getRequest() { + return request; + } + + public void setRequest(HttpServletRequest request) { + this.request = request; + } + + public HttpServletResponse getResponse() { + return response; + } + + public void setResponse(HttpServletResponse response) { + this.response = response; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public GatewayHttpProxy getProxy() { + return proxy; + } + + + public void setProxy(GatewayHttpProxy proxy) { + this.proxy = proxy; + } + + public boolean hasException() { + return proxy.getException() != null; + } + + public String getProxyUrl() { + return proxyUrl; + } + + public void setProxyUrl(String proxyUrl) { + this.proxyUrl = proxyUrl; + } + + public boolean isSkipExceptionRender() { + return skipExceptionRender; + } + + public void setSkipExceptionRender(boolean skipExceptionRender) { + this.skipExceptionRender = skipExceptionRender; + } +} diff --git a/src/main/java/io/jboot/components/gateway/GatewayLoadBalanceStrategy.java b/src/main/java/io/jboot/components/gateway/GatewayLoadBalanceStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..2f8b71420bc77c6172dc58cf21df127d5f2c44e1 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/GatewayLoadBalanceStrategy.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +import javax.servlet.http.HttpServletRequest; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 负载均衡策略 + */ +public interface GatewayLoadBalanceStrategy { + + /** + * 默认的负载均衡策略,随机返回一个 url + */ + GatewayLoadBalanceStrategy DEFAULT_STRATEGY = (config, request) -> { + String[] urls = config.getHealthUris(); + if (urls == null || urls.length == 0) { + return null; + } else if (urls.length == 1) { + return urls[0]; + } else { + return urls[ThreadLocalRandom.current().nextInt(urls.length)]; + } + }; + + String getUrl(JbootGatewayConfig config, HttpServletRequest request); +} diff --git a/src/main/java/io/jboot/components/gateway/GatewaySentinelProcesser.java b/src/main/java/io/jboot/components/gateway/GatewaySentinelProcesser.java new file mode 100644 index 0000000000000000000000000000000000000000..d2f20d35bb272fd3f80f3b589f2eb22e3c688783 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/GatewaySentinelProcesser.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +import com.alibaba.csp.sentinel.*; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.jfinal.kit.LogKit; +import io.jboot.support.sentinel.SentinelUtil; +import io.jboot.utils.StrUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class GatewaySentinelProcesser { + + + public void process(GatewayHttpProxy proxy, String proxyUrl, JbootGatewayConfig config, HttpServletRequest req, HttpServletResponse resp, boolean skipExceptionRender) { + Entry entry = null; + String resourceName = SentinelUtil.buildResource(req); + try { + entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_API_GATEWAY, EntryType.IN); + proxy.sendRequest(proxyUrl, req, resp); + } catch (BlockException ex) { + if (skipExceptionRender) { + GatewayErrorRender errorRender = JbootGatewayManager.me().getGatewayErrorRender(); + if (errorRender != null) { + errorRender.renderError(ex, GatewayErrorRender.sentinelBlockedError, config, req, resp); + } else { + processBlocked(config, req, resp); + } + } else { + proxy.setException(ex); + } + } finally { + if (proxy.getException() != null) { + Tracer.traceEntry(proxy.getException(), entry); + } + if (entry != null) { + entry.exit(); + } + } + } + + + private void processBlocked(JbootGatewayConfig config, HttpServletRequest req, HttpServletResponse resp) { + StringBuffer url = req.getRequestURL(); + + if ("GET".equalsIgnoreCase(req.getMethod()) && StrUtil.isNotBlank(req.getQueryString())) { + url.append("?").append(req.getQueryString()); + } + + try { + if (StringUtil.isNotBlank(config.getSentinelBlockPage())) { + String redirectUrl = config.getSentinelBlockPage() + "?http_referer=" + url.toString(); + resp.sendRedirect(redirectUrl); + } else if (config.getSentinelBlockJsonMap() != null && !config.getSentinelBlockJsonMap().isEmpty()) { + SentinelUtil.writeDefaultBlockedJson(resp, config.getSentinelBlockJsonMap()); + } else { + SentinelUtil.writeDefaultBlockedPage(resp); + } + } catch (IOException ex) { + LogKit.error(ex.toString(), ex); + } + } + + +} diff --git a/src/main/java/io/jboot/components/gateway/JbootGatewayConfig.java b/src/main/java/io/jboot/components/gateway/JbootGatewayConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2db17662ba10a79bc409afea66223db871e7ed56 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/JbootGatewayConfig.java @@ -0,0 +1,512 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +import io.jboot.core.spi.JbootSpiLoader; +import io.jboot.utils.ArrayUtil; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.StrUtil; + +import javax.servlet.http.HttpServletRequest; +import java.io.Serializable; +import java.util.*; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/22 + */ +public class JbootGatewayConfig implements Serializable { + + public static final String DEFAULT_PROXY_CONTENT_TYPE = "text/html;charset=utf-8"; + public static final GatewayInterceptor[] EMPTY_GATEWAY_INTERCEPTOR_ARRAY = new GatewayInterceptor[0]; + + private String name; + private Set uri; + + + // 是否启用健康检查 + private boolean uriHealthCheckEnable; + + // URI 健康检查路径,要求服务 statusCode = 200 + // 当配置 uriHealthCheckPath 后,健康检查的 url 地址为 uri + uriHealthCheckPath + private String uriHealthCheckPath; + + // 是否启用 + private boolean enable = false; + + // 是否启用 sentinel 限流 + private boolean sentinelEnable = false; + // sentinel 被限流后跳转地址 + private String sentinelBlockPage; + // sentinel 被渲染的json内容,如果配置 sentinelBlockPage,则 sentinelBlockJsonMap 配置无效 + private Map sentinelBlockJsonMap; + + // 设置 http 代理的参数 + private int proxyReadTimeout = 10000; //10s + private int proxyConnectTimeout = 5000; //5s + private int proxyRetries = 2; //2 times + private String proxyContentType = DEFAULT_PROXY_CONTENT_TYPE; + + + private String[] pathEquals; + private String[] pathContains; + private String[] pathStartsWith; + private String[] pathEndsWith; + + + private String[] hostEquals; + private String[] hostContains; + private String[] hostStartsWith; + private String[] hostEndsWith; + + + private Map queryEquals; + private String[] queryContains; + + //拦截器配置,一般可以用于对请求进行 鉴权 等处理 + private String[] interceptors; + private String loadBalanceStrategy; + +// 暂时不支持 cookie +// private Map cookieEquals; +// private String[] cookieContains; + + //不健康的 URI 地址 + private Set unHealthUris = Collections.synchronizedSet(new HashSet<>()); + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + + public Set getUri() { + return uri; + } + + public void setUri(Set uri) { + this.uri = uri; + } + + //服务发现的 URI 列表 + private Set discoveryUris; + + //健康的 URI 缓存 + private String[] healthUris; + + //健康 URI 是否有变化的标识 + private boolean healthUriChanged = true; + + public String[] getHealthUris() { + if (healthUriChanged) { + synchronized (this) { + if (healthUriChanged) { + if ((uri == null || uri.isEmpty()) && (discoveryUris == null || discoveryUris.isEmpty())) { + healthUris = null; + } else { + HashSet healthUriSet = new HashSet<>(); + if (uri != null && !uri.isEmpty()) { + healthUriSet.addAll(uri); + } + + if (discoveryUris != null && !discoveryUris.isEmpty()) { + healthUriSet.addAll(discoveryUris); + } + + if (!unHealthUris.isEmpty()) { + healthUriSet.removeAll(unHealthUris); + } + healthUris = healthUriSet.isEmpty() ? null : healthUriSet.toArray(new String[healthUriSet.size()]); + + } + healthUriChanged = false; + } + } + } + return healthUris; + } + + public boolean isUriHealthCheckEnable() { + return uriHealthCheckEnable; + } + + public void setUriHealthCheckEnable(boolean uriHealthCheckEnable) { + this.uriHealthCheckEnable = uriHealthCheckEnable; + } + + public String getUriHealthCheckPath() { + return uriHealthCheckPath; + } + + public void setUriHealthCheckPath(String uriHealthCheckPath) { + this.uriHealthCheckPath = uriHealthCheckPath; + } + + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public boolean isSentinelEnable() { + return sentinelEnable; + } + + public void setSentinelEnable(boolean sentinelEnable) { + this.sentinelEnable = sentinelEnable; + } + + public String getSentinelBlockPage() { + return sentinelBlockPage; + } + + public void setSentinelBlockPage(String sentinelBlockPage) { + this.sentinelBlockPage = sentinelBlockPage; + } + + public Map getSentinelBlockJsonMap() { + return sentinelBlockJsonMap; + } + + public void setSentinelBlockJsonMap(Map sentinelBlockJsonMap) { + this.sentinelBlockJsonMap = sentinelBlockJsonMap; + } + + public int getProxyReadTimeout() { + return proxyReadTimeout; + } + + public void setProxyReadTimeout(int proxyReadTimeout) { + this.proxyReadTimeout = proxyReadTimeout; + } + + public int getProxyConnectTimeout() { + return proxyConnectTimeout; + } + + public void setProxyConnectTimeout(int proxyConnectTimeout) { + this.proxyConnectTimeout = proxyConnectTimeout; + } + + public int getProxyRetries() { + return proxyRetries; + } + + public void setProxyRetries(int proxyRetries) { + this.proxyRetries = proxyRetries; + } + + public String getProxyContentType() { + return proxyContentType; + } + + public void setProxyContentType(String proxyContentType) { + this.proxyContentType = proxyContentType; + } + + public String[] getPathEquals() { + return pathEquals; + } + + public void setPathEquals(String[] pathEquals) { + this.pathEquals = pathEquals; + } + + public String[] getPathContains() { + return pathContains; + } + + public void setPathContains(String[] pathContains) { + this.pathContains = pathContains; + } + + public String[] getPathStartsWith() { + return pathStartsWith; + } + + public void setPathStartsWith(String[] pathStartsWith) { + this.pathStartsWith = pathStartsWith; + } + + public String[] getPathEndsWith() { + return pathEndsWith; + } + + public void setPathEndsWith(String[] pathEndsWith) { + this.pathEndsWith = pathEndsWith; + } + + public String[] getHostEquals() { + return hostEquals; + } + + public void setHostEquals(String[] hostEquals) { + this.hostEquals = hostEquals; + } + + public String[] getHostContains() { + return hostContains; + } + + public void setHostContains(String[] hostContains) { + this.hostContains = hostContains; + } + + public String[] getHostStartsWith() { + return hostStartsWith; + } + + public void setHostStartsWith(String[] hostStartsWith) { + this.hostStartsWith = hostStartsWith; + } + + public String[] getHostEndsWith() { + return hostEndsWith; + } + + public void setHostEndsWith(String[] hostEndsWith) { + this.hostEndsWith = hostEndsWith; + } + + public Map getQueryEquals() { + return queryEquals; + } + + public void setQueryEquals(Map queryEquals) { + this.queryEquals = queryEquals; + } + + public String[] getQueryContains() { + return queryContains; + } + + public void setQueryContains(String[] queryContains) { + this.queryContains = queryContains; + } + + public String[] getInterceptors() { + return interceptors; + } + + public void setInterceptors(String[] interceptors) { + this.interceptors = interceptors; + } + + + private GatewayInterceptor[] gatewayInterceptors; + + public GatewayInterceptor[] getGatewayInterceptors() { + if (gatewayInterceptors == null) { + if (interceptors == null || interceptors.length == 0) { + gatewayInterceptors = EMPTY_GATEWAY_INTERCEPTOR_ARRAY; + } else { + gatewayInterceptors = new GatewayInterceptor[interceptors.length]; + for (int i = 0; i < interceptors.length; i++) { +// GatewayInterceptor interceptor = ClassUtil.newInstance(interceptors[i]); + GatewayInterceptor interceptor = JbootSpiLoader.load(GatewayInterceptor.class, interceptors[i]); + if (interceptor == null) { + throw new NullPointerException("can not new instance by class:" + interceptors[i]); + } + gatewayInterceptors[i] = interceptor; + } + } + } + return gatewayInterceptors; + } + + + public void setGatewayInterceptors(GatewayInterceptor[] gatewayInterceptors) { + this.gatewayInterceptors = gatewayInterceptors; + } + + + public String getLoadBalanceStrategy() { + return loadBalanceStrategy; + } + + public void setLoadBalanceStrategy(String loadBalanceStrategy) { + this.loadBalanceStrategy = loadBalanceStrategy; + } + + private GatewayLoadBalanceStrategy gatewayLoadBalanceStrategy; + + public GatewayLoadBalanceStrategy buildLoadBalanceStrategy() { + if (gatewayLoadBalanceStrategy != null) { + return gatewayLoadBalanceStrategy; + } + + if (StrUtil.isBlank(loadBalanceStrategy)) { + gatewayLoadBalanceStrategy = GatewayLoadBalanceStrategy.DEFAULT_STRATEGY; + } else { + GatewayLoadBalanceStrategy glbs = ClassUtil.newInstance(loadBalanceStrategy); + if (glbs == null) { + throw new NullPointerException("Can not new instance by class: " + loadBalanceStrategy); + } + gatewayLoadBalanceStrategy = glbs; + } + + return gatewayLoadBalanceStrategy; + } + + public void setGatewayLoadBalanceStrategy(GatewayLoadBalanceStrategy strategy) { + this.gatewayLoadBalanceStrategy = strategy; + } + + + public boolean matches(HttpServletRequest request) { + if (request == null) { + return false; + } + + String path = request.getServletPath(); + if (pathEquals != null) { + for (String p : pathEquals) { + if (path.equals(p)) { + return true; + } + } + } + + if (pathContains != null) { + for (String p : pathContains) { + if (path.contains(p)) { + return true; + } + } + } + + if (pathStartsWith != null) { + for (String p : pathStartsWith) { + if (path.startsWith(p)) { + return true; + } + } + } + + if (pathEndsWith != null) { + for (String p : pathEndsWith) { + if (path.endsWith(p)) { + return true; + } + } + } + + String host = request.getServerName(); + if (hostEquals != null) { + for (String h : hostEquals) { + if (host.equals(h)) { + return true; + } + } + } + + if (hostContains != null) { + for (String h : hostContains) { + if (host.contains(h)) { + return true; + } + } + } + + if (hostStartsWith != null) { + for (String h : hostStartsWith) { + if (host.startsWith(h)) { + return true; + } + } + } + + if (hostEndsWith != null) { + for (String h : hostEndsWith) { + if (host.endsWith(h)) { + return true; + } + } + } + + if (queryContains != null || queryEquals != null) { + Map queryMap = StrUtil.queryStringToMap(request.getQueryString()); + if (!queryMap.isEmpty()) { + + if (queryContains != null) { + for (String q : queryContains) { + if (queryMap.containsKey(q)) { + return true; + } + } + } + + if (queryEquals != null) { + for (Map.Entry e : queryEquals.entrySet()) { + String queryValue = queryMap.get(e.getKey()); + if (Objects.equals(queryValue, e.getValue())) { + return true; + } + } + } + } + } + + + return false; + } + + public void syncDiscoveryUris(Collection syncUris) { + if (ArrayUtil.isSameElements(syncUris, discoveryUris)) { + return; + } + + if (syncUris == null || syncUris.isEmpty()) { + discoveryUris = null; + healthUriChanged = true; + return; + } + + if (discoveryUris == null) { + discoveryUris = new HashSet<>(syncUris); + healthUriChanged = true; + } else { + discoveryUris.clear(); + discoveryUris.addAll(syncUris); + healthUriChanged = true; + } + } + + + public void addUnHealthUri(String uri) { + if (unHealthUris.add(uri)) { + healthUriChanged = true; + } + } + + + public void removeUnHealthUri(String uri) { + if (unHealthUris.size() > 0) { + if (unHealthUris.remove(uri)) { + healthUriChanged = true; + } + } + } + + +} diff --git a/src/main/java/io/jboot/components/gateway/JbootGatewayHandler.java b/src/main/java/io/jboot/components/gateway/JbootGatewayHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..9439b4e1d258de35853ac570a746f78f0369af05 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/JbootGatewayHandler.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +import com.jfinal.handler.Handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/22 + */ +public class JbootGatewayHandler extends Handler { + + @Override + public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { + JbootGatewayConfig config = JbootGatewayManager.me().matchingConfig(request); + if (config != null) { + isHandled[0] = true; + new GatewayInvocation(config, request, response).invoke(); + } else { + next.handle(target, request, response, isHandled); + } + } + +} diff --git a/src/main/java/io/jboot/components/gateway/JbootGatewayHealthChecker.java b/src/main/java/io/jboot/components/gateway/JbootGatewayHealthChecker.java new file mode 100644 index 0000000000000000000000000000000000000000..6d2926d868a91e5326defe56a88ad6bf8d75b502 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/JbootGatewayHealthChecker.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +import com.jfinal.kit.LogKit; +import io.jboot.components.http.JbootHttpRequest; +import io.jboot.utils.HttpUtil; +import io.jboot.utils.NamedThreadFactory; +import io.jboot.utils.StrUtil; + +import java.util.Set; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/21 + */ +public class JbootGatewayHealthChecker implements Runnable { + + private static JbootGatewayHealthChecker me = new JbootGatewayHealthChecker(); + + public static JbootGatewayHealthChecker me() { + return me; + } + + + private ScheduledThreadPoolExecutor fixedScheduler; + private long fixedSchedulerInitialDelay = 10; + private long fixedSchedulerDelay = 30; + + + /** + * 开始健康检查 + * 多次执行,只会启动一次 + */ + public synchronized void start() { + if (fixedScheduler == null) { + fixedScheduler = new ScheduledThreadPoolExecutor(1, new NamedThreadFactory("jboot-gateway-health-check")); + fixedScheduler.scheduleWithFixedDelay(this, fixedSchedulerInitialDelay, fixedSchedulerDelay, TimeUnit.SECONDS); + } + } + + public void stop() { + fixedScheduler.shutdown(); + fixedScheduler = null; + } + + + @Override + public void run() { + try { + doHealthCheck(); + } catch (Exception ex) { + LogKit.error(ex.toString(), ex); + } + } + + /** + * 健康检查 + */ + private void doHealthCheck() { + for (JbootGatewayConfig config : JbootGatewayManager.me().getConfigMap().values()) { + if (config.isEnable() && config.isUriHealthCheckEnable() && StrUtil.isNotBlank(config.getUriHealthCheckPath())) { + Set uris = config.getUri(); + for (String uri : uris) { + String url = uri + config.getUriHealthCheckPath(); + if (getHttpCode(url) == 200) { + config.removeUnHealthUri(uri); + } else { + config.addUnHealthUri(uri); + } + } + } + } + } + + private int getHttpCode(String url) { + try { + JbootHttpRequest req = JbootHttpRequest.create(url); + req.setReadBody(false); + return HttpUtil.handle(req).getResponseCode(); + } catch (Exception ex) { + // do nothing + return 0; + } + } + + public ScheduledThreadPoolExecutor getFixedScheduler() { + return fixedScheduler; + } + + public long getFixedSchedulerInitialDelay() { + return fixedSchedulerInitialDelay; + } + + public void setFixedSchedulerInitialDelay(long fixedSchedulerInitialDelay) { + this.fixedSchedulerInitialDelay = fixedSchedulerInitialDelay; + } + + public long getFixedSchedulerDelay() { + return fixedSchedulerDelay; + } + + public void setFixedSchedulerDelay(long fixedSchedulerDelay) { + this.fixedSchedulerDelay = fixedSchedulerDelay; + } +} diff --git a/src/main/java/io/jboot/components/gateway/JbootGatewayManager.java b/src/main/java/io/jboot/components/gateway/JbootGatewayManager.java new file mode 100644 index 0000000000000000000000000000000000000000..977cf19b8075337aff737f1eaaeb22e4d7981529 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/JbootGatewayManager.java @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway; + +import io.jboot.utils.ConfigUtil; +import io.jboot.components.gateway.discovery.GatewayDiscovery; +import io.jboot.components.gateway.discovery.GatewayDiscoveryManager; +import io.jboot.components.gateway.discovery.GatewayInstance; +import io.jboot.utils.StrUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/21 + */ +public class JbootGatewayManager { + + private static JbootGatewayManager me = new JbootGatewayManager(); + + public static JbootGatewayManager me() { + return me; + } + + private Map configMap; + private GatewayErrorRender gatewayErrorRender; + private GatewayDiscovery discovery; + + + private JbootGatewayManager() { + + initDiscovery(); + + initConfigs(); + } + + private void initConfigs() { + Map configMap = ConfigUtil.getConfigModels(JbootGatewayConfig.class, "jboot.gateway"); + for (Map.Entry entry : configMap.entrySet()) { + if ("discovery".equals(entry.getKey()) || "instance".equals(entry.getKey())) { + continue; + } + JbootGatewayConfig config = entry.getValue(); + if (StrUtil.isBlank(config.getName())) { + config.setName(entry.getKey()); + } + registerConfig(config); + } + } + + /** + * 初始化服务发现 + */ + private void initDiscovery() { + this.discovery = GatewayDiscoveryManager.me().getGatewayDiscovery(); + } + + public boolean isConfigOk() { + return configMap != null && !configMap.isEmpty(); + } + + + /** + * 动态注册新的路由配置 + * + * @param config 配置信息 + */ + public void registerConfig(JbootGatewayConfig config) { + if (configMap == null) { + configMap = new ConcurrentHashMap<>(); + } + configMap.put(config.getName(), config); + + if (discovery != null) { + List healthyInstances = discovery.selectInstances(config.getName(), true); + syncDiscoveryUris(healthyInstances, config); + + discovery.subscribe(config.getName(), eventInfo -> { + List changedInstances = eventInfo.getInstances(); + syncDiscoveryUris(changedInstances, config); + }); + } + + if (config.isEnable()) { + JbootGatewayHealthChecker.me().start(); + } + } + + private void syncDiscoveryUris(List instances, JbootGatewayConfig config) { + if (instances == null) { + config.syncDiscoveryUris(null); + } else { + Set uris = new HashSet<>(); + instances.forEach(instance -> { + if (instance.isHealthy()) { + uris.add(instance.getUri()); + } + }); + config.syncDiscoveryUris(uris); + } + } + + + /** + * 动态移除路由配置 + * + * @param name 配置名称 + * @return 被移除的配置信息 + */ + public JbootGatewayConfig removeConfig(String name) { + return configMap == null ? null : configMap.remove(name); + } + + + /** + * 获取某个配置信息 + * + * @param name 配置名称 + * @return 配置信息 + */ + public JbootGatewayConfig getConfig(String name) { + return configMap == null ? null : configMap.get(name); + } + + + /** + * 获取所有的配置信息 + * + * @return + */ + public Map getConfigMap() { + return configMap; + } + + /** + * 匹配可用的网关 + * + * @param req 请求 + * @return 返回匹配到的网关配置 + */ + public JbootGatewayConfig matchingConfig(HttpServletRequest req) { + if (configMap != null && !configMap.isEmpty()) { + for (JbootGatewayConfig config : configMap.values()) { + if (config.isEnable() && config.matches(req)) { + return config; + } + } + } + return null; + } + + public GatewayErrorRender getGatewayErrorRender() { + return gatewayErrorRender; + } + + public void setGatewayErrorRender(GatewayErrorRender gatewayErrorRender) { + this.gatewayErrorRender = gatewayErrorRender; + } + + +} diff --git a/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscovery.java b/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscovery.java new file mode 100644 index 0000000000000000000000000000000000000000..1389e827b3a96c4ead6eb49d283fefd833a66af8 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscovery.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway.discovery; + +import java.util.List; + +/** + * GatewayDiscovery. + */ +public interface GatewayDiscovery { + + + void registerInstance(GatewayInstance instance); + + void deregisterInstance(GatewayInstance instance); + + List getAllInstances(String serviceName); + + List selectInstances(String serviceName, boolean healthy); + + void subscribe(String serviceName, GatewayDiscoveryListener listener); + +} + diff --git a/src/main/java/io/jboot/web/fixedinterceptor/FixedInterceptorWapper.java b/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscoveryConfig.java similarity index 41% rename from src/main/java/io/jboot/web/fixedinterceptor/FixedInterceptorWapper.java rename to src/main/java/io/jboot/components/gateway/discovery/GatewayDiscoveryConfig.java index 63d9f7746d58b5c779ada7441f8407ad955c95dd..201fe5468b19f81c149a260d09041d234ad41643 100644 --- a/src/main/java/io/jboot/web/fixedinterceptor/FixedInterceptorWapper.java +++ b/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscoveryConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,42 +13,47 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.web.fixedinterceptor; +package io.jboot.components.gateway.discovery; -/** - * FixedInterceptorWapper 排序用 - * - * @author Rlax - */ -public class FixedInterceptorWapper { +import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.utils.StrUtil; + +@ConfigModel(prefix = "jboot.gateway.discovery") +public class GatewayDiscoveryConfig { - private FixedInterceptor fixedInterceptor; + public static final String TYPE_NACOS = "nacos"; - private int orderNo = 100; + private String type;// nacos, other + private String group = "DEFAULT_GROUP"; + private boolean enable; - public FixedInterceptorWapper(FixedInterceptor fixedInterceptor) { - this.fixedInterceptor = fixedInterceptor; + public String getType() { + return type; } - public FixedInterceptorWapper(FixedInterceptor fixedInterceptor, int orderNo) { - this.fixedInterceptor = fixedInterceptor; - this.orderNo = orderNo; + public void setType(String type) { + this.type = type; } - public FixedInterceptor getFixedInterceptor() { - return fixedInterceptor; + public String getGroup() { + return group; } - public void setFixedInterceptor(FixedInterceptor fixedInterceptor) { - this.fixedInterceptor = fixedInterceptor; + public void setGroup(String group) { + this.group = group; } - public int getOrderNo() { - return orderNo; + public boolean isEnable() { + return enable; } - public void setOrderNo(int orderNo) { - this.orderNo = orderNo; + public void setEnable(boolean enable) { + this.enable = enable; } + public boolean isConfigOk() { + return StrUtil.isNotBlank(type); + } + + } diff --git a/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscoveryListener.java b/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscoveryListener.java new file mode 100644 index 0000000000000000000000000000000000000000..b53b37e7a56cbe806d50c13a516665db5d1bf87d --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscoveryListener.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway.discovery; + +import java.util.ArrayList; +import java.util.List; + +/** + * GatewayDiscoveryListener. + */ +public interface GatewayDiscoveryListener { + + void onEvent(EventInfo eventInfo); + + class EventInfo { + + private String serviceName; + private List instances; + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public List getInstances() { + return instances; + } + + public void setInstances(List instances) { + this.instances = instances; + } + + public void addInstances(GatewayInstance instance) { + if (this.instances == null) { + this.instances = new ArrayList<>(); + } + this.instances.add(instance); + } + } + +} + diff --git a/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscoveryManager.java b/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscoveryManager.java new file mode 100644 index 0000000000000000000000000000000000000000..06cd87c65b796ebb7a6a9bfd72c69622388b6837 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/discovery/GatewayDiscoveryManager.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway.discovery; + + +import io.jboot.Jboot; +import io.jboot.utils.ConfigUtil; +import io.jboot.core.spi.JbootSpiLoader; +import io.jboot.utils.NetUtil; + +import java.util.Map; + +public class GatewayDiscoveryManager { + + private static GatewayDiscoveryManager manager = new GatewayDiscoveryManager(); + + private GatewayDiscoveryManager() { + } + + public static GatewayDiscoveryManager me() { + return manager; + } + + private GatewayDiscoveryConfig discoveryConfig; + private GatewayDiscovery gatewayDiscovery; + private boolean isInited = false; + + + public void init() { + discoveryConfig = Jboot.config(GatewayDiscoveryConfig.class); + + gatewayDiscovery = createDiscovery(discoveryConfig); + + exportLocalInstance(gatewayDiscovery); + + isInited = true; + } + + /** + * 暴露本地的示例到 nacos 等服务注册中心 + * + * @param gatewayDiscovery + */ + private void exportLocalInstance(GatewayDiscovery gatewayDiscovery) { + if (gatewayDiscovery == null) { + return; + } + + Map instanceConfigMap = ConfigUtil.getConfigModels(GatewayInstanceConfig.class, "jboot.gateway.instance"); + for (GatewayInstanceConfig instanceConfig : instanceConfigMap.values()) { + + GatewayInstance instance = new GatewayInstance(); + instance.setHealthy(true); + instance.setServiceName(instanceConfig.getName()); + instance.setUri(instanceConfig.toUri()); + + instance.setHost(NetUtil.getLocalIpAddress()); + instance.setPort(Integer.parseInt(Jboot.configValue("undertow.port", "8080"))); + + gatewayDiscovery.registerInstance(instance); + } + } + + public GatewayDiscovery getGatewayDiscovery() { + if (!isInited){ + init(); + } + return gatewayDiscovery; + } + + public GatewayDiscoveryConfig getDiscoveryConfig() { + return discoveryConfig; + } + + public GatewayDiscovery createDiscovery(GatewayDiscoveryConfig config) { + if (config == null || !config.isConfigOk() || !config.isEnable()) { + return null; + } + + switch (config.getType()) { + case GatewayDiscoveryConfig.TYPE_NACOS: + return new NacosGatewayDiscovery(); + default: + return JbootSpiLoader.load(GatewayDiscovery.class, config.getType()); + } + } + + +} + + + + + + diff --git a/src/main/java/io/jboot/components/gateway/discovery/GatewayInstance.java b/src/main/java/io/jboot/components/gateway/discovery/GatewayInstance.java new file mode 100644 index 0000000000000000000000000000000000000000..1a8090088cf3828b8f33004d1d9ca0a5232edd13 --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/discovery/GatewayInstance.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway.discovery; + +public class GatewayInstance { + + private String serviceName; + private String host; + private int port; + private String uri; + private boolean healthy = true; + + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public boolean isHealthy() { + return healthy; + } + + public void setHealthy(boolean healthy) { + this.healthy = healthy; + } + +} diff --git a/src/main/java/io/jboot/components/gateway/discovery/GatewayInstanceConfig.java b/src/main/java/io/jboot/components/gateway/discovery/GatewayInstanceConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..285027a2a85ac6482dd943099c90647fe07c97bc --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/discovery/GatewayInstanceConfig.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway.discovery; + +import io.jboot.Jboot; +import io.jboot.utils.NetUtil; +import io.jboot.utils.StrUtil; + +public class GatewayInstanceConfig { + + private String name; + private String uriScheme = "http"; + private String uriHost; + private int uriPort; + private String uriPath; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUriScheme() { + return uriScheme; + } + + public void setUriScheme(String uriScheme) { + this.uriScheme = uriScheme; + } + + public String getUriHost() { + return uriHost; + } + + public void setUriHost(String uriHost) { + this.uriHost = uriHost; + } + + public int getUriPort() { + return uriPort; + } + + public void setUriPort(int uriPort) { + this.uriPort = uriPort; + } + + public String getUriPath() { + return uriPath; + } + + public void setUriPath(String uriPath) { + this.uriPath = uriPath; + } + + public String toUri() { + StringBuilder sb = new StringBuilder(uriScheme).append("://"); + if (StrUtil.isNotBlank(uriHost)) { + sb.append(uriHost); + } else { + sb.append(NetUtil.getLocalIpAddress()); + } + if (uriPort == 0) { + uriPort = Integer.parseInt(Jboot.configValue("undertow.port", "8080")); + } + sb.append(":").append(uriPort); + if (StrUtil.isNotBlank(uriPath)) { + sb.append(uriPath); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/jboot/components/gateway/discovery/NacosGatewayDiscovery.java b/src/main/java/io/jboot/components/gateway/discovery/NacosGatewayDiscovery.java new file mode 100644 index 0000000000000000000000000000000000000000..3088dcbc65e822d883ef6b9c46f62770ea0560cb --- /dev/null +++ b/src/main/java/io/jboot/components/gateway/discovery/NacosGatewayDiscovery.java @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.gateway.discovery; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.NamingFactory; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.listener.NamingEvent; +import com.alibaba.nacos.api.naming.pojo.Instance; +import io.jboot.Jboot; +import io.jboot.app.config.support.nacos.NacosServerConfig; +import io.jboot.utils.StrUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class NacosGatewayDiscovery implements GatewayDiscovery { + + private GatewayDiscoveryConfig discoveryConfig; + private NacosServerConfig nacosServerConfig; + private NamingService namingService; + + + public NacosGatewayDiscovery() { + this.discoveryConfig = GatewayDiscoveryManager.me().getDiscoveryConfig(); + this.nacosServerConfig = Jboot.config(NacosServerConfig.class, "jboot.gateway.discovery.nacos"); + try { + this.namingService = NamingFactory.createNamingService(nacosServerConfig.toProperties()); + } catch (NacosException e) { + throw new RuntimeException("Can not create Nacos NamingService for gateway discovry. ", e); + } + } + + @Override + public void registerInstance(GatewayInstance instance) { + try { + namingService.registerInstance(instance.getServiceName(), discoveryConfig.getGroup(), gatewayInstance2NacosInstance(instance)); + } catch (NacosException e) { + e.printStackTrace(); + } + } + + @Override + public void deregisterInstance(GatewayInstance instance) { + try { + namingService.deregisterInstance(instance.getServiceName(), discoveryConfig.getGroup(), gatewayInstance2NacosInstance(instance)); + } catch (NacosException e) { + e.printStackTrace(); + } + } + + + @Override + public List getAllInstances(String serviceName) { + try { + List nacosInstanceList = namingService.getAllInstances(serviceName, discoveryConfig.getGroup()); + if (nacosInstanceList != null && !nacosInstanceList.isEmpty()) { + List retList = new ArrayList<>(); + for (Instance nacosInstance : nacosInstanceList) { + retList.add(nacosInstance2GatewayInstance(nacosInstance)); + } + return retList; + } + } catch (NacosException e) { + e.printStackTrace(); + } + return null; + } + + + @Override + public List selectInstances(String serviceName, boolean healthy) { + try { + List nacosInstanceList = namingService.selectInstances(serviceName, discoveryConfig.getGroup(), healthy); + if (nacosInstanceList != null && !nacosInstanceList.isEmpty()) { + List retList = new ArrayList<>(); + for (Instance nacosInstance : nacosInstanceList) { + retList.add(nacosInstance2GatewayInstance(nacosInstance)); + } + return retList; + } + } catch (NacosException e) { + e.printStackTrace(); + } + return null; + } + + + @Override + public void subscribe(String serviceName, GatewayDiscoveryListener listener) { + try { + namingService.subscribe(serviceName, discoveryConfig.getGroup(), event -> { + GatewayDiscoveryListener.EventInfo eventInfo = new GatewayDiscoveryListener.EventInfo(); + eventInfo.setServiceName(((NamingEvent) event).getServiceName()); + List nacosInstanceList = ((NamingEvent) event).getInstances(); + if (nacosInstanceList != null && !nacosInstanceList.isEmpty()) { + for (Instance nacosInstance : nacosInstanceList) { + GatewayInstance instance = nacosInstance2GatewayInstance(nacosInstance); + instance.setServiceName(eventInfo.getServiceName()); + eventInfo.addInstances(instance); + } + } + listener.onEvent(eventInfo); + }); + } catch (NacosException e) { + e.printStackTrace(); + } + } + + private GatewayInstance nacosInstance2GatewayInstance(Instance nacosInstance) { + if (nacosInstance == null) { + return null; + } + GatewayInstance instance = new GatewayInstance(); + instance.setHost(nacosInstance.getIp()); + instance.setPort(nacosInstance.getPort()); + instance.setServiceName(instance.getServiceName()); + instance.setHealthy(nacosInstance.isHealthy()); + + Map metadata = nacosInstance.getMetadata(); + if (metadata != null && metadata.containsKey("uri")) { + instance.setUri(metadata.get("uri")); + } + + return instance; + } + + private Instance gatewayInstance2NacosInstance(GatewayInstance gatewayInstance) { + if (gatewayInstance == null) { + return null; + } + Instance instance = new Instance(); + instance.setIp(gatewayInstance.getHost()); + instance.setPort(gatewayInstance.getPort()); + instance.setServiceName(instance.getServiceName()); + instance.setHealthy(gatewayInstance.isHealthy()); + + if (StrUtil.isNotBlank(gatewayInstance.getUri())) { + instance.addMetadata("uri", gatewayInstance.getUri()); + } + + return instance; + } +} diff --git a/src/main/java/io/jboot/components/http/HttpMimeTypes.java b/src/main/java/io/jboot/components/http/HttpMimeTypes.java new file mode 100644 index 0000000000000000000000000000000000000000..d2d11deb3152c53164381540f0a1a80d1f465860 --- /dev/null +++ b/src/main/java/io/jboot/components/http/HttpMimeTypes.java @@ -0,0 +1,173 @@ +package io.jboot.components.http; + +import java.util.HashMap; +import java.util.Map; + +public class HttpMimeTypes { + + private static final Map defaultMappings = new HashMap<>(200); + + static { + defaultMappings.put("txt", "text/plain"); + defaultMappings.put("css", "text/css"); + defaultMappings.put("html", "text/html"); + defaultMappings.put("htm", "text/html"); + defaultMappings.put("gif", "image/gif"); + defaultMappings.put("jpg", "image/jpeg"); + defaultMappings.put("jpe", "image/jpeg"); + defaultMappings.put("jpeg", "image/jpeg"); + defaultMappings.put("bmp", "image/bmp"); + defaultMappings.put("js", "application/javascript"); + defaultMappings.put("png", "image/png"); + defaultMappings.put("java", "text/plain"); + defaultMappings.put("body", "text/html"); + defaultMappings.put("rtx", "text/richtext"); + defaultMappings.put("tsv", "text/tab-separated-values"); + defaultMappings.put("etx", "text/x-setext"); + defaultMappings.put("json", "application/json"); + defaultMappings.put("class", "application/java"); + defaultMappings.put("csh", "application/x-csh"); + defaultMappings.put("sh", "application/x-sh"); + defaultMappings.put("tcl", "application/x-tcl"); + defaultMappings.put("tex", "application/x-tex"); + defaultMappings.put("texinfo", "application/x-texinfo"); + defaultMappings.put("texi", "application/x-texinfo"); + defaultMappings.put("t", "application/x-troff"); + defaultMappings.put("tr", "application/x-troff"); + defaultMappings.put("roff", "application/x-troff"); + defaultMappings.put("man", "application/x-troff-man"); + defaultMappings.put("me", "application/x-troff-me"); + defaultMappings.put("ms", "application/x-wais-source"); + defaultMappings.put("src", "application/x-wais-source"); + defaultMappings.put("zip", "application/zip"); + defaultMappings.put("bcpio", "application/x-bcpio"); + defaultMappings.put("cpio", "application/x-cpio"); + defaultMappings.put("gtar", "application/x-gtar"); + defaultMappings.put("shar", "application/x-shar"); + defaultMappings.put("sv4cpio", "application/x-sv4cpio"); + defaultMappings.put("sv4crc", "application/x-sv4crc"); + defaultMappings.put("tar", "application/x-tar"); + defaultMappings.put("ustar", "application/x-ustar"); + defaultMappings.put("dvi", "application/x-dvi"); + defaultMappings.put("hdf", "application/x-hdf"); + defaultMappings.put("latex", "application/x-latex"); + defaultMappings.put("bin", "application/octet-stream"); + defaultMappings.put("oda", "application/oda"); + defaultMappings.put("pdf", "application/pdf"); + defaultMappings.put("ps", "application/postscript"); + defaultMappings.put("eps", "application/postscript"); + defaultMappings.put("ai", "application/postscript"); + defaultMappings.put("rtf", "application/rtf"); + defaultMappings.put("nc", "application/x-netcdf"); + defaultMappings.put("cdf", "application/x-netcdf"); + defaultMappings.put("cer", "application/x-x509-ca-cert"); + defaultMappings.put("exe", "application/octet-stream"); + defaultMappings.put("gz", "application/x-gzip"); + defaultMappings.put("Z", "application/x-compress"); + defaultMappings.put("z", "application/x-compress"); + defaultMappings.put("hqx", "application/mac-binhex40"); + defaultMappings.put("mif", "application/x-mif"); + defaultMappings.put("ico", "image/x-icon"); + defaultMappings.put("ief", "image/ief"); + defaultMappings.put("tiff", "image/tiff"); + defaultMappings.put("tif", "image/tiff"); + defaultMappings.put("ras", "image/x-cmu-raster"); + defaultMappings.put("pnm", "image/x-portable-anymap"); + defaultMappings.put("pbm", "image/x-portable-bitmap"); + defaultMappings.put("pgm", "image/x-portable-graymap"); + defaultMappings.put("ppm", "image/x-portable-pixmap"); + defaultMappings.put("rgb", "image/x-rgb"); + defaultMappings.put("xbm", "image/x-xbitmap"); + defaultMappings.put("xpm", "image/x-xpixmap"); + defaultMappings.put("xwd", "image/x-xwindowdump"); + defaultMappings.put("au", "audio/basic"); + defaultMappings.put("snd", "audio/basic"); + defaultMappings.put("aif", "audio/x-aiff"); + defaultMappings.put("aiff", "audio/x-aiff"); + defaultMappings.put("aifc", "audio/x-aiff"); + defaultMappings.put("wav", "audio/x-wav"); + defaultMappings.put("mp3", "audio/mpeg"); + defaultMappings.put("mpeg", "video/mpeg"); + defaultMappings.put("mpg", "video/mpeg"); + defaultMappings.put("mpe", "video/mpeg"); + defaultMappings.put("qt", "video/quicktime"); + defaultMappings.put("mov", "video/quicktime"); + defaultMappings.put("avi", "video/x-msvideo"); + defaultMappings.put("movie", "video/x-sgi-movie"); + defaultMappings.put("avx", "video/x-rad-screenplay"); + defaultMappings.put("wrl", "x-world/x-vrml"); + defaultMappings.put("mpv2", "video/mpeg2"); + defaultMappings.put("jnlp", "application/x-java-jnlp-file"); + + defaultMappings.put("eot", "application/vnd.ms-fontobject"); + defaultMappings.put("woff", "application/font-woff"); + defaultMappings.put("woff2", "application/font-woff2"); + defaultMappings.put("ttf", "application/x-font-ttf"); + defaultMappings.put("otf", "application/x-font-opentype"); + defaultMappings.put("sfnt", "application/font-sfnt"); + + /* Add XML related MIMEs */ + + defaultMappings.put("xml", "application/xml"); + defaultMappings.put("xhtml", "application/xhtml+xml"); + defaultMappings.put("xsl", "application/xml"); + defaultMappings.put("svg", "image/svg+xml"); + defaultMappings.put("svgz", "image/svg+xml"); + defaultMappings.put("wbmp", "image/vnd.wap.wbmp"); + defaultMappings.put("wml", "text/vnd.wap.wml"); + defaultMappings.put("wmlc", "application/vnd.wap.wmlc"); + defaultMappings.put("wmls", "text/vnd.wap.wmlscript"); + defaultMappings.put("wmlscriptc", "application/vnd.wap.wmlscriptc"); + + /** + * 视频相关 + */ + defaultMappings.put("asf", "video/x-ms-asf"); + defaultMappings.put("asx", "video/x-ms-asf"); + defaultMappings.put("flv", "video/x-flv"); + defaultMappings.put("mp4", "video/mp4"); + defaultMappings.put("mps", "video/x-mpeg"); + defaultMappings.put("mpv", "video/mpg"); + defaultMappings.put("mpa", "video/x-mpg"); + defaultMappings.put("m4e", "video/mpeg4"); + defaultMappings.put("m2v", "video/x-mpeg"); + defaultMappings.put("wmv", "video/x-ms-wmv"); + defaultMappings.put("3gp", "video/3gpp"); + defaultMappings.put("ts", "video/MP2T"); + + + /** + * 音频相关 + */ + defaultMappings.put("mp2", "audio/mp2"); + defaultMappings.put("m3u", "audio/x-mpegurl"); + defaultMappings.put("m3u8", "audio/x-mpegurl"); + defaultMappings.put("mpga", "audio/rn-mpeg"); + defaultMappings.put("ra", "audio/vnd.rn-realaudio"); + defaultMappings.put("ram", "audio/x-pn-realaudio"); + defaultMappings.put("wax", "audio/x-ms-wax"); + defaultMappings.put("wma", "audio/x-ms-wma"); + + /** + * 文档相关 + */ + defaultMappings.put("doc", "application/msword"); + defaultMappings.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + defaultMappings.put("xls", "application/vnd.ms-excel"); + defaultMappings.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + defaultMappings.put("pot", "application/vnd.ms-powerpoint"); + defaultMappings.put("ppt", "application/vnd.ms-powerpoint"); + defaultMappings.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"); + + } + + public static String getMimeType(String file) { + String lower = file.toLowerCase(); + int pos = lower.lastIndexOf('.'); + if (pos == -1) { + return null; //no extension + } + return defaultMappings.get(lower.substring(pos + 1)); + } + +} diff --git a/src/main/java/io/jboot/components/http/HttpProxyInfo.java b/src/main/java/io/jboot/components/http/HttpProxyInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..8d60fe7a31e25e0bd6fd9e4f7f7a7069f19f7e2d --- /dev/null +++ b/src/main/java/io/jboot/components/http/HttpProxyInfo.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.http; + +import io.jboot.utils.StrUtil; + +import java.io.Serializable; +import java.net.Authenticator; +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.Proxy; + +import static java.net.Proxy.Type.HTTP; + +public class HttpProxyInfo implements Serializable { + + private String proxyHost; + private Integer proxyPort; + private String proxyUser; + private String proxyPassword; + + private Proxy proxy; + + public HttpProxyInfo() { + } + + public HttpProxyInfo(String proxyHost, Integer proxyPort) { + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + } + + public HttpProxyInfo(String proxyHost, Integer proxyPort, String proxyUser, String proxyPassword) { + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.proxyUser = proxyUser; + this.proxyPassword = proxyPassword; + setAuthenticator(); + } + + public void setAuthenticator() { + if (StrUtil.isNotBlank(proxyUser) && StrUtil.isNotBlank(proxyPassword)) { + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray()); + } + }); + } + } + + public String getProxyHost() { + return proxyHost; + } + + public void setProxyHost(String proxyHost) { + this.proxyHost = proxyHost; + } + + public Integer getProxyPort() { + return proxyPort; + } + + public void setProxyPort(Integer proxyPort) { + this.proxyPort = proxyPort; + } + + public String getProxyUser() { + return proxyUser; + } + + public void setProxyUser(String proxyUser) { + this.proxyUser = proxyUser; + } + + public String getProxyPassword() { + return proxyPassword; + } + + public void setProxyPassword(String proxyPassword) { + this.proxyPassword = proxyPassword; + } + + + public Proxy getProxy() { + if (proxy == null) { + proxy = new Proxy(HTTP, new InetSocketAddress(getProxyHost(), getProxyPort())); + } + return proxy; + } +} diff --git a/src/main/java/io/jboot/components/http/JbootHttp.java b/src/main/java/io/jboot/components/http/JbootHttp.java index 7b5ed884b5987a2c1b0c334a5b34de63197637f7..858038a2c7c6255223265d858f74c36c2043f3ef 100644 --- a/src/main/java/io/jboot/components/http/JbootHttp.java +++ b/src/main/java/io/jboot/components/http/JbootHttp.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -17,6 +17,6 @@ package io.jboot.components.http; public interface JbootHttp { - public JbootHttpResponse handle(JbootHttpRequest request); + JbootHttpResponse handle(JbootHttpRequest request); } diff --git a/src/main/java/io/jboot/components/http/JbootHttpConfig.java b/src/main/java/io/jboot/components/http/JbootHttpConfig.java index b6224a633df5de465743e6d0cfe79ae697a22efc..d6a3d0fff0d76161a28ecbb9826ddaeac765f785 100644 --- a/src/main/java/io/jboot/components/http/JbootHttpConfig.java +++ b/src/main/java/io/jboot/components/http/JbootHttpConfig.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -16,6 +16,7 @@ package io.jboot.components.http; import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.utils.StrUtil; @ConfigModel(prefix = "jboot.http") @@ -26,6 +27,19 @@ public class JbootHttpConfig { public String type = TYPE_DEFAULT; + private String certPath; + private String certPass; + + private int readTimeOut = JbootHttpRequest.READ_TIME_OUT; + private int connectTimeOut = JbootHttpRequest.CONNECT_TIME_OUT; + private String contentType = JbootHttpRequest.CONTENT_TYPE_URL_ENCODED; + + private String proxyHost; + private Integer proxyPort; + private String proxyUser; + private String proxyPassword; + + public String getType() { return type; } @@ -33,4 +47,90 @@ public class JbootHttpConfig { public void setType(String type) { this.type = type; } + + public String getCertPath() { + return certPath; + } + + public void setCertPath(String certPath) { + this.certPath = certPath; + } + + public String getCertPass() { + return certPass; + } + + public void setCertPass(String certPass) { + this.certPass = certPass; + } + + public int getReadTimeOut() { + return readTimeOut; + } + + public void setReadTimeOut(int readTimeOut) { + this.readTimeOut = readTimeOut; + } + + public int getConnectTimeOut() { + return connectTimeOut; + } + + public void setConnectTimeOut(int connectTimeOut) { + this.connectTimeOut = connectTimeOut; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getProxyHost() { + return proxyHost; + } + + public void setProxyHost(String proxyHost) { + this.proxyHost = proxyHost; + } + + public Integer getProxyPort() { + return proxyPort; + } + + public void setProxyPort(Integer proxyPort) { + this.proxyPort = proxyPort; + } + + public String getProxyUser() { + return proxyUser; + } + + public void setProxyUser(String proxyUser) { + this.proxyUser = proxyUser; + } + + public String getProxyPassword() { + return proxyPassword; + } + + public void setProxyPassword(String proxyPassword) { + this.proxyPassword = proxyPassword; + } + + private HttpProxyInfo httpProxyInfo; + + public HttpProxyInfo getHttpProxyInfo() { + if (httpProxyInfo != null) { + return httpProxyInfo; + } + if (StrUtil.isNotBlank(proxyHost) && proxyPort != null && proxyPort > 0) { + httpProxyInfo = new HttpProxyInfo(proxyHost, proxyPort, proxyUser, proxyPassword); + } + return httpProxyInfo; + } + + } diff --git a/src/main/java/io/jboot/components/http/JbootHttpManager.java b/src/main/java/io/jboot/components/http/JbootHttpManager.java index 2afc86536000805198be1182889f1c5169c00365..5bda73f23fbf56de68bae25b2276f07a247f749b 100644 --- a/src/main/java/io/jboot/components/http/JbootHttpManager.java +++ b/src/main/java/io/jboot/components/http/JbootHttpManager.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -19,22 +19,22 @@ import io.jboot.Jboot; import io.jboot.components.http.jboot.JbootHttpImpl; import io.jboot.components.http.okhttp.OKHttpImpl; import io.jboot.core.spi.JbootSpiLoader; -import io.jboot.utils.ClassUtil; public class JbootHttpManager { - private static JbootHttpManager manager; + private static JbootHttpManager me = new JbootHttpManager(); + private JbootHttp jbootHttp; + private JbootHttpConfig httpConfig; public static JbootHttpManager me() { - if (manager == null) { - manager = ClassUtil.singleton(JbootHttpManager.class); - } - return manager; + return me; } + private JbootHttpManager() { + httpConfig = Jboot.config(JbootHttpConfig.class); + } - private JbootHttp jbootHttp; public JbootHttp getJbootHttp() { if (jbootHttp == null) { @@ -44,10 +44,13 @@ public class JbootHttpManager { } - private JbootHttp buildJbootHttp() { - JbootHttpConfig config = Jboot.config(JbootHttpConfig.class); + public JbootHttpConfig getHttpConfig() { + return httpConfig; + } - switch (config.getType()) { + + private JbootHttp buildJbootHttp() { + switch (httpConfig.getType()) { case JbootHttpConfig.TYPE_DEFAULT: return new JbootHttpImpl(); case JbootHttpConfig.TYPE_OKHTTP: @@ -55,7 +58,7 @@ public class JbootHttpManager { case JbootHttpConfig.TYPE_HTTPCLIENT: throw new RuntimeException("not finished!!!!"); default: - return JbootSpiLoader.load(JbootHttp.class, config.getType()); + return JbootSpiLoader.load(JbootHttp.class, httpConfig.getType()); } } diff --git a/src/main/java/io/jboot/components/http/JbootHttpRequest.java b/src/main/java/io/jboot/components/http/JbootHttpRequest.java index 241cdeb2e8de2243fcb06f45e3dc5449ddf70118..f301446a969a5fddb8e7c51560a43380b1f94256 100644 --- a/src/main/java/io/jboot/components/http/JbootHttpRequest.java +++ b/src/main/java/io/jboot/components/http/JbootHttpRequest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,13 @@ package io.jboot.components.http; import io.jboot.utils.StrUtil; +import javax.net.ssl.SSLContext; import java.io.File; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.HashMap; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.Proxy; +import java.util.LinkedHashMap; import java.util.Map; /** @@ -42,26 +45,52 @@ public class JbootHttpRequest { public static final int CONNECT_TIME_OUT = 1000 * 5; // 5秒 public static final String CHAR_SET = "UTF-8"; + public static final String CONTENT_TYPE_TEXT = "text/plain; charset=utf-8"; public static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8"; - public static final String CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded;charset=utf-8"; - - Map headers; - Map params; + public static final String CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded; charset=utf-8"; private String requestUrl; - private String certPath; - private String certPass; + + private Map headers; + private Map params; + private String method = METHOD_GET; - private int readTimeOut = READ_TIME_OUT; - private int connectTimeOut = CONNECT_TIME_OUT; private String charset = CHAR_SET; private boolean multipartFormData = false; + private String certPath; + private String certPass; + + private int readTimeOut; + private int connectTimeOut; + private String contentType; + + private File downloadFile; - private String contentType = CONTENT_TYPE_URL_ENCODED; - private String postContent; + private String bodyContent; + + // 如果某些时候只是为了去读取 http 头信息,而不需要 http body,可以配置为 false + private boolean readBody = true; + + // 遇到重定向是否自动跟随 + private boolean instanceFollowRedirects = false; + + // 自定义 sslContext + private SSLContext sslContext; + + // 自定义 http 代理 + private HttpProxyInfo httpProxyInfo; + + // 是否自动重定向(手动实现的,而非) + private boolean autoRedirect = true; + + // 最大的重定向次数 + private int maxRedirectCount = 3; + + // 当前的重定向次数 + private int currentRedirectCount = 0; public static JbootHttpRequest create(String url) { @@ -88,16 +117,30 @@ public class JbootHttpRequest { } public JbootHttpRequest() { + JbootHttpConfig config = JbootHttpManager.me().getHttpConfig(); + + if (StrUtil.isNotBlank(config.getCertPath())) { + this.certPath = config.getCertPath(); + } + if (StrUtil.isNotBlank(config.getCertPass())) { + this.certPass = config.getCertPass(); + } + + this.readTimeOut = config.getReadTimeOut(); + this.connectTimeOut = config.getConnectTimeOut(); + this.contentType = config.getContentType(); + this.httpProxyInfo = config.getHttpProxyInfo(); } public JbootHttpRequest(String url) { + this(); this.requestUrl = url; } public void addParam(String key, Object value) { if (params == null) { - params = new HashMap<>(); + params = new LinkedHashMap<>(); } if (value instanceof File) { setMultipartFormData(true); @@ -107,7 +150,7 @@ public class JbootHttpRequest { public void addParams(Map map) { if (params == null) { - params = new HashMap<>(); + params = new LinkedHashMap<>(); } for (Map.Entry entry : map.entrySet()) { if (entry.getValue() == null) { @@ -129,6 +172,35 @@ public class JbootHttpRequest { return certPath; } + public InputStream getCertInputStream() throws FileNotFoundException { + if (StrUtil.isBlank(certPath)) { + return null; + } + + if (certPath.toLowerCase().startsWith("classpath:")) { + + String path = certPath.substring(10).trim(); + InputStream inStream = getClassLoader().getResourceAsStream(path); + + if (inStream == null) { + inStream = getClassLoader().getResourceAsStream("webapp/" + path); + } + + if (inStream == null) { + throw new FileNotFoundException("Can not load resource: " + path + " in classpath."); + } else { + return inStream; + } + } else { + return new FileInputStream(certPath); + } + } + + private ClassLoader getClassLoader() { + ClassLoader ret = Thread.currentThread().getContextClassLoader(); + return ret != null ? ret : getClass().getClassLoader(); + } + public void setCertPath(String certPath) { this.certPath = certPath; } @@ -174,13 +246,17 @@ public class JbootHttpRequest { return headers; } + public String getHeader(String name){ + return headers != null ? headers.get(name) : null; + } + public void setHeaders(Map headers) { this.headers = headers; } public void addHeader(String key, String value) { if (headers == null) { - headers = new HashMap<>(); + headers = new LinkedHashMap<>(); } headers.put(key, value); } @@ -191,7 +267,7 @@ public class JbootHttpRequest { } if (this.headers == null) { - this.headers = new HashMap<>(); + this.headers = new LinkedHashMap<>(); } this.headers.putAll(headers); } @@ -219,6 +295,14 @@ public class JbootHttpRequest { return METHOD_POST.equalsIgnoreCase(method); } + public boolean isPutRequest() { + return METHOD_PUT.equalsIgnoreCase(method); + } + + public boolean isPostOrPutRequest() { + return isPostRequest() || isPutRequest(); + } + public String getCharset() { return charset; } @@ -251,48 +335,30 @@ public class JbootHttpRequest { this.contentType = contentType; } - public String getPostContent() { - if (postContent != null) { - initGetUrl(); - return postContent; + + public String getBodyContent() { + return bodyContent; + } + + public String getUploadBodyString() { + if (bodyContent != null) { + return bodyContent; } else { return buildParams(); } } - public void setPostContent(String postContent) { - this.postContent = postContent; + public void setBodyContent(String bodyContent) { + this.bodyContent = bodyContent; } private String buildParams() { - Map params = getParams(); - if (params == null || params.isEmpty()) { - return null; - } - - StringBuilder builder = new StringBuilder(); - for (Map.Entry entry : params.entrySet()) { - if (entry.getKey() != null && StrUtil.isNotBlank(entry.getValue())) { - try { - builder.append(entry.getKey().trim()).append("=") - .append(URLEncoder.encode(entry.getValue().toString(), getCharset())).append("&"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - } - } - - if (builder.charAt(builder.length() - 1) == '&') { - builder.deleteCharAt(builder.length() - 1); - } - - return builder.toString(); + return StrUtil.mapToQueryString(getParams()); } - public void initGetUrl() { - + public void appendParasToUrl() { String params = buildParams(); if (StrUtil.isBlank(params)) { @@ -308,4 +374,69 @@ public class JbootHttpRequest { setRequestUrl(originUrl); } + + + public boolean isHttps() { + return requestUrl != null && requestUrl.toLowerCase().startsWith("https"); + } + + public boolean isReadBody() { + return readBody; + } + + public void setReadBody(boolean readBody) { + this.readBody = readBody; + } + + public boolean isInstanceFollowRedirects() { + return instanceFollowRedirects; + } + + public void setInstanceFollowRedirects(boolean instanceFollowRedirects) { + this.instanceFollowRedirects = instanceFollowRedirects; + } + + public SSLContext getSslContext() { + return sslContext; + } + + public void setSslContext(SSLContext sslContext) { + this.sslContext = sslContext; + } + + public HttpProxyInfo getHttpProxyInfo() { + return httpProxyInfo; + } + + public void setHttpProxyInfo(HttpProxyInfo httpProxyInfo) { + this.httpProxyInfo = httpProxyInfo; + } + + public Proxy getProxy() { + return httpProxyInfo != null ? httpProxyInfo.getProxy() : null; + } + + public boolean isAutoRedirect() { + return autoRedirect; + } + + public void setAutoRedirect(boolean autoRedirect) { + this.autoRedirect = autoRedirect; + } + + public int getMaxRedirectCount() { + return maxRedirectCount; + } + + public void setMaxRedirectCount(int maxRedirectCount) { + this.maxRedirectCount = maxRedirectCount; + } + + public int getCurrentRedirectCount() { + return currentRedirectCount; + } + + public void setCurrentRedirectCount(int currentRedirectCount) { + this.currentRedirectCount = currentRedirectCount; + } } diff --git a/src/main/java/io/jboot/components/http/JbootHttpResponse.java b/src/main/java/io/jboot/components/http/JbootHttpResponse.java index f76fd5044407f59922b98f4e2cf9a8909ad2514b..30d15438e1c988ae3bf246ca3178e4d4b547b80e 100644 --- a/src/main/java/io/jboot/components/http/JbootHttpResponse.java +++ b/src/main/java/io/jboot/components/http/JbootHttpResponse.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,35 +21,39 @@ import java.io.*; import java.util.List; import java.util.Map; -public class JbootHttpResponse { - private static final Log log = Log.getLog(JbootHttpResponse.class); +public class JbootHttpResponse implements Closeable { + private static final Log LOG = Log.getLog(JbootHttpResponse.class); + + private final JbootHttpRequest request; private String content; - private OutputStream outputStream; + private OutputStream contentStream; private File file; private Throwable error; private Map> headers; private int responseCode; private String contentType; - public JbootHttpResponse() { - this.outputStream = new ByteArrayOutputStream(); - } - - public JbootHttpResponse(File file) { - if (!file.getParentFile().exists()) { - file.getParentFile().mkdirs(); - } + public JbootHttpResponse(JbootHttpRequest request) { + this.request = request; - if (file.exists()) { - file.delete(); - } + File downloadToFile = request.getDownloadFile(); - try { - this.file = file; - this.outputStream = new FileOutputStream(file); - } catch (Exception e) { - throw new RuntimeException(e); + if (downloadToFile != null) { + if (!downloadToFile.getParentFile().exists() && !downloadToFile.getParentFile().mkdirs()) { + LOG.error("Can not mkdirs for: " + downloadToFile.getParentFile()); + } + if (downloadToFile.exists() && !downloadToFile.delete()) { + LOG.error("Can not delete file: " + downloadToFile); + } + try { + this.file = downloadToFile; + this.contentStream = new FileOutputStream(file); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + this.contentStream = new ByteArrayOutputStream(); } } @@ -60,15 +64,29 @@ public class JbootHttpResponse { * @return */ public String getContent() { + return getContent(request.getCharset()); + } + + /** + * 获取数据内容 + * + * @return + */ + public String getContent(String charset) { if (content != null) { return content; } - if (outputStream != null && outputStream instanceof ByteArrayOutputStream) { - return new String(((ByteArrayOutputStream) outputStream).toByteArray()); + if (contentStream != null && contentStream instanceof ByteArrayOutputStream) { + try { + return ((ByteArrayOutputStream) contentStream).toString(charset); + } catch (UnsupportedEncodingException e) { + LOG.error(e.toString(), e); + } } return null; } + public void setContent(String content) { this.content = content; } @@ -79,14 +97,14 @@ public class JbootHttpResponse { * * @param inputStream */ - public void pipe(InputStream inputStream) { + public void copyStream(InputStream inputStream) { try { byte[] buffer = new byte[1024]; for (int len = 0; (len = inputStream.read(buffer)) > 0; ) { - outputStream.write(buffer, 0, len); + contentStream.write(buffer, 0, len); } } catch (Throwable throwable) { - log.error(throwable.toString(), throwable); + LOG.error(throwable.toString(), throwable); setError(throwable); } } @@ -94,17 +112,19 @@ public class JbootHttpResponse { /** * 结束response和释放资源 */ - public void finish() { - if (outputStream != null) { + @Override + public void close() { + if (contentStream != null) { try { - outputStream.flush(); - outputStream.close(); + contentStream.flush(); + contentStream.close(); } catch (IOException e) { - e.printStackTrace(); + LOG.error(e.toString(), e); } } } + public boolean isNotError() { return !isError(); } @@ -153,4 +173,15 @@ public class JbootHttpResponse { this.contentType = contentType; } + + @Override + public String toString() { + return "JbootHttpResponse{" + + "\nfile=" + file + + "\nheaders=" + headers + + "\nresponseCode=" + responseCode + + "\ncontentType=" + contentType + + "\ncontent=" + getContent() + + "\n}"; + } } diff --git a/src/main/java/io/jboot/components/http/jboot/JbootHttpImpl.java b/src/main/java/io/jboot/components/http/jboot/JbootHttpImpl.java index c2c62cd099344f4b00639ef78303a0287ca95341..f49fa0ed030f1b9f8ce1ece3e2fa1dc5ed0b1c49 100644 --- a/src/main/java/io/jboot/components/http/jboot/JbootHttpImpl.java +++ b/src/main/java/io/jboot/components/http/jboot/JbootHttpImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,27 @@ package io.jboot.components.http.jboot; import com.jfinal.log.Log; +import io.jboot.components.http.HttpMimeTypes; import io.jboot.components.http.JbootHttp; import io.jboot.components.http.JbootHttpRequest; import io.jboot.components.http.JbootHttpResponse; import io.jboot.exception.JbootException; import io.jboot.utils.ArrayUtil; +import io.jboot.utils.QuietlyUtil; import io.jboot.utils.StrUtil; import javax.net.ssl.*; import java.io.*; +import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.URL; import java.security.KeyStore; import java.security.SecureRandom; import java.security.cert.X509Certificate; +import java.util.List; import java.util.Map; +import java.util.zip.GZIPInputStream; public class JbootHttpImpl implements JbootHttp { @@ -41,10 +46,7 @@ public class JbootHttpImpl implements JbootHttp { @Override public JbootHttpResponse handle(JbootHttpRequest request) { - - JbootHttpResponse response = request.getDownloadFile() == null - ? new JbootHttpResponse() - : new JbootHttpResponse(request.getDownloadFile()); + JbootHttpResponse response = new JbootHttpResponse(request); doProcess(request, response); return response; } @@ -52,105 +54,184 @@ public class JbootHttpImpl implements JbootHttp { private void doProcess(JbootHttpRequest request, JbootHttpResponse response) { HttpURLConnection connection = null; - InputStream stream = null; + InputStream inStream = null; try { + //获取 http 链接 connection = getConnection(request); - configConnection(connection, request); + //配置 http 链接 + configConnection(connection, request); - if (request.isPostRequest() == false) { - connection.setInstanceFollowRedirects(true); - connection.connect(); - } - /** - * 处理 post请求 - */ - else if (request.isPostRequest()) { + //post 或者 put 请求 + if (request.isPostRequest() || request.isPutRequest()) { - connection.setRequestMethod("POST"); connection.setDoOutput(true); //处理文件上传的post提交 - if (request.isMultipartFormData()){ + if (request.isMultipartFormData()) { if (ArrayUtil.isNotEmpty(request.getParams())) { - uploadData(request, connection); + uploadByMultipart(request, connection); } } + //处理正常的post提交 - else { - String postContent = request.getPostContent(); - if (StrUtil.isNotEmpty(postContent)) { - DataOutputStream dos = new DataOutputStream(connection.getOutputStream()); - dos.write(postContent.getBytes(request.getCharset())); - dos.flush(); - dos.close(); + else { + String uploadBodyString = request.getUploadBodyString(); + if (StrUtil.isNotEmpty(uploadBodyString)) { + byte[] bytes = uploadBodyString.getBytes(request.getCharset()); + + if (StrUtil.isBlank(request.getHeader("Content-Length"))) { + connection.setRequestProperty("Content-Length", String.valueOf(bytes.length)); + } + + try (OutputStream outStream = connection.getOutputStream();) { + outStream.write(bytes); + outStream.flush(); + } } - } } + //get 请求 + else { + connection.connect(); + } + + int responseCode = connection.getResponseCode(); + + //自动重定向 + if (responseCode >= 300 && responseCode < 400 && request.isAutoRedirect()) { + processRedirect(request, response, connection); + return; + } - stream = getInutStream(connection); + + inStream = getInputStream(connection, responseCode); response.setContentType(connection.getContentType()); response.setResponseCode(connection.getResponseCode()); response.setHeaders(connection.getHeaderFields()); - response.pipe(stream); - response.finish(); + //是否要读取 body 数据 + if (request.isReadBody()) { + response.copyStream(inStream); + } } catch (Throwable ex) { - LOG.warn(ex.toString(), ex); response.setError(ex); + LOG.error(ex.toString(), ex); } finally { + if (connection != null) { connection.disconnect(); } - if (stream != null) { - try { - stream.close(); - } catch (IOException e) { - e.printStackTrace(); - } + + QuietlyUtil.closeQuietly(inStream, response); + } + } + + + /** + * 手动重定向 + * + * @param request + * @param response + * @param connection + */ + private void processRedirect(JbootHttpRequest request, JbootHttpResponse response, HttpURLConnection connection) throws IOException { + if (request.getCurrentRedirectCount() > request.getMaxRedirectCount()) { + throw new IOException("Exceeded redirect count."); + } + + + String location = connection.getHeaderField("Location"); + request.setCurrentRedirectCount(request.getCurrentRedirectCount() + 1); + + //绝对路径 + if (location.startsWith("/")) { + int firstSlash = request.getRequestUrl().indexOf("/", 8); // 8 == "https://".length() + location = request.getRequestUrl().substring(0, firstSlash) + location; + } + + //相对路径 + else if (!location.toLowerCase().startsWith("http")) { + int lastSlash = request.getRequestUrl().lastIndexOf("/"); + location = request.getRequestUrl().substring(0, lastSlash + 1) + location; + } + + //携带 cookie + String responseCookieString = connection.getHeaderField("Set-Cookie"); + if (StrUtil.isNotBlank(responseCookieString)) { + List cookies = HttpCookie.parse(responseCookieString); + StringBuilder cookie = new StringBuilder(StrUtil.obtainDefault(request.getHeader("Cookie"), "")); + for (HttpCookie httpCookie : cookies) { + cookie.append(httpCookie.getName()).append("=").append(httpCookie.getValue()).append("; "); } + request.addHeader("Cookie", cookie.toString()); } + + request.setRequestUrl(location); + request.setMethod(JbootHttpRequest.METHOD_GET); + + doProcess(request, response); } - private InputStream getInutStream(HttpURLConnection connection) throws IOException { - return connection.getResponseCode() >= 400 ? connection.getErrorStream() : connection.getInputStream(); + + private InputStream getInputStream(HttpURLConnection connection, int responseCode) throws IOException { + InputStream stream = responseCode >= 400 ? connection.getErrorStream() : connection.getInputStream(); + if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) { + return new GZIPInputStream(stream); + } else { + return stream; + } + } - private void uploadData(JbootHttpRequest request, HttpURLConnection connection) throws IOException { + + private void uploadByMultipart(JbootHttpRequest request, HttpURLConnection connection) throws IOException { String endFlag = "\r\n"; - String boundary = "---------" + StrUtil.uuid(); - connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); + String startFlag = "--"; + String boundary = "------" + StrUtil.uuid(); + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); DataOutputStream dos = new DataOutputStream(connection.getOutputStream()); for (Map.Entry entry : request.getParams().entrySet()) { if (entry.getValue() instanceof File) { File file = (File) entry.getValue(); checkFileNormal(file); - dos.writeBytes(boundary + endFlag); - dos.writeBytes(String.format("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"", entry.getKey(), file.getName()) + endFlag); - dos.writeBytes(endFlag); - FileInputStream fStream = new FileInputStream(file); - byte[] buffer = new byte[2028]; - for (int len = 0; (len = fStream.read(buffer)) > 0; ) { - dos.write(buffer, 0, len); - } + writeString(dos, request, startFlag + boundary + endFlag); + writeString(dos, request, "Content-Disposition: form-data; name=\"" + entry.getKey() + "\"; filename=\"" + file.getName() + "\""); + writeString(dos, request, endFlag + "Content-Type: " + HttpMimeTypes.getMimeType(file.getName())); + writeString(dos, request, endFlag + endFlag); - dos.writeBytes(endFlag); + writeFile(dos, file); + + writeString(dos, request, endFlag); } else { - dos.writeBytes("Content-Disposition: form-data; name=\"" + entry.getKey() + "\""); - dos.writeBytes(endFlag); - dos.writeBytes(endFlag); - dos.writeBytes(String.valueOf(entry.getValue())); - dos.writeBytes(endFlag); + writeString(dos, request, startFlag + boundary + endFlag); + writeString(dos, request, "Content-Disposition: form-data; name=\"" + entry.getKey() + "\""); + writeString(dos, request, endFlag + endFlag); + writeString(dos, request, String.valueOf(entry.getValue())); + writeString(dos, request, endFlag); } } - dos.writeBytes("--" + boundary + "--" + endFlag); + writeString(dos, request, startFlag + boundary + startFlag + endFlag); + dos.flush(); + } + + private void writeString(DataOutputStream dos, JbootHttpRequest request, String s) throws IOException { + dos.write(s.getBytes(request.getCharset())); + } + + private void writeFile(DataOutputStream dos, File file) throws IOException { + try (FileInputStream fStream = new FileInputStream(file)) { + byte[] buffer = new byte[2028]; + for (int len = 0; (len = fStream.read(buffer)) > 0; ) { + dos.write(buffer, 0, len); + } + } } private static void checkFileNormal(File file) { @@ -173,10 +254,11 @@ public class JbootHttpImpl implements JbootHttp { connection.setReadTimeout(request.getReadTimeOut()); connection.setConnectTimeout(request.getConnectTimeOut()); connection.setRequestMethod(request.getMethod()); + connection.setInstanceFollowRedirects(request.isInstanceFollowRedirects()); + //如果 reqeust 的 header 不配置 content-Type, 使用默认的 + connection.setRequestProperty("Content-Type", request.getContentType()); - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36"); if (request.getHeaders() != null && request.getHeaders().size() > 0) { for (Map.Entry entry : request.getHeaders().entrySet()) { connection.setRequestProperty(entry.getKey(), entry.getValue()); @@ -184,35 +266,38 @@ public class JbootHttpImpl implements JbootHttp { } } - private static HttpURLConnection getConnection(JbootHttpRequest request) { - try { - if (request.isPostRequest() == false) { - request.initGetUrl(); - } - if (request.getRequestUrl().toLowerCase().startsWith("https")) { - return getHttpsConnection(request); - } else { - return getHttpConnection(request.getRequestUrl()); - } - } catch (Throwable ex) { - throw new JbootException(ex); + private static HttpURLConnection getConnection(JbootHttpRequest request) throws Exception { + + //get 请求 或者 带有body内容的 post 请求,需要在 url 追加参数 + if (!request.isPostOrPutRequest() || request.getBodyContent() != null) { + request.appendParasToUrl(); } + + return request.isHttps() ? getHttpsConnection(request) : getHttpConnection(request); } - private static HttpURLConnection getHttpConnection(String urlStr) throws Exception { - URL url = new URL(urlStr); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + private static HttpURLConnection getHttpConnection(JbootHttpRequest request) throws Exception { + URL url = new URL(request.getRequestUrl()); + HttpURLConnection conn = (HttpURLConnection) (request.getProxy() != null + ? url.openConnection(request.getProxy()) : url.openConnection()); return conn; } private static HttpsURLConnection getHttpsConnection(JbootHttpRequest request) throws Exception { URL url = new URL(request.getRequestUrl()); - HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + HttpsURLConnection conn = (HttpsURLConnection) (request.getProxy() != null + ? url.openConnection(request.getProxy()) : url.openConnection()); - if (request.getCertPath() != null && request.getCertPass() != null) { + //自定义 sslContext + if (request.getSslContext() != null) { + SSLSocketFactory ssf = request.getSslContext().getSocketFactory(); + conn.setSSLSocketFactory(ssf); + } + //配置证书的路径和密码 + else if (request.getCertPath() != null && request.getCertPass() != null) { KeyStore clientStore = KeyStore.getInstance("PKCS12"); - clientStore.load(new FileInputStream(request.getCertPath()), request.getCertPass().toCharArray()); + clientStore.load(request.getCertInputStream(), request.getCertPass().toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(clientStore, request.getCertPass().toCharArray()); @@ -222,12 +307,14 @@ public class JbootHttpImpl implements JbootHttp { TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(clientStore); - SSLContext sslContext = SSLContext.getInstance("TLSv1"); + SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, trustManagerFactory.getTrustManagers(), new SecureRandom()); conn.setSSLSocketFactory(sslContext.getSocketFactory()); - } else { + } + // 默认的 sslContext + else { conn.setHostnameVerifier(hnv); SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); if (sslContext != null) { @@ -255,12 +342,7 @@ public class JbootHttpImpl implements JbootHttp { } }; - private static HostnameVerifier hnv = new HostnameVerifier() { - @Override - public boolean verify(String hostname, SSLSession session) { - return true; - } - }; + private static HostnameVerifier hnv = (hostname, session) -> true; } diff --git a/src/main/java/io/jboot/components/http/okhttp/OKHttpImpl.java b/src/main/java/io/jboot/components/http/okhttp/OKHttpImpl.java index 3db1242f8f962312543ba060e272899370d3c989..a6e3ce1250ed802cabf145594e34bdd4864e03e9 100644 --- a/src/main/java/io/jboot/components/http/okhttp/OKHttpImpl.java +++ b/src/main/java/io/jboot/components/http/okhttp/OKHttpImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package io.jboot.components.http.okhttp; -import com.jfinal.log.Log; import io.jboot.components.http.JbootHttp; import io.jboot.components.http.JbootHttpRequest; import io.jboot.components.http.JbootHttpResponse; @@ -23,7 +22,6 @@ import okhttp3.*; import javax.net.ssl.*; import java.io.File; -import java.io.FileInputStream; import java.security.KeyStore; import java.security.SecureRandom; import java.security.cert.X509Certificate; @@ -32,10 +30,8 @@ import java.util.Map; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.core.http.okhttp */ public class OKHttpImpl implements JbootHttp { - private static final Log LOG = Log.getLog(OKHttpImpl.class); public OKHttpImpl() { @@ -43,33 +39,32 @@ public class OKHttpImpl implements JbootHttp { @Override public JbootHttpResponse handle(JbootHttpRequest request) { - - JbootHttpResponse response = request.getDownloadFile() == null - ? new JbootHttpResponse() - : new JbootHttpResponse(request.getDownloadFile()); + JbootHttpResponse response = new JbootHttpResponse(request); doProcess(request, response); - return response; } private void doProcess(JbootHttpRequest request, JbootHttpResponse response) { try { - if (request.isPostRequest()) { + // post 请求 或者 put 请求 + if (request.isPostOrPutRequest()) { + if (request.getBodyContent() != null) { + request.appendParasToUrl(); + } doProcessPostRequest(request, response); } - /** - * get 获取 其他 请求 - */ + // 其他非 post 和 put 请求 else { - request.initGetUrl(); + request.appendParasToUrl(); doProcessGetRequest(request, response); } } catch (Throwable ex) { - LOG.error(ex.toString(), ex); response.setError(ex); + } finally { + response.close(); } } @@ -103,7 +98,7 @@ public class OKHttpImpl implements JbootHttp { // requestBody = builder.build(); MediaType mediaType = MediaType.parse(request.getContentType()); - requestBody = RequestBody.create(mediaType, request.getPostContent()); + requestBody = RequestBody.create(mediaType, request.getUploadBodyString()); } @@ -121,8 +116,10 @@ public class OKHttpImpl implements JbootHttp { Response okHttpResponse = call.execute(); response.setResponseCode(okHttpResponse.code()); response.setContentType(okHttpResponse.body().contentType().type()); - response.pipe(okHttpResponse.body().byteStream()); - response.finish(); + + if (request.isReadBody()) { + response.copyStream(okHttpResponse.body().byteStream()); + } } @@ -131,14 +128,26 @@ public class OKHttpImpl implements JbootHttp { return getHttpsClient(request); } - return new OkHttpClient(); + + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (request.getProxy() != null) { + builder.proxy(request.getProxy()); + } + + return builder.build(); } public OkHttpClient getHttpsClient(JbootHttpRequest request) throws Exception { OkHttpClient.Builder builder = new OkHttpClient.Builder(); - if (request.getCertPath() != null && request.getCertPass() != null) { + //自定义 sslContext + if (request.getSslContext() != null) { + SSLSocketFactory ssf = request.getSslContext().getSocketFactory(); + builder.sslSocketFactory(ssf, trustAnyTrustManager); + } + //配置证书的路径和密码 + else if (request.getCertPath() != null && request.getCertPass() != null) { KeyStore clientStore = KeyStore.getInstance("PKCS12"); - clientStore.load(new FileInputStream(request.getCertPath()), request.getCertPass().toCharArray()); + clientStore.load(request.getCertInputStream(), request.getCertPass().toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(clientStore, request.getCertPass().toCharArray()); @@ -151,7 +160,7 @@ public class OKHttpImpl implements JbootHttp { TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); - SSLContext sslContext = SSLContext.getInstance("TLSv1"); + SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, trustManagers, new SecureRandom()); X509TrustManager x509TrustManager = trustAnyTrustManager; @@ -176,20 +185,19 @@ public class OKHttpImpl implements JbootHttp { private static X509TrustManager trustAnyTrustManager = new X509TrustManager() { + @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { } + @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { } + @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }; - private static HostnameVerifier hnv = new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { - return true; - } - }; + private static HostnameVerifier hnv = (hostname, session) -> true; } diff --git a/src/main/java/io/jboot/components/limiter/LimitConfig.java b/src/main/java/io/jboot/components/limiter/LimitConfig.java index 4831e1a201fb72bbc48579b1f928df52d47cf503..5a4b9f01b468ca18ad911e812a40d7b59c27ab8d 100644 --- a/src/main/java/io/jboot/components/limiter/LimitConfig.java +++ b/src/main/java/io/jboot/components/limiter/LimitConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,12 @@ public class LimitConfig { */ private String rule; + + /** + * IP 白名单,不受限流的配置 + */ + private String ipWhitelist; + /** * 默认的降级处理器(被限流后的处理器) */ @@ -73,6 +79,14 @@ public class LimitConfig { this.rule = rule; } + public String getIpWhitelist() { + return ipWhitelist; + } + + public void setIpWhitelist(String ipWhitelist) { + this.ipWhitelist = ipWhitelist; + } + public String getFallbackProcesser() { return fallbackProcesser; } diff --git a/src/main/java/io/jboot/components/limiter/LimitFallbackProcesser.java b/src/main/java/io/jboot/components/limiter/LimitFallbackProcesser.java index 28f02cbfdc273cb2fc88b74eaef3ad99f2bbd235..fc7dc389f3692bfdec3ef456986c8037e207d061 100644 --- a/src/main/java/io/jboot/components/limiter/LimitFallbackProcesser.java +++ b/src/main/java/io/jboot/components/limiter/LimitFallbackProcesser.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/limiter/LimitFallbackProcesserDefault.java b/src/main/java/io/jboot/components/limiter/LimitFallbackProcesserDefault.java index 10c624880aed1ddfda8d201a0956808fd05edaa6..02a7bddbbdc0b10d50028484842afb984d3761f0 100644 --- a/src/main/java/io/jboot/components/limiter/LimitFallbackProcesserDefault.java +++ b/src/main/java/io/jboot/components/limiter/LimitFallbackProcesserDefault.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/limiter/LimitInfo.java b/src/main/java/io/jboot/components/limiter/LimitInfo.java index 934e593309ad663e291f8bb85fe06c1df35c9f44..6de9ce895862843c921ed2469819369770e36db5 100644 --- a/src/main/java/io/jboot/components/limiter/LimitInfo.java +++ b/src/main/java/io/jboot/components/limiter/LimitInfo.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/limiter/LimitScope.java b/src/main/java/io/jboot/components/limiter/LimitScope.java new file mode 100644 index 0000000000000000000000000000000000000000..2ce267f6bf4bef67255ea50b3c164059f5244a61 --- /dev/null +++ b/src/main/java/io/jboot/components/limiter/LimitScope.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.limiter; + +public enum LimitScope { + + /** + * 整个集群限次,多实例共享 + */ + CLUSTER, + + /** + * 每个实例单独限次 + */ + NODE; + +} diff --git a/src/main/java/io/jboot/components/limiter/LimitType.java b/src/main/java/io/jboot/components/limiter/LimitType.java index 45dd10001961b5609b5d1ad61c0c4072d9d7b392..ceee39cc600c563378df0fe9759bcf64748b9a60 100644 --- a/src/main/java/io/jboot/components/limiter/LimitType.java +++ b/src/main/java/io/jboot/components/limiter/LimitType.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,10 @@ */ package io.jboot.components.limiter; +import com.google.common.collect.Sets; + +import java.util.Set; + /** * 限制类型 */ @@ -22,12 +26,30 @@ public class LimitType { /** * 令牌桶,通过 guava 的 RateLimiter 来实现 + * 时间有关,每秒钟允许有多少个请求 */ public static final String TOKEN_BUCKET = "tb"; + /** + * IP 并发量限制,通过 RateLimiter 来实现 + * 每个 ip 在每 1 秒内允许请求的数量 + */ + public static final String IP_TOKEN_BUCKET = "iptb"; + /** * 并发量,通过 Semaphore 来实现 + * 和并发有关,和请求时间无关 */ public static final String CONCURRENCY = "cc"; + /** + * IP 并发量限制,通过 Semaphore 来实现 + * 和并发有关,和请求时间无关 + */ + public static final String IP_CONCURRENCY = "ipcc"; + + + public static Set types = Sets.newHashSet(TOKEN_BUCKET, IP_TOKEN_BUCKET, CONCURRENCY, IP_CONCURRENCY); + + } diff --git a/src/main/java/io/jboot/components/limiter/LimiterManager.java b/src/main/java/io/jboot/components/limiter/LimiterManager.java index 9e0aee5ab77890a1d9cee8073201b491e71f5a0f..2369ee01200d48b59bbb3b165199a68dd53830f6 100644 --- a/src/main/java/io/jboot/components/limiter/LimiterManager.java +++ b/src/main/java/io/jboot/components/limiter/LimiterManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package io.jboot.components.limiter; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.util.concurrent.RateLimiter; import com.jfinal.aop.Invocation; import io.jboot.Jboot; @@ -23,21 +25,30 @@ import io.jboot.utils.ClassUtil; import io.jboot.utils.StrUtil; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Semaphore; -import java.util.regex.Matcher; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; public class LimiterManager { - private Set configPackageOrTargets = new HashSet<>(); - private Map typeAndRateCache = new HashMap<>(); + private HashSet limitConfigBeans = new HashSet<>(); + private HashSet ipWhitelist = new HashSet<>(); + private LimitConfig limitConfig = Jboot.config(LimitConfig.class); + + + private Cache semaphoreCache = Caffeine.newBuilder() + .expireAfterAccess(10, TimeUnit.MINUTES) + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(); + + + private Cache rateLimiterCache = Caffeine.newBuilder() + .expireAfterAccess(10, TimeUnit.MINUTES) + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(); - private Map semaphoreCache = new ConcurrentHashMap<>(); - private Map rateLimiterCache = new ConcurrentHashMap<>(); - private Boolean enable; private LimitFallbackProcesser fallbackProcesser; private static LimiterManager me = new LimiterManager(); @@ -52,6 +63,11 @@ public class LimiterManager { public void init() { doInitFallbackProcesser(); doParseConfig(); + + Set ips = StrUtil.splitToSetByComma(limitConfig.getIpWhitelist()); + if (ips != null) { + ipWhitelist.addAll(ips); + } } private void doInitFallbackProcesser() { @@ -94,13 +110,13 @@ public class LimiterManager { if (!ensureLegal(packageOrTarget, type, rate.trim())) { continue; } + packageOrTarget = packageOrTarget.replace(".", "\\.") .replace("(", "\\(") .replace(")", "\\)") .replace("*", ".*"); - configPackageOrTargets.add(packageOrTarget.trim()); - typeAndRateCache.put(packageOrTarget.trim(), new TypeAndRate(type.trim(), Integer.valueOf(rate.trim()))); + limitConfigBeans.add(new LimitConfigBean(packageOrTarget.trim(),type.trim(), Integer.valueOf(rate.trim()))); } } @@ -111,54 +127,37 @@ public class LimiterManager { * @param packageOrTarget * @return */ - public TypeAndRate matchConfig(String packageOrTarget) { + public LimitConfigBean matchConfig(String packageOrTarget) { - if (!isEnable() || configPackageOrTargets.isEmpty()) { + if (!isEnable() || limitConfigBeans.isEmpty()) { return null; } - for (String configPackageOrTarget : configPackageOrTargets) { - if (match(packageOrTarget, configPackageOrTarget)) { - return typeAndRateCache.get(configPackageOrTarget); + for (LimitConfigBean value : limitConfigBeans) { + if (value.isMatched(packageOrTarget)){ + return value; } } return null; } - public RateLimiter getOrCreateRateLimiter(String resource, int rate) { - RateLimiter limiter = rateLimiterCache.get(resource); - if (limiter == null || limiter.getRate() != rate) { - synchronized (resource.intern()) { - limiter = rateLimiterCache.get(resource); - if (limiter == null) { - limiter = RateLimiter.create(rate); - rateLimiterCache.put(resource, limiter); - } - } - } - return limiter; + + public boolean isInIpWhitelist(String ip){ + return !ipWhitelist.isEmpty() && ipWhitelist.contains(ip); } - public Semaphore getOrCreateSemaphore(String resource, int rate) { - Semaphore semaphore = semaphoreCache.get(resource); - if (semaphore == null) { - synchronized (resource.intern()) { - semaphore = semaphoreCache.get(resource); - if (semaphore == null) { - semaphore = new Semaphore(rate); - semaphoreCache.put(resource, semaphore); - } - } - } - return semaphore; + public RateLimiter getOrCreateRateLimiter(String resKey, int rate) { + return rateLimiterCache.get(resKey, s -> RateLimiter.create(rate)); } - private static boolean match(String string, String regex) { - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(string); - return matcher.matches(); + public Semaphore getOrCreateSemaphore(String resKey, int rate) { + return semaphoreCache.get(resKey, s -> new Semaphore(rate)); + } + + public HashSet getLimitConfigBeans() { + return limitConfigBeans; } /** @@ -174,7 +173,7 @@ public class LimiterManager { return false; } - if (!LimitType.CONCURRENCY.equals(type) && !LimitType.TOKEN_BUCKET.equals(type)) { + if (!LimitType.types.contains(type)) { return false; } @@ -186,23 +185,34 @@ public class LimiterManager { } public boolean isEnable() { - if (enable == null) { - enable = Jboot.config(LimitConfig.class).isEnable(); - } - return enable; + return limitConfig.isEnable(); } public void processFallback(String resource, String fallback, Invocation inv) { fallbackProcesser.process(resource, fallback, inv); } - public static class TypeAndRate { + public static class LimitConfigBean { + + private String packageOrTarget; private String type; private int rate; + private Pattern pattern; + - public TypeAndRate(String type, int rate) { + public LimitConfigBean(String packageOrTarget, String type, int rate) { + this.packageOrTarget = packageOrTarget; this.type = type; this.rate = rate; + this.pattern = Pattern.compile(packageOrTarget); + } + + public String getPackageOrTarget() { + return packageOrTarget; + } + + public void setPackageOrTarget(String packageOrTarget) { + this.packageOrTarget = packageOrTarget; } public String getType() { @@ -220,5 +230,9 @@ public class LimiterManager { public void setRate(int rate) { this.rate = rate; } + + public boolean isMatched(String packageOrTarget){ + return pattern.matcher(packageOrTarget).matches(); + } } } diff --git a/src/main/java/io/jboot/components/limiter/annotation/EnableLimit.java b/src/main/java/io/jboot/components/limiter/annotation/EnableLimit.java index 7ea3d6022ba56dc2b2a46a46e7ac5b641d300cab..e8708fbfa99c4678c30d3b64e50bc063c851b5d6 100644 --- a/src/main/java/io/jboot/components/limiter/annotation/EnableLimit.java +++ b/src/main/java/io/jboot/components/limiter/annotation/EnableLimit.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package io.jboot.components.limiter.annotation; +import io.jboot.components.limiter.LimitScope; import io.jboot.components.limiter.LimitType; import java.lang.annotation.*; @@ -38,6 +39,12 @@ public @interface EnableLimit { */ String type() default LimitType.TOKEN_BUCKET; + + /** + * 作用域,默认为单节点本地限次 + */ + LimitScope scope() default LimitScope.NODE; + /** * 频率 * diff --git a/src/main/java/io/jboot/components/limiter/LimiterInterceptor.java b/src/main/java/io/jboot/components/limiter/interceptor/BaseLimiterInterceptor.java similarity index 45% rename from src/main/java/io/jboot/components/limiter/LimiterInterceptor.java rename to src/main/java/io/jboot/components/limiter/interceptor/BaseLimiterInterceptor.java index c7d499e95ff8c09c98060a56af628004a17fc298..cdb8de5268990e4269f1b91130b6b73c83734fe6 100644 --- a/src/main/java/io/jboot/components/limiter/LimiterInterceptor.java +++ b/src/main/java/io/jboot/components/limiter/interceptor/BaseLimiterInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,69 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.components.limiter; +package io.jboot.components.limiter.interceptor; import com.google.common.util.concurrent.RateLimiter; -import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; -import io.jboot.components.limiter.annotation.EnableLimit; -import io.jboot.utils.AnnotationUtil; +import io.jboot.components.limiter.LimiterManager; +import io.jboot.components.limiter.redis.RedisRateLimitUtil; import io.jboot.utils.ClassUtil; import io.jboot.utils.StrUtil; -import io.jboot.web.fixedinterceptor.FixedInterceptor; import javax.servlet.http.HttpServletRequest; import java.util.concurrent.Semaphore; -public class LimiterInterceptor implements FixedInterceptor, Interceptor { +public abstract class BaseLimiterInterceptor { - @Override - public void intercept(Invocation inv) { - String packageOrTarget = getPackageOrTarget(inv); - LimiterManager.TypeAndRate typeAndRate = LimiterManager.me().matchConfig(packageOrTarget); - if (typeAndRate != null) { - doInterceptByTypeAndRate(typeAndRate, packageOrTarget, inv); - return; - } - - EnableLimit enableLimit = inv.getMethod().getAnnotation(EnableLimit.class); - if (enableLimit != null) { - String resource = StrUtil.obtainDefaultIfBlank(enableLimit.resource(), packageOrTarget); - doInterceptByLimitInfo(enableLimit, resource, inv); - return; - } - - inv.invoke(); - } - - - private void doInterceptByTypeAndRate(LimiterManager.TypeAndRate typeAndRate, String resource, Invocation inv) { - switch (typeAndRate.getType()) { - case LimitType.CONCURRENCY: - doInterceptForConcurrency(typeAndRate.getRate(), resource, null, inv); - break; - case LimitType.TOKEN_BUCKET: - doInterceptForTokenBucket(typeAndRate.getRate(), resource, null, inv); - break; - } - } - - private void doInterceptByLimitInfo(EnableLimit enableLimit, String resource, Invocation inv) { - String type = AnnotationUtil.get(enableLimit.type()); - switch (type) { - case LimitType.CONCURRENCY: - doInterceptForConcurrency(enableLimit.rate(), resource, enableLimit.fallback(), inv); - break; - case LimitType.TOKEN_BUCKET: - doInterceptForTokenBucket(enableLimit.rate(), resource, enableLimit.fallback(), inv); - break; - } - } - - - private void doInterceptForConcurrency(int rate, String resource, String fallback, Invocation inv) { + protected void doInterceptForConcurrency(int rate, String resource, String fallback, Invocation inv) { Semaphore semaphore = LimiterManager.me().getOrCreateSemaphore(resource, rate); boolean acquire = false; try { @@ -95,7 +49,7 @@ public class LimiterInterceptor implements FixedInterceptor, Interceptor { } - private void doInterceptForTokenBucket(int rate, String resource, String fallback, Invocation inv) { + protected void doInterceptForTokenBucket(int rate, String resource, String fallback, Invocation inv) { RateLimiter limiter = LimiterManager.me().getOrCreateRateLimiter(resource, rate); //允许通行 if (limiter.tryAcquire()) { @@ -107,16 +61,27 @@ public class LimiterInterceptor implements FixedInterceptor, Interceptor { } } - private void doExecFallback(String resource, String fallback, Invocation inv) { + protected void doInterceptForTokenBucketWithCluster(int rate, String resource, String fallback, Invocation inv) { + //允许通行 + if (RedisRateLimitUtil.tryAcquire(resource, rate)) { + inv.invoke(); + } + //不允许通行 + else { + doExecFallback(resource, fallback, inv); + } + } + + protected void doExecFallback(String resource, String fallback, Invocation inv) { LimiterManager.me().processFallback(resource, fallback, inv); } - private String getPackageOrTarget(Invocation inv) { + protected String getPackageOrTarget(Invocation inv) { return inv.isActionInvocation() ? buildUrl(inv) : ClassUtil.buildMethodString(inv.getMethod()); } - private String buildUrl(Invocation inv) { + protected String buildUrl(Invocation inv) { HttpServletRequest request = inv.getController().getRequest(); String uri = request.getRequestURI(); String query = request.getQueryString(); diff --git a/src/main/java/io/jboot/components/limiter/interceptor/LimiterGlobalInterceptor.java b/src/main/java/io/jboot/components/limiter/interceptor/LimiterGlobalInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..e1d01a473ca1d33c0c8b8ae20be13f98bcf69833 --- /dev/null +++ b/src/main/java/io/jboot/components/limiter/interceptor/LimiterGlobalInterceptor.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.limiter.interceptor; + + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.limiter.LimitType; +import io.jboot.components.limiter.LimiterManager; +import io.jboot.utils.RequestUtil; + +public class LimiterGlobalInterceptor extends BaseLimiterInterceptor implements Interceptor { + + + @Override + public void intercept(Invocation inv) { + LimiterManager manager = LimiterManager.me(); + if (inv.isActionInvocation() && manager.isInIpWhitelist(RequestUtil.getIpAddress(inv.getController().getRequest()))) { + inv.invoke(); + } else { + String packageOrTarget = getPackageOrTarget(inv); + LimiterManager.LimitConfigBean configBean = manager.matchConfig(packageOrTarget); + if (configBean != null) { + doInterceptByTypeAndRate(configBean, packageOrTarget, inv); + } else { + inv.invoke(); + } + } + } + + + private void doInterceptByTypeAndRate(LimiterManager.LimitConfigBean limitConfigBean, String resource, Invocation inv) { + switch (limitConfigBean.getType()) { + case LimitType.CONCURRENCY: + doInterceptForConcurrency(limitConfigBean.getRate(), resource, null, inv); + break; + case LimitType.IP_CONCURRENCY: + String resKey1 = RequestUtil.getIpAddress(inv.getController().getRequest()) + ":" + resource; + doInterceptForConcurrency(limitConfigBean.getRate(), resKey1, null, inv); + break; + case LimitType.TOKEN_BUCKET: + doInterceptForTokenBucket(limitConfigBean.getRate(), resource, null, inv); + break; + case LimitType.IP_TOKEN_BUCKET: + String resKey2 = RequestUtil.getIpAddress(inv.getController().getRequest()) + ":" + resource; + doInterceptForTokenBucket(limitConfigBean.getRate(), resKey2, null, inv); + break; + } + } + + +} diff --git a/src/main/java/io/jboot/components/limiter/interceptor/LimiterInterceptor.java b/src/main/java/io/jboot/components/limiter/interceptor/LimiterInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..ea9f30883775eb4fbfe983a2ff4a6994440758d6 --- /dev/null +++ b/src/main/java/io/jboot/components/limiter/interceptor/LimiterInterceptor.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.limiter.interceptor; + + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.limiter.LimitScope; +import io.jboot.components.limiter.LimitType; +import io.jboot.components.limiter.LimiterManager; +import io.jboot.components.limiter.annotation.EnableLimit; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.RequestUtil; +import io.jboot.utils.StrUtil; + +public class LimiterInterceptor extends BaseLimiterInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + if (inv.isActionInvocation() && LimiterManager.me().isInIpWhitelist(RequestUtil.getIpAddress(inv.getController().getRequest()))) { + inv.invoke(); + } else { + String packageOrTarget = getPackageOrTarget(inv); + EnableLimit enableLimit = inv.getMethod().getAnnotation(EnableLimit.class); + String resource = StrUtil.obtainDefault(enableLimit.resource(), packageOrTarget); + doInterceptByLimitInfo(enableLimit, resource, inv); + } + } + + + private void doInterceptByLimitInfo(EnableLimit enableLimit, String resource, Invocation inv) { + String type = AnnotationUtil.get(enableLimit.type()); + switch (type) { + case LimitType.CONCURRENCY: + if (LimitScope.CLUSTER == enableLimit.scope()) { + throw new IllegalArgumentException("Concurrency limit for cluster not implement!"); + } + doInterceptForConcurrency(enableLimit.rate(), resource, enableLimit.fallback(), inv); + break; + case LimitType.IP_CONCURRENCY: + if (LimitScope.CLUSTER == enableLimit.scope()) { + throw new IllegalArgumentException("Ip limit for cluster not implement!"); + } + String resKey1 = RequestUtil.getIpAddress(inv.getController().getRequest()) + ":" + resource; + doInterceptForConcurrency(enableLimit.rate(), resKey1, enableLimit.fallback(), inv); + break; + case LimitType.TOKEN_BUCKET: + if (LimitScope.CLUSTER == enableLimit.scope()) { + doInterceptForTokenBucketWithCluster(enableLimit.rate(), resource, enableLimit.fallback(), inv); + } else { + doInterceptForTokenBucket(enableLimit.rate(), resource, enableLimit.fallback(), inv); + } + break; + case LimitType.IP_TOKEN_BUCKET: + String resKey2 = RequestUtil.getIpAddress(inv.getController().getRequest()) + ":" + resource; + if (LimitScope.CLUSTER == enableLimit.scope()) { + doInterceptForTokenBucketWithCluster(enableLimit.rate(), resKey2, enableLimit.fallback(), inv); + } else { + doInterceptForTokenBucket(enableLimit.rate(), resKey2, enableLimit.fallback(), inv); + } + break; + } + } + + +} diff --git a/src/main/java/io/jboot/web/JbootJson.java b/src/main/java/io/jboot/components/limiter/interceptor/LimiterInterceptorBuilder.java similarity index 40% rename from src/main/java/io/jboot/web/JbootJson.java rename to src/main/java/io/jboot/components/limiter/interceptor/LimiterInterceptorBuilder.java index 870625a1c25b9319fd8874c1150c5d40fb2f2eb9..9428b0bcb906105fdc713abccdd0ba1b5fdd0c14 100644 --- a/src/main/java/io/jboot/web/JbootJson.java +++ b/src/main/java/io/jboot/components/limiter/interceptor/LimiterInterceptorBuilder.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,47 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.web; +package io.jboot.components.limiter.interceptor; -import com.alibaba.fastjson.JSON; -import com.jfinal.json.JFinalJson; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.components.limiter.LimiterManager; +import io.jboot.components.limiter.annotation.EnableLimit; +import io.jboot.core.weight.Weight; -import java.util.Iterator; -import java.util.Map; +import java.lang.reflect.Method; +/** + * @author michael yang (fuhai999@gmail.com) + */ +@AutoLoad +@Weight(-1) +public class LimiterInterceptorBuilder implements InterceptorBuilder { -public class JbootJson extends JFinalJson { - + private LimiterManager manager = LimiterManager.me(); @Override - protected String mapToJson(Map map, int depth) { - optimizeMapAttrs(map); - return map == null || map.isEmpty() ? "null" : super.mapToJson(map, depth); - } + public void build(Class targetClass, Method method, Interceptors interceptors) { - - /** - * 优化 map 的属性 - * - * @param map - */ - private void optimizeMapAttrs(Map map) { - if (map == null || map.isEmpty()) { + if (manager.isEnable() && !manager.getLimitConfigBeans().isEmpty()) { + interceptors.add(LimiterGlobalInterceptor.class); return; } - Iterator iter = map.entrySet().iterator(); - while (iter.hasNext()) { - Map.Entry entry = (Map.Entry) iter.next(); - //移除 null 值的属性 - if (entry.getValue() == null) { - iter.remove(); - } + + if (Util.hasAnnotation(method, EnableLimit.class)) { + interceptors.add(LimiterInterceptor.class); } } - @Override - public T parse(String jsonString, Class type) { - return JSON.parseObject(jsonString, type); - } } diff --git a/src/main/java/io/jboot/components/limiter/redis/RedisRateLimitUtil.java b/src/main/java/io/jboot/components/limiter/redis/RedisRateLimitUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..8e07a706e5fedc89621d8bc2ebf06cdb23e0f493 --- /dev/null +++ b/src/main/java/io/jboot/components/limiter/redis/RedisRateLimitUtil.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.limiter.redis; + +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.support.redis.JbootRedis; +import io.jboot.support.redis.JbootRedisManager; + +/** + * 通过lua脚本来进行限次 + */ +public class RedisRateLimitUtil { + + private static final String RATE_LIMIT_SCRIPT = "local c" + + "\nc = redis.call('get',KEYS[1])" + + // 调用量已经超过最大值,直接返回 + "\nif c and tonumber(c) > tonumber(ARGV[1]) then" + + "\nreturn tonumber(c);" + + "\nend" + + // 自增 + "\nc = redis.call('incr',KEYS[1])" + + "\nif tonumber(c) == 1 then" + + // 从第一次调用开始限流,设置对应键值的过期 + "\nredis.call('expire',KEYS[1],ARGV[2])" + + "\nend" + + "\nreturn c;"; + + private static JbootRedis redis; + + /** + * 限制时长默认为1秒 + */ + public static boolean tryAcquire(String resource, int rate) { + return tryAcquire(resource, rate, 1); + } + + /** + * 尝试是否能正常执行 + * + * @param resource 资源名 + * @param rate 限制次数 + * @param periodSeconds 限制时长,单位为秒 + * @return true 可以执行 + * false 限次,禁止 + */ + public static boolean tryAcquire(String resource, int rate, int periodSeconds) { + if (redis == null) { + redis = JbootRedisManager.me().getRedis(); + if (redis == null) { + throw new JbootIllegalConfigException("Redis config not well, can not use LimitScope.CLUSTER in @EnableLimit() "); + } + } + Long count = (Long) redis.eval(RATE_LIMIT_SCRIPT, 1, resource, String.valueOf(rate), String.valueOf(periodSeconds)); + return count <= rate; + } +} diff --git a/src/main/java/io/jboot/components/mq/Jbootmq.java b/src/main/java/io/jboot/components/mq/Jbootmq.java index dda58a5065437c09be37a9a1aa4fd415e500dbe2..e5e4d79c8c801b84e92f3e85a435634479cd9dcc 100644 --- a/src/main/java/io/jboot/components/mq/Jbootmq.java +++ b/src/main/java/io/jboot/components/mq/Jbootmq.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -20,22 +20,26 @@ import java.util.Collection; public interface Jbootmq { - public void enqueue(Object message, String toChannel); + void enqueue(Object message, String toChannel); - public void publish(Object message, String toChannel); + void publish(Object message, String toChannel); - public void addMessageListener(JbootmqMessageListener listener); + void addMessageListener(JbootmqMessageListener listener); - public void addMessageListener(JbootmqMessageListener listener, String forChannel); + void addMessageListener(JbootmqMessageListener listener, String forChannel); - public void removeListener(JbootmqMessageListener listener); + void removeListener(JbootmqMessageListener listener); - public void removeAllListeners(); + void removeAllListeners(); - public Collection getGlobalListeners(); + Collection getGlobalListeners(); - public Collection getListenersByChannel(String channel); + Collection getListenersByChannel(String channel); - public boolean startListening(); + boolean startListening(); + + boolean stopListening(); + + JbootmqConfig getConfig(); } diff --git a/src/main/java/io/jboot/components/mq/JbootmqBase.java b/src/main/java/io/jboot/components/mq/JbootmqBase.java index f1aa310b466ad6158958658666f2e98439e3be3f..e8fe67bb9cce08367a90c366fe07a176843de986 100644 --- a/src/main/java/io/jboot/components/mq/JbootmqBase.java +++ b/src/main/java/io/jboot/components/mq/JbootmqBase.java @@ -1,179 +1,233 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.components.mq; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Sets; -import com.jfinal.log.Log; -import io.jboot.Jboot; -import io.jboot.components.serializer.JbootSerializer; -import io.jboot.components.serializer.JbootSerializerManager; -import io.jboot.exception.JbootException; -import io.jboot.utils.NamedThreadFactory; -import io.jboot.utils.StrUtil; - -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.concurrent.*; - - -public abstract class JbootmqBase implements Jbootmq { - - private static final Log LOG = Log.getLog(JbootmqBase.class); - - private List globalListeners = new CopyOnWriteArrayList<>(); - private Multimap channelListeners = ArrayListMultimap.create(); - protected JbootmqConfig config = Jboot.config(JbootmqConfig.class); - - protected Set channels = Sets.newHashSet(); - protected Set syncRecevieMessageChannels = Sets.newHashSet(); - protected JbootSerializer serializer; - - - private final ExecutorService threadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new SynchronousQueue<>(), new NamedThreadFactory("jbootmq")); - - - public JbootmqBase() { - String channelString = config.getChannel(); - if (StrUtil.isBlank(channelString)) { - return; - } - - this.channels.addAll(StrUtil.splitToSet(channelString, ",")); - - if (StrUtil.isNotBlank(config.getSyncRecevieMessageChannel())) { - this.syncRecevieMessageChannels.addAll(StrUtil.splitToSet(config.getSyncRecevieMessageChannel(), ",")); - } - } - - - @Override - public void addMessageListener(JbootmqMessageListener listener) { - globalListeners.add(listener); - } - - @Override - public void addMessageListener(JbootmqMessageListener listener, String forChannel) { - String[] forChannels = forChannel.split(","); - for (String channel : forChannels) { - if (StrUtil.isBlank(channel)) { - continue; - } - channelListeners.put(channel.trim(), listener); - } - } - - @Override - public void removeListener(JbootmqMessageListener listener) { - globalListeners.remove(listener); - for (String channel : channelListeners.keySet()) { - channelListeners.remove(channel, listener); - } - } - - @Override - public void removeAllListeners() { - globalListeners.clear(); - channelListeners.clear(); - } - - - @Override - public Collection getGlobalListeners() { - return globalListeners; - } - - - @Override - public Collection getListenersByChannel(String channel) { - return channelListeners.get(channel); - } - - public void notifyListeners(String channel, Object message) { - - boolean globalResult = notifyListeners(channel, message, globalListeners); - boolean channelResult = notifyListeners(channel, message, channelListeners.get(channel)); - - if (!globalResult && !channelResult) { - LOG.error("application has recevied mq message, bug has no listener to process it. channel:" + - channel + " message:" + message); - } - } - - - protected boolean notifyListeners(String channel, Object message, Collection listeners) { - if (listeners == null || listeners.size() == 0) { - return false; - } - - if (syncRecevieMessageChannels.contains(channel)) { - for (JbootmqMessageListener listener : listeners) { - try { - listener.onMessage(channel, message); - } catch (Throwable ex) { - LOG.warn("listener[" + listener.getClass().getName() + "] execute mq message is error. channel:" + - channel + " message:" + message); - } - } - } else { - for (JbootmqMessageListener listener : listeners) { - threadPool.execute(() -> { - listener.onMessage(channel, message); - }); - } - } - - return true; - } - - - public JbootSerializer getSerializer() { - if (serializer == null) { - if (StrUtil.isBlank(config.getSerializer())) { - serializer = Jboot.getSerializer(); - } else { - serializer = JbootSerializerManager.me().getSerializer(config.getSerializer()); - } - } - return serializer; - } - - - protected boolean isStartListen = false; - - @Override - public boolean startListening() { - if (isStartListen) { - throw new JbootException("jboot mq is started before."); - } - - if (channels == null || channels.isEmpty()) { - throw new JbootException("mq channels is null or empty, please config channels"); - } - - onStartListening(); - isStartListen = true; - return true; - } - - - protected abstract void onStartListening(); - -} +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.mq; + +import com.jfinal.kit.LogKit; +import com.jfinal.log.Log; +import io.jboot.Jboot; +import io.jboot.components.serializer.JbootSerializer; +import io.jboot.utils.NamedThreadPools; +import io.jboot.utils.StrUtil; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; + + +public abstract class JbootmqBase implements Jbootmq { + + private static final Log LOG = Log.getLog(JbootmqBase.class); + + protected final JbootmqConfig config; + + private List globalListeners = new CopyOnWriteArrayList<>(); + private Map> channelListeners = new ConcurrentHashMap<>(); + + protected Set channels = new HashSet<>(); + protected Set syncReceiveMessageChannels = new HashSet<>(); + protected JbootSerializer serializer; + + private ExecutorService threadPool = NamedThreadPools.newFixedThreadPool("jbootmq"); + + public JbootmqBase(JbootmqConfig config) { + this.config = config; + String channelString = config.getChannel(); + if (StrUtil.isBlank(channelString)) { + return; + } + + this.channels.addAll(StrUtil.splitToSet(channelString, ",")); + + if (StrUtil.isNotBlank(config.getSyncRecevieMessageChannel())) { + this.syncReceiveMessageChannels.addAll(StrUtil.splitToSet(config.getSyncRecevieMessageChannel(), ",")); + } + } + + + @Override + public void addMessageListener(JbootmqMessageListener listener) { + globalListeners.add(listener); + } + + + @Override + public void addMessageListener(JbootmqMessageListener listener, String forChannel) { + String[] forChannels = forChannel.split(","); + for (String channel : forChannels) { + if (StrUtil.isNotBlank(channel)) { + addChannelListener(channel.trim(), listener); + } + } + } + + public final synchronized void addChannelListener(String channel, JbootmqMessageListener listener) { + List listeners = channelListeners.get(channel); + if (listeners == null) { + listeners = new CopyOnWriteArrayList<>(); + channelListeners.put(channel, listeners); + } + listeners.add(listener); + channels.add(channel); + } + + + @Override + public void removeListener(JbootmqMessageListener listener) { + globalListeners.remove(listener); + for (List listeners : channelListeners.values()) { + listeners.remove(listener); + } + } + + @Override + public void removeAllListeners() { + globalListeners.clear(); + channelListeners.forEach((s, list) -> list.clear()); + channelListeners.clear(); + } + + + @Override + public Collection getGlobalListeners() { + return globalListeners; + } + + + @Override + public Collection getListenersByChannel(String channel) { + return channelListeners.get(channel); + } + + public void notifyListeners(String channel, Object message, MessageContext context) { + + boolean globalResult = notifyListeners(channel, message, context, globalListeners); + boolean channelResult = notifyListeners(channel, message, context, channelListeners.get(channel)); + + if (!globalResult && !channelResult) { + LOG.warn("Jboot has received mq message, But it has no listener to process. channel:" + + channel + " message:" + message); + } + } + + + protected boolean notifyListeners(String channel, Object message, MessageContext context, Collection listeners) { + if (listeners == null || listeners.size() == 0) { + return false; + } + + if (syncReceiveMessageChannels.contains(channel)) { + for (JbootmqMessageListener listener : listeners) { + try { + listener.onMessage(channel, message, context); + } catch (Throwable ex) { + LOG.warn("listener[" + listener.getClass().getName() + "] execute mq message is error. channel:" + + channel + " message:" + message); + } + } + } else { + for (JbootmqMessageListener listener : listeners) { + threadPool.execute(() -> { + listener.onMessage(channel, message, context); + }); + } + } + + return true; + } + + + public JbootSerializer getSerializer() { + if (serializer == null) { + serializer = StrUtil.isNotBlank(config.getSerializer()) + ? Jboot.getSerializer(config.getSerializer()) + : Jboot.getSerializer(); + } + return serializer; + } + + + protected boolean isStarted = false; + + @Override + public boolean startListening() { + if (isStarted) { + return true; + } + + if (channels == null || channels.isEmpty()) { + LogKit.warn("Jboot MQ started fail. because it's channels is empty, please config channels. " + + "MQ name: {}, type:{}", config.getName(), config.getType()); + return false; + } + + try { + isStarted = true; + onStartListening(); + } catch (Exception ex) { + LogKit.error("Jboot MQ start fail!", ex); + isStarted = false; + return false; + } + + return true; + } + + + @Override + public boolean stopListening() { + if (!isStarted) { + return true; + } + + try { + isStarted = false; + onStopListening(); + } catch (Exception ex) { + LogKit.error("Jboot MQ stop fail!", ex); + isStarted = true; + return false; + } + + return true; + } + + public boolean isStarted() { + return isStarted; + } + + protected abstract void onStartListening(); + + protected abstract void onStopListening(); + + + @Override + public JbootmqConfig getConfig() { + return config; + } + + public void setSerializer(JbootSerializer serializer) { + this.serializer = serializer; + } + + public ExecutorService getThreadPool() { + return threadPool; + } + + public void setThreadPool(ExecutorService threadPool) { + this.threadPool = threadPool; + } +} diff --git a/src/main/java/io/jboot/components/mq/JbootmqConfig.java b/src/main/java/io/jboot/components/mq/JbootmqConfig.java index 72c9c548864c0cb6257548f4bcc22982ec795e73..22fe84081268860e4b3637130c32faf93be033f7 100644 --- a/src/main/java/io/jboot/components/mq/JbootmqConfig.java +++ b/src/main/java/io/jboot/components/mq/JbootmqConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,11 @@ */ package io.jboot.components.mq; +import com.google.common.collect.Sets; import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.utils.StrUtil; + +import java.util.Set; @ConfigModel(prefix = "jboot.mq") @@ -24,19 +28,27 @@ public class JbootmqConfig { public static final String TYPE_ACTIVEMQ = "activemq"; public static final String TYPE_ALIYUNMQ = "aliyunmq"; public static final String TYPE_RABBITMQ = "rabbitmq"; + public static final String TYPE_ROCKETMQ = "rocketmq"; public static final String TYPE_QPID = "qpid"; + public static final String TYPE_LOCAL = "local"; - private String type = TYPE_REDIS; - private String channel; - private String syncRecevieMessageChannel; //可同步接收消息的channel配置 - private String serializer; + public static final Set TYPES = Sets.newHashSet(TYPE_REDIS, TYPE_ACTIVEMQ, TYPE_ALIYUNMQ, TYPE_RABBITMQ + , TYPE_ROCKETMQ, TYPE_QPID, TYPE_LOCAL); - public String getChannel() { - return channel; + + private String name = "default"; // MQ 的名称,可以配置多个 MQ 实例,但是需要名称不能一样 + private String type; // MQ 的类型: redis、rocketmq 等 + private String typeName; // MQ 相同的类型,可能有多一个实例,比如两个 redis,此时需要配置实例的名称 + private String channel; // 发送的通道,或者是 topic,多个用英文逗号二开 + private String syncRecevieMessageChannel; //可同步接收消息的 channel 配置 + private String serializer; // MQ 默认的序列化方案 + + public String getName() { + return name; } - public void setChannel(String channel) { - this.channel = channel; + public void setName(String name) { + this.name = name; } public String getType() { @@ -47,13 +59,20 @@ public class JbootmqConfig { this.type = type; } + public String getTypeName() { + return typeName; + } - public String getSerializer() { - return serializer; + public void setTypeName(String typeName) { + this.typeName = typeName; } - public void setSerializer(String serializer) { - this.serializer = serializer; + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; } public String getSyncRecevieMessageChannel() { @@ -63,4 +82,16 @@ public class JbootmqConfig { public void setSyncRecevieMessageChannel(String syncRecevieMessageChannel) { this.syncRecevieMessageChannel = syncRecevieMessageChannel; } + + public String getSerializer() { + return serializer; + } + + public void setSerializer(String serializer) { + this.serializer = serializer; + } + + public boolean isConfigOk() { + return StrUtil.isNotEmpty(type); + } } diff --git a/src/main/java/io/jboot/components/mq/JbootmqManager.java b/src/main/java/io/jboot/components/mq/JbootmqManager.java index c5f13097a7149a7d1723470a9dbc1fa102690113..268986d634b3b967d9796df934abb24c43d7695e 100644 --- a/src/main/java/io/jboot/components/mq/JbootmqManager.java +++ b/src/main/java/io/jboot/components/mq/JbootmqManager.java @@ -1,74 +1,129 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.components.mq; - -import io.jboot.Jboot; -import io.jboot.components.mq.aliyunmq.JbootAliyunmqImpl; -import io.jboot.components.mq.qpidmq.JbootQpidmqImpl; -import io.jboot.components.mq.rabbitmq.JbootRabbitmqImpl; -import io.jboot.components.mq.redismq.JbootRedismqImpl; -import io.jboot.core.spi.JbootSpiLoader; -import io.jboot.utils.ClassUtil; - - -public class JbootmqManager { - - private static JbootmqManager manager; - - public static JbootmqManager me() { - if (manager == null) { - manager = ClassUtil.singleton(JbootmqManager.class); - } - return manager; - } - - - private Jbootmq jbootmq; - - public Jbootmq getJbootmq() { - if (jbootmq == null) { - JbootmqConfig config = Jboot.config(JbootmqConfig.class); - jbootmq = getJbootmq(config); - } - return jbootmq; - } - - public Jbootmq getJbootmq(JbootmqConfig config) { - return buildJbootmq(config); - } - - private Jbootmq buildJbootmq(JbootmqConfig config) { - if (config == null) { - throw new IllegalArgumentException("config must not be null"); - } - - switch (config.getType()) { - case JbootmqConfig.TYPE_REDIS: - return new JbootRedismqImpl(); - case JbootmqConfig.TYPE_ALIYUNMQ: - return new JbootAliyunmqImpl(); - case JbootmqConfig.TYPE_RABBITMQ: - return new JbootRabbitmqImpl(); - case JbootmqConfig.TYPE_QPID: - return new JbootQpidmqImpl(); - case JbootmqConfig.TYPE_ACTIVEMQ: - throw new RuntimeException("not finished!!!!"); - default: - return JbootSpiLoader.load(Jbootmq.class, config.getType()); - } - - } -} +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.mq; + +import io.jboot.Jboot; +import io.jboot.components.mq.aliyunmq.JbootAliyunmqImpl; +import io.jboot.components.mq.local.JbootLocalmqImpl; +import io.jboot.components.mq.qpidmq.JbootQpidmqImpl; +import io.jboot.components.mq.rabbitmq.JbootRabbitmqImpl; +import io.jboot.components.mq.redismq.JbootRedismqImpl; +import io.jboot.components.mq.rocketmq.JbootRocketmqImpl; +import io.jboot.core.spi.JbootSpiLoader; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.ConfigUtil; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +public class JbootmqManager { + + private static JbootmqManager manager = new JbootmqManager(); + + private JbootmqManager(){} + + public static JbootmqManager me() { + return manager; + } + + private Map jbootmqMap = new ConcurrentHashMap<>(); + + public Jbootmq getJbootmq() { + return getJbootmq("default"); + } + + + public Jbootmq getJbootmq(String name) { + Jbootmq mq = jbootmqMap.get(name); + if (mq == null) { + synchronized (this) { + mq = jbootmqMap.get(name); + if (mq == null) { + Map configModels = ConfigUtil.getConfigModels(JbootmqConfig.class); + JbootmqConfig.TYPES.forEach(configModels::remove); + + configModels.putIfAbsent("default", Jboot.config(JbootmqConfig.class)); + + JbootmqConfig mqConfig = null; + if (!configModels.containsKey(name)) { + for (JbootmqConfig config : configModels.values()) { + if (name.equals(config.getTypeName())) { + mqConfig = config; + break; + } + } + if (mqConfig == null) { + throw new JbootIllegalConfigException("Please config \"jboot.mq.other" + name + ".type\" in your jboot.properties."); + } + } + else { + mqConfig = configModels.get(name); + } + + mq = getJbootmq(mqConfig); + if (mq != null) { + jbootmqMap.put(name, mq); + } + } + } + } + return mq; + } + + public Jbootmq getJbootmq(JbootmqConfig config) { + return buildJbootmq(config); + } + + private Jbootmq buildJbootmq(JbootmqConfig config) { + if (config == null) { + throw new IllegalArgumentException("config must not be null"); + } + + if (!config.isConfigOk()) { + return null; + } + + switch (config.getType()) { + case JbootmqConfig.TYPE_REDIS: + return new JbootRedismqImpl(config); + case JbootmqConfig.TYPE_ALIYUNMQ: + return new JbootAliyunmqImpl(config); + case JbootmqConfig.TYPE_RABBITMQ: + return new JbootRabbitmqImpl(config); + case JbootmqConfig.TYPE_ROCKETMQ: + return new JbootRocketmqImpl(config); + case JbootmqConfig.TYPE_QPID: + return new JbootQpidmqImpl(config); + case JbootmqConfig.TYPE_ACTIVEMQ: + throw new RuntimeException("not finished!!!!"); + case JbootmqConfig.TYPE_LOCAL: + return new JbootLocalmqImpl(config); + default: + return JbootSpiLoader.load(Jbootmq.class, config.getType(), config); + } + + } + + + public void init() { + jbootmqMap.values().forEach(Jbootmq::startListening); + } + + public void stop() { + jbootmqMap.values().forEach(Jbootmq::stopListening); + } +} diff --git a/src/main/java/io/jboot/components/mq/JbootmqMessageListener.java b/src/main/java/io/jboot/components/mq/JbootmqMessageListener.java index fa0d3641723856c76a76a161afab3e27d1809545..0775047a251ebb1a1fc68bfc7ba56547f125c193 100644 --- a/src/main/java/io/jboot/components/mq/JbootmqMessageListener.java +++ b/src/main/java/io/jboot/components/mq/JbootmqMessageListener.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -18,9 +18,22 @@ package io.jboot.components.mq; public interface JbootmqMessageListener { + + /** + * @param channel + * @param message + * @param context + */ + default void onMessage(String channel, Object message, MessageContext context) { + this.onMessage(channel, message); + } + + /** * @param channel of topic * @param message topic message */ - void onMessage(String channel, Object message); + @Deprecated + default void onMessage(String channel, Object message) { + } } diff --git a/src/main/java/io/jboot/components/mq/MessageContext.java b/src/main/java/io/jboot/components/mq/MessageContext.java new file mode 100644 index 0000000000000000000000000000000000000000..64a7f4f397cec8e1770a0543f1ddaa9006b92c2b --- /dev/null +++ b/src/main/java/io/jboot/components/mq/MessageContext.java @@ -0,0 +1,14 @@ +package io.jboot.components.mq; + +public class MessageContext { + + final Jbootmq mq; + + public MessageContext(Jbootmq mq) { + this.mq = mq; + } + + public Jbootmq getMq() { + return mq; + } +} diff --git a/src/main/java/io/jboot/components/mq/activemq/JbootActivemq.java b/src/main/java/io/jboot/components/mq/activemq/JbootActivemq.java index 03b2a0d9d04ea5543a2ab918ae97e6784a3037fc..a512e13814ad7ef607901fc66dc8b3fc3fd8b1ff 100644 --- a/src/main/java/io/jboot/components/mq/activemq/JbootActivemq.java +++ b/src/main/java/io/jboot/components/mq/activemq/JbootActivemq.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,15 @@ package io.jboot.components.mq.activemq; import io.jboot.components.mq.Jbootmq; import io.jboot.components.mq.JbootmqBase; +import io.jboot.components.mq.JbootmqConfig; public class JbootActivemq extends JbootmqBase implements Jbootmq { + public JbootActivemq(JbootmqConfig config) { + super(config); + } + @Override public void enqueue(Object message, String toChannel) { @@ -35,4 +40,9 @@ public class JbootActivemq extends JbootmqBase implements Jbootmq { protected void onStartListening() { } + + @Override + protected void onStopListening() { + + } } diff --git a/src/main/java/io/jboot/components/mq/aliyunmq/AliyunmqMessageContext.java b/src/main/java/io/jboot/components/mq/aliyunmq/AliyunmqMessageContext.java new file mode 100644 index 0000000000000000000000000000000000000000..efe7e57f7b4cd71ab68234e5fefe047b5eaa7564 --- /dev/null +++ b/src/main/java/io/jboot/components/mq/aliyunmq/AliyunmqMessageContext.java @@ -0,0 +1,36 @@ +package io.jboot.components.mq.aliyunmq; + +import com.aliyun.openservices.ons.api.Action; +import com.aliyun.openservices.ons.api.ConsumeContext; +import com.aliyun.openservices.ons.api.Message; +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.Jbootmq; + +public class AliyunmqMessageContext extends MessageContext { + + final Message orginalMessage; + final ConsumeContext context; + private Action returnAction = Action.CommitMessage; + + public AliyunmqMessageContext(Jbootmq mq, Message orginalMessage, ConsumeContext context) { + super(mq); + this.orginalMessage = orginalMessage; + this.context = context; + } + + public Message getOrginalMessage() { + return orginalMessage; + } + + public ConsumeContext getContext() { + return context; + } + + public Action getReturnAction() { + return returnAction; + } + + public void setReturnAction(Action returnAction) { + this.returnAction = returnAction; + } +} diff --git a/src/main/java/io/jboot/components/mq/aliyunmq/JbootAliyunmqConfig.java b/src/main/java/io/jboot/components/mq/aliyunmq/JbootAliyunmqConfig.java index 53778d3241ee38f44c01402e85b05e845d74e0e1..75e6245586a068c834a860d326e0d1a46412b8c1 100644 --- a/src/main/java/io/jboot/components/mq/aliyunmq/JbootAliyunmqConfig.java +++ b/src/main/java/io/jboot/components/mq/aliyunmq/JbootAliyunmqConfig.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -27,6 +27,13 @@ public class JbootAliyunmqConfig { private String addr; private String sendMsgTimeoutMillis = "3000"; + private String broadcastChannelPrefix = "broadcast-"; + + private String subscribeSubExpression = "*"; + + private String instanceName; + + public String getAccessKey() { return accessKey; } @@ -67,4 +74,27 @@ public class JbootAliyunmqConfig { this.sendMsgTimeoutMillis = sendMsgTimeoutMillis; } + public String getBroadcastChannelPrefix() { + return broadcastChannelPrefix; + } + + public void setBroadcastChannelPrefix(String broadcastChannelPrefix) { + this.broadcastChannelPrefix = broadcastChannelPrefix; + } + + public String getSubscribeSubExpression() { + return subscribeSubExpression; + } + + public void setSubscribeSubExpression(String subscribeSubExpression) { + this.subscribeSubExpression = subscribeSubExpression; + } + + public String getInstanceName() { + return instanceName; + } + + public void setInstanceName(String instanceName) { + this.instanceName = instanceName; + } } diff --git a/src/main/java/io/jboot/components/mq/aliyunmq/JbootAliyunmqImpl.java b/src/main/java/io/jboot/components/mq/aliyunmq/JbootAliyunmqImpl.java index 7b99e1f4939f3f48292f4fd22a4cb07917389843..f90816773126cca2e18d8a71eeeb81db48224a9d 100644 --- a/src/main/java/io/jboot/components/mq/aliyunmq/JbootAliyunmqImpl.java +++ b/src/main/java/io/jboot/components/mq/aliyunmq/JbootAliyunmqImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,70 +16,149 @@ package io.jboot.components.mq.aliyunmq; import com.aliyun.openservices.ons.api.*; +import com.jfinal.log.Log; import io.jboot.Jboot; import io.jboot.components.mq.Jbootmq; import io.jboot.components.mq.JbootmqBase; +import io.jboot.components.mq.JbootmqConfig; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.ConfigUtil; +import io.jboot.utils.StrUtil; +import java.util.Map; import java.util.Properties; -public class JbootAliyunmqImpl extends JbootmqBase implements Jbootmq, MessageListener { +public class JbootAliyunmqImpl extends JbootmqBase implements Jbootmq { + private static final Log LOG = Log.getLog(JbootAliyunmqImpl.class); private Producer producer; private Consumer consumer; - - public JbootAliyunmqImpl() { - super(); - - Properties properties = createProperties(); - producer = ONSFactory.createProducer(properties); - producer.start(); - + private JbootAliyunmqConfig aliyunmqConfig; + + public JbootAliyunmqImpl(JbootmqConfig config) { + super(config); + String typeName = config.getTypeName(); + if (StrUtil.isNotBlank(typeName)) { + Map configModels = ConfigUtil.getConfigModels(JbootAliyunmqConfig.class); + if (!configModels.containsKey(typeName)) { + throw new JbootIllegalConfigException("Please config \"jboot.mq.aliyun." + typeName + ".addr\" in your jboot.properties."); + } + aliyunmqConfig = configModels.get(typeName); + } else { + aliyunmqConfig = Jboot.config(JbootAliyunmqConfig.class); + } } @Override protected void onStartListening() { + startQueueConsumer(); + startBroadCastConsumer(); + } + + @Override + protected void onStopListening() { + if (consumer != null) { + consumer.shutdown(); + consumer = null; + } + } + + public void startQueueConsumer() { Properties properties = createProperties(); + consumer = ONSFactory.createConsumer(properties); + for (String channel : channels) { + consumer.subscribe(aliyunmqConfig.getBroadcastChannelPrefix() + channel, aliyunmqConfig.getSubscribeSubExpression(), (message, consumeContext) -> { + AliyunmqMessageContext context = new AliyunmqMessageContext(this, message, consumeContext); + notifyListeners(channel, getSerializer().deserialize(message.getBody()) + , context); + return context.getReturnAction(); + }); + } + consumer.start(); + } + + public void startBroadCastConsumer() { + Properties properties = createProperties(); + properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.BROADCASTING); consumer = ONSFactory.createConsumer(properties); - for (String c : channels) { - consumer.subscribe(c, "*", this); + for (String channel : channels) { + consumer.subscribe(channel, aliyunmqConfig.getSubscribeSubExpression(), (message, consumeContext) -> { + AliyunmqMessageContext aliyunmqMessageInfo = new AliyunmqMessageContext(this, message, consumeContext); + notifyListeners(channel, getSerializer().deserialize(message.getBody()) + , aliyunmqMessageInfo); + return aliyunmqMessageInfo.getReturnAction(); + }); } consumer.start(); } + @Override public void enqueue(Object message, String toChannel) { - throw new RuntimeException("not finished!"); + Message sendMsg = null; + if (message instanceof Message) { + sendMsg = (Message) message; + } else { + byte[] bytes = getSerializer().serialize(message); + sendMsg = new Message(toChannel, "*", bytes); + } + SendResult result = getProducer().send(sendMsg); + if (result == null) { + LOG.warn("Rockect mq send message fail!!!"); + } } + @Override public void publish(Object message, String toChannel) { - byte[] bytes = getSerializer().serialize(message); - Message onsMessage = new Message(toChannel, "*", bytes); - producer.send(onsMessage); + Message sendMsg = null; + if (message instanceof Message) { + sendMsg = (Message) message; + } else { + byte[] bytes = getSerializer().serialize(message); + sendMsg = new Message(aliyunmqConfig.getBroadcastChannelPrefix() + toChannel, "*", bytes); + } + SendResult result = getProducer().send(sendMsg); + if (result == null) { + LOG.warn("Rockect mq send message fail!!!"); + } } - @Override - public Action consume(Message message, ConsumeContext context) { - byte[] bytes = message.getBody(); - Object object = getSerializer().deserialize(bytes); - notifyListeners(message.getTopic(), object); - return Action.CommitMessage; + + public Producer getProducer() { + if (producer == null) { + synchronized (this) { + if (producer == null) { + createProducer(); + } + } + } + return producer; } - private Properties createProperties() { - JbootAliyunmqConfig aliyunmqConfig = Jboot.config(JbootAliyunmqConfig.class); + public void createProducer() { + Properties properties = createProperties(); + producer = ONSFactory.createProducer(properties); + producer.start(); + } + + + public Properties createProperties() { Properties properties = new Properties(); properties.put(PropertyKeyConst.AccessKey, aliyunmqConfig.getAccessKey());//AccessKey 阿里云身份验证,在阿里云服务器管理控制台创建 properties.put(PropertyKeyConst.SecretKey, aliyunmqConfig.getSecretKey());//SecretKey 阿里云身份验证,在阿里云服务器管理控制台创建 properties.put(PropertyKeyConst.ProducerId, aliyunmqConfig.getProducerId());//您在控制台创建的Producer ID - properties.put(PropertyKeyConst.ONSAddr, aliyunmqConfig.getAddr()); + properties.put(PropertyKeyConst.NAMESRV_ADDR, aliyunmqConfig.getAddr()); + properties.put(PropertyKeyConst.InstanceName, aliyunmqConfig.getInstanceName()); properties.setProperty(PropertyKeyConst.SendMsgTimeoutMillis, aliyunmqConfig.getSendMsgTimeoutMillis());//设置发送超时时间,单位毫秒 + + return properties; } } diff --git a/src/main/java/io/jboot/components/mq/rocketmq/JbootRocketmq.java b/src/main/java/io/jboot/components/mq/local/JbootLocalmqImpl.java similarity index 58% rename from src/main/java/io/jboot/components/mq/rocketmq/JbootRocketmq.java rename to src/main/java/io/jboot/components/mq/local/JbootLocalmqImpl.java index 8efe6e6c43018fbe64f930245770d08c1a764a1f..369f6596f77b5620594c6c7d83016783478d65ba 100644 --- a/src/main/java/io/jboot/components/mq/rocketmq/JbootRocketmq.java +++ b/src/main/java/io/jboot/components/mq/local/JbootLocalmqImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,26 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.components.mq.rocketmq; +package io.jboot.components.mq.local; -import io.jboot.components.mq.Jbootmq; import io.jboot.components.mq.JbootmqBase; +import io.jboot.components.mq.JbootmqConfig; +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/7 + */ +public class JbootLocalmqImpl extends JbootmqBase { -public class JbootRocketmq extends JbootmqBase implements Jbootmq { + public JbootLocalmqImpl(JbootmqConfig config) { + super(config); + } @Override - public void enqueue(Object message, String toChannel) { - + protected void onStartListening() { + //do nothing } @Override - public void publish(Object message, String toChannel) { - + protected void onStopListening() { + //do nothing } @Override - protected void onStartListening() { + public void enqueue(Object message, String toChannel) { + notifyListeners(toChannel, message, new LocalmqMessageContext(this)); + } + @Override + public void publish(Object message, String toChannel) { + notifyListeners(toChannel, message, new LocalmqMessageContext(this)); } } diff --git a/src/main/java/io/jboot/components/mq/local/LocalmqMessageContext.java b/src/main/java/io/jboot/components/mq/local/LocalmqMessageContext.java new file mode 100644 index 0000000000000000000000000000000000000000..f53412e9474bb84098a9821222249762f9aae5be --- /dev/null +++ b/src/main/java/io/jboot/components/mq/local/LocalmqMessageContext.java @@ -0,0 +1,13 @@ +package io.jboot.components.mq.local; + +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.Jbootmq; + +public class LocalmqMessageContext extends MessageContext { + + + public LocalmqMessageContext(Jbootmq mq) { + super(mq); + } + +} diff --git a/src/main/java/io/jboot/components/mq/qpidmq/JbootQpidmqConfig.java b/src/main/java/io/jboot/components/mq/qpidmq/JbootQpidmqConfig.java index 1854298d7d73d090fef273a33e0091ef4b4d485c..921a88171b6ff11f00485200ac8a69b49bc15d52 100644 --- a/src/main/java/io/jboot/components/mq/qpidmq/JbootQpidmqConfig.java +++ b/src/main/java/io/jboot/components/mq/qpidmq/JbootQpidmqConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,18 @@ import io.jboot.app.config.annotation.ConfigModel; @ConfigModel(prefix = "jboot.mq.qpid") public class JbootQpidmqConfig { - private String username = "admin"; - private String password = "admin"; + /** + * 默认账号 admin + */ + private String username; + + /** + * 默认密码 admin + */ + private String password; private String host = "127.0.0.1:5672"; + private String virtualHost; private boolean serializerEnable = true; diff --git a/src/main/java/io/jboot/components/mq/qpidmq/JbootQpidmqImpl.java b/src/main/java/io/jboot/components/mq/qpidmq/JbootQpidmqImpl.java index 88aa482de0269ae120131da0b72f6b183472eae2..746faca01bc912e62276d632306ba7ca3a8d47cf 100644 --- a/src/main/java/io/jboot/components/mq/qpidmq/JbootQpidmqImpl.java +++ b/src/main/java/io/jboot/components/mq/qpidmq/JbootQpidmqImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,12 @@ package io.jboot.components.mq.qpidmq; import com.jfinal.log.Log; import io.jboot.Jboot; +import io.jboot.utils.ConfigUtil; import io.jboot.components.mq.Jbootmq; import io.jboot.components.mq.JbootmqBase; +import io.jboot.components.mq.JbootmqConfig; import io.jboot.exception.JbootException; +import io.jboot.exception.JbootIllegalConfigException; import io.jboot.utils.ArrayUtil; import io.jboot.utils.StrUtil; import org.apache.qpid.client.AMQAnyDestination; @@ -27,11 +30,11 @@ import org.apache.qpid.client.AMQConnection; import org.apache.qpid.jms.Connection; import javax.jms.*; +import java.util.Map; /** * @author 徐海峰 (27533892@qq.com) * @version V1.0 - * @Package io.jboot.core.mq.qpid */ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { @@ -39,10 +42,26 @@ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { private Connection connection = null; private boolean serializerEnable = true; + private JbootQpidmqConfig qpidConfig = null; + + private Thread queueThread; + private Thread topicThread; + + public JbootQpidmqImpl(JbootmqConfig config) { + super(config); + + String typeName = config.getTypeName(); + if (StrUtil.isNotBlank(typeName)) { + Map configModels = ConfigUtil.getConfigModels(JbootQpidmqConfig.class); + if (!configModels.containsKey(typeName)) { + throw new JbootIllegalConfigException("Please config \"jboot.mq.qpid." + typeName + ".host\" in your jboot.properties."); + } + qpidConfig = configModels.get(typeName); + } else { + qpidConfig = Jboot.config(JbootQpidmqConfig.class); + } + - public JbootQpidmqImpl() { - super(); - JbootQpidmqConfig qpidConfig = Jboot.config(JbootQpidmqConfig.class); serializerEnable = qpidConfig.isSerializerEnable(); try { @@ -64,6 +83,12 @@ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { } } + @Override + protected void onStopListening() { + queueThread.interrupt(); + topicThread.interrupt(); + } + @Override public void enqueue(Object message, String toChannel) { @@ -84,26 +109,26 @@ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { MessageProducer producer = session.createProducer(destination); producer.setTimeToLive(30000); - Message m = null; - if (!serializerEnable) { - m = session.createTextMessage((String) message); + Message sendMsg = null; + if (message instanceof Message) { + sendMsg = (Message) message; + } else if (!serializerEnable) { + sendMsg = session.createTextMessage((String) message); } else { byte[] data = getSerializer().serialize(message); - m = session.createBytesMessage(); - m.setIntProperty("data-len", data.length); - ((BytesMessage) m).writeBytes(data); + sendMsg = session.createBytesMessage(); + sendMsg.setIntProperty("data-len", data.length); + ((BytesMessage) sendMsg).writeBytes(data); } - producer.send(m); + producer.send(sendMsg); } catch (Exception e) { LOG.error(e.toString(), e); } } - private String getConnectionUrl() { - JbootQpidmqConfig qpidConfig = Jboot.config(JbootQpidmqConfig.class); - + public String getConnectionUrl() { StringBuffer url = new StringBuffer(); url.append("amqp://"); url.append(qpidConfig.getUsername()); @@ -129,7 +154,7 @@ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { return url.toString(); } - private String getQueueAddr(String channel) { + public String getQueueAddr(String channel) { StringBuffer addr = new StringBuffer(); addr.append("ADDR:"); addr.append(channel); @@ -138,7 +163,7 @@ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { return addr.toString(); } - private String getTopicAddr(String channel) { + public String getTopicAddr(String channel) { StringBuffer addr = new StringBuffer(); addr.append("ADDR:amq.topic/"); addr.append(channel); @@ -146,7 +171,7 @@ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { return addr.toString(); } - private void startReceiveMsgThread() throws Exception { + public void startReceiveMsgThread() throws Exception { if (ArrayUtil.isNullOrEmpty(this.channels)) { return; } @@ -157,12 +182,14 @@ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { String queueAddr = getQueueAddr(channel); Destination queue = new AMQAnyDestination(queueAddr); MessageConsumer queueConsumer = session.createConsumer(queue); - new Thread(new ReceiveMsgThread(queueConsumer, channel, serializerEnable)).start(); + queueThread = new Thread(new ReceiveMsgThread(queueConsumer, channel, serializerEnable)); + queueThread.start(); String topicAddr = getTopicAddr(channel); Destination topic = new AMQAnyDestination(topicAddr); MessageConsumer topicConsumer = session.createConsumer(topic); - new Thread(new ReceiveMsgThread(topicConsumer, channel, serializerEnable)).start(); + topicThread = new Thread(new ReceiveMsgThread(topicConsumer, channel, serializerEnable)); + topicThread.start(); } } @@ -180,7 +207,7 @@ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { @Override public void run() { try { - for (; ; ) { + while (isStarted) { Message message = consumer.receive(); if (message == null) { continue; @@ -201,7 +228,7 @@ public class JbootQpidmqImpl extends JbootmqBase implements Jbootmq { } if (object != null) { - notifyListeners(channel, object); + notifyListeners(channel, object, new QpidmqMessageContext(JbootQpidmqImpl.this, message)); } } } catch (Exception e) { diff --git a/src/main/java/io/jboot/components/mq/qpidmq/QpidmqMessageContext.java b/src/main/java/io/jboot/components/mq/qpidmq/QpidmqMessageContext.java new file mode 100644 index 0000000000000000000000000000000000000000..205e7b30b1fa31fb47dec2dfca5235e427830394 --- /dev/null +++ b/src/main/java/io/jboot/components/mq/qpidmq/QpidmqMessageContext.java @@ -0,0 +1,17 @@ +package io.jboot.components.mq.qpidmq; + +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.Jbootmq; + +import javax.jms.Message; + +public class QpidmqMessageContext extends MessageContext { + + + final Message orignalMessage; + + public QpidmqMessageContext(Jbootmq mq, Message orignalMessage) { + super(mq); + this.orignalMessage = orignalMessage; + } +} diff --git a/src/main/java/io/jboot/components/mq/rabbitmq/JbootRabbitmqConfig.java b/src/main/java/io/jboot/components/mq/rabbitmq/JbootRabbitmqConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..bd2c883d16611548062a8d6bed517403c03fa158 --- /dev/null +++ b/src/main/java/io/jboot/components/mq/rabbitmq/JbootRabbitmqConfig.java @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.mq.rabbitmq; + +import io.jboot.app.config.annotation.ConfigModel; + + +@ConfigModel(prefix = "jboot.mq.rabbitmq") +public class JbootRabbitmqConfig { + + + /** + * 默认 username 为 guest + */ + private String username; + + /** + * 默认密码为 guest + */ + private String password; + + private String host = "127.0.0.1"; + private int port = 5672; + private String virtualHost; + + private String broadcastChannelPrefix = "broadcast-"; + private String broadcastChannelRoutingKey = ""; + + //若配置为 false,则需要在 OnMessage 里,调用 MessageContext.getChannel().baseAck(或者baseNack)进行消费(或者标识消费失败) + private boolean autoAck = true; + + // 默认开启队列 + private boolean queueEnable = true; + private boolean queueDeclareDurable = false; + private boolean queueDeclareExclusive = false; + private boolean queueDeclareAutoDelete = false; + + + // 默认开启广播模式 + private boolean broadcastEnable = true; + + private String broadcastExchangeDeclareExchangeType = "fanout"; + private boolean broadcastExchangeDeclareDurable = false; + + private boolean broadcastQueueDeclareDurable = false; + private boolean broadcastQueueDeclareExclusive = false; + private boolean broadcastQueueDeclareAutoDelete = true; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getVirtualHost() { + return virtualHost; + } + + public void setVirtualHost(String virtualHost) { + this.virtualHost = virtualHost; + } + + public String getBroadcastChannelPrefix() { + return broadcastChannelPrefix; + } + + public void setBroadcastChannelPrefix(String broadcastChannelPrefix) { + this.broadcastChannelPrefix = broadcastChannelPrefix; + } + + public String getBroadcastChannelRoutingKey() { + return broadcastChannelRoutingKey; + } + + public void setBroadcastChannelRoutingKey(String broadcastChannelRoutingKey) { + this.broadcastChannelRoutingKey = broadcastChannelRoutingKey; + } + + public boolean isAutoAck() { + return autoAck; + } + + public void setAutoAck(boolean autoAck) { + this.autoAck = autoAck; + } + + public boolean isQueueDeclareDurable() { + return queueDeclareDurable; + } + + public void setQueueDeclareDurable(boolean queueDeclareDurable) { + this.queueDeclareDurable = queueDeclareDurable; + } + + public boolean isQueueDeclareExclusive() { + return queueDeclareExclusive; + } + + public void setQueueDeclareExclusive(boolean queueDeclareExclusive) { + this.queueDeclareExclusive = queueDeclareExclusive; + } + + public boolean isQueueDeclareAutoDelete() { + return queueDeclareAutoDelete; + } + + public void setQueueDeclareAutoDelete(boolean queueDeclareAutoDelete) { + this.queueDeclareAutoDelete = queueDeclareAutoDelete; + } + + public String getBroadcastExchangeDeclareExchangeType() { + return broadcastExchangeDeclareExchangeType; + } + + public void setBroadcastExchangeDeclareExchangeType(String broadcastExchangeDeclareExchangeType) { + this.broadcastExchangeDeclareExchangeType = broadcastExchangeDeclareExchangeType; + } + + public boolean isBroadcastExchangeDeclareDurable() { + return broadcastExchangeDeclareDurable; + } + + public void setBroadcastExchangeDeclareDurable(boolean broadcastExchangeDeclareDurable) { + this.broadcastExchangeDeclareDurable = broadcastExchangeDeclareDurable; + } + + public boolean isBroadcastQueueDeclareDurable() { + return broadcastQueueDeclareDurable; + } + + public void setBroadcastQueueDeclareDurable(boolean broadcastQueueDeclareDurable) { + this.broadcastQueueDeclareDurable = broadcastQueueDeclareDurable; + } + + public boolean isBroadcastQueueDeclareExclusive() { + return broadcastQueueDeclareExclusive; + } + + public void setBroadcastQueueDeclareExclusive(boolean broadcastQueueDeclareExclusive) { + this.broadcastQueueDeclareExclusive = broadcastQueueDeclareExclusive; + } + + public boolean isBroadcastQueueDeclareAutoDelete() { + return broadcastQueueDeclareAutoDelete; + } + + public void setBroadcastQueueDeclareAutoDelete(boolean broadcastQueueDeclareAutoDelete) { + this.broadcastQueueDeclareAutoDelete = broadcastQueueDeclareAutoDelete; + } + + public boolean isQueueEnable() { + return queueEnable; + } + + public void setQueueEnable(boolean queueEnable) { + this.queueEnable = queueEnable; + } + + public boolean isBroadcastEnable() { + return broadcastEnable; + } + + public void setBroadcastEnable(boolean broadcastEnable) { + this.broadcastEnable = broadcastEnable; + } +} diff --git a/src/main/java/io/jboot/components/mq/rabbitmq/JbootRabbitmqImpl.java b/src/main/java/io/jboot/components/mq/rabbitmq/JbootRabbitmqImpl.java index 7a4cd29dfbeca794a82792c3301a748c2d8abfa1..aa10128d261030b157080ea1688b7cc8286446f2 100644 --- a/src/main/java/io/jboot/components/mq/rabbitmq/JbootRabbitmqImpl.java +++ b/src/main/java/io/jboot/components/mq/rabbitmq/JbootRabbitmqImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,20 @@ */ package io.jboot.components.mq.rabbitmq; -import com.google.common.collect.Maps; import com.rabbitmq.client.*; import io.jboot.Jboot; +import io.jboot.app.JbootApplicationConfig; +import io.jboot.utils.ConfigUtil; import io.jboot.components.mq.Jbootmq; import io.jboot.components.mq.JbootmqBase; +import io.jboot.components.mq.JbootmqConfig; import io.jboot.exception.JbootException; +import io.jboot.exception.JbootIllegalConfigException; import io.jboot.utils.StrUtil; import java.io.IOException; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * doc : http://www.rabbitmq.com/api-guide.html @@ -33,101 +37,144 @@ public class JbootRabbitmqImpl extends JbootmqBase implements Jbootmq { private Connection connection; - private Map channelMap = Maps.newConcurrentMap(); + private Map channelMap = new ConcurrentHashMap<>(); - public JbootRabbitmqImpl() { - super(); - JbootmqRabbitmqConfig rabbitmqConfig = Jboot.config(JbootmqRabbitmqConfig.class); + private JbootRabbitmqConfig rabbitmqConfig; + private JbootApplicationConfig appConfig; - ConnectionFactory factory = new ConnectionFactory(); - factory.setHost(rabbitmqConfig.getHost()); - factory.setPort(rabbitmqConfig.getPort()); - if (StrUtil.isNotBlank(rabbitmqConfig.getVirtualHost())) { - factory.setVirtualHost(rabbitmqConfig.getVirtualHost()); - } - if (StrUtil.isNotBlank(rabbitmqConfig.getUsername())) { - factory.setUsername(rabbitmqConfig.getUsername()); - } + public JbootRabbitmqImpl(JbootmqConfig config) { + super(config); - if (StrUtil.isNotBlank(rabbitmqConfig.getPassword())) { - factory.setPassword(rabbitmqConfig.getPassword()); + String typeName = config.getTypeName(); + if (StrUtil.isNotBlank(typeName)) { + Map configModels = ConfigUtil.getConfigModels(JbootRabbitmqConfig.class); + if (!configModels.containsKey(typeName)) { + throw new JbootIllegalConfigException("Please config \"jboot.mq.rabbitmq." + typeName + ".host\" in your jboot.properties."); + } + rabbitmqConfig = configModels.get(typeName); + } else { + rabbitmqConfig = Jboot.config(JbootRabbitmqConfig.class); } try { + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(rabbitmqConfig.getHost()); + factory.setPort(rabbitmqConfig.getPort()); + + if (StrUtil.isNotBlank(rabbitmqConfig.getVirtualHost())) { + factory.setVirtualHost(rabbitmqConfig.getVirtualHost()); + } + + if (StrUtil.isNotBlank(rabbitmqConfig.getUsername())) { + factory.setUsername(rabbitmqConfig.getUsername()); + } + + if (StrUtil.isNotBlank(rabbitmqConfig.getPassword())) { + factory.setPassword(rabbitmqConfig.getPassword()); + } + connection = factory.newConnection(); + } catch (Exception e) { - throw new JbootException("can not connection rabbitmq server", e); + throw new JbootException("Can not connection rabbitmq server", e); } } + @Override protected void onStartListening() { for (String toChannel : channels) { - registerListner(getChannel(toChannel), toChannel); - } - } - private void registerListner(final Channel channel, String toChannel) { - if (channel == null) { - return; - } - try { + //广播通道 + if (rabbitmqConfig.isBroadcastEnable()) { + Channel broadcastChannel = getChannel(toChannel, false); + bindChannel(broadcastChannel, buildBroadcastChannelName(toChannel), toChannel); + } - /** - * Broadcast listener - */ - channel.basicConsume("", true, new DefaultConsumer(channel) { - @Override - public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { - Object o = getSerializer().deserialize(body); - notifyListeners(envelope.getExchange(), o); - } - }); + //队列通道 + if (rabbitmqConfig.isQueueEnable()) { + final Channel queueChannel = getChannel(toChannel, true); + bindChannel(queueChannel, toChannel, toChannel); + } + } + } + @Override + protected void onStopListening() { + connection.abort(); + } - /** - * Queue listener - */ - channel.basicConsume(toChannel, true, new DefaultConsumer(channel) { - @Override - public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { - Object o = getSerializer().deserialize(body); - notifyListeners(envelope.getRoutingKey(), o); - } - }); - } catch (IOException e) { - e.printStackTrace(); + public void bindChannel(Channel channel, String name, String orginaChannelName) { + if (channel != null) { + try { + channel.basicConsume(name, rabbitmqConfig.isAutoAck(), new DefaultConsumer(channel) { + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { + Object o = getSerializer().deserialize(body); + notifyListeners(orginaChannelName, o, new RabbitmqMessageContext(JbootRabbitmqImpl.this, channel, consumerTag, envelope, properties)); + } + }); + } catch (IOException e) { + e.printStackTrace(); + } } } - private Channel getChannel(String toChannel) { - - Channel channel = channelMap.get(toChannel); + public synchronized Channel getChannel(String toChannel, boolean queueMode) { + Channel channel = channelMap.get(toChannel + queueMode); if (channel == null) { try { channel = connection.createChannel(); - channel.queueDeclare(toChannel, false, false, false, null); - channel.exchangeDeclare(toChannel, BuiltinExchangeType.FANOUT); - String queueName = channel.queueDeclare().getQueue(); - channel.queueBind(queueName, toChannel, toChannel); - } catch (IOException e) { - throw new JbootException("can not create channel", e); - } - if (channel != null) { - channelMap.put(toChannel, channel); + //队列模式,只需要创建 队列就可以了,不需要定义交换机 + if (queueMode) { + channel.queueDeclare(toChannel + , rabbitmqConfig.isQueueDeclareDurable() + , rabbitmqConfig.isQueueDeclareExclusive() + , rabbitmqConfig.isQueueDeclareAutoDelete() + , null); + } + + //广播模式,需要定义交换机,发送者直接把消息发送到交换机里 + else { + channel.queueDeclare(buildBroadcastChannelName(toChannel) + , rabbitmqConfig.isBroadcastQueueDeclareDurable() + , rabbitmqConfig.isBroadcastQueueDeclareExclusive() + , rabbitmqConfig.isBroadcastQueueDeclareAutoDelete() + , null); + + + BuiltinExchangeType exchangeType = BuiltinExchangeType.FANOUT; + for (BuiltinExchangeType type : BuiltinExchangeType.values()) { + if (type.getType().equals(rabbitmqConfig.getBroadcastExchangeDeclareExchangeType())) { + exchangeType = type; + } + } + channel.exchangeDeclare(toChannel, exchangeType, rabbitmqConfig.isBroadcastExchangeDeclareDurable()); + channel.queueBind(buildBroadcastChannelName(toChannel), toChannel, rabbitmqConfig.getBroadcastChannelRoutingKey()); + } + + } catch (Exception ex) { + throw new JbootException("Can not create rabbit mq channel.", ex); } + + channelMap.put(toChannel + queueMode, channel); } return channel; } + public String buildBroadcastChannelName(String channel) { + return rabbitmqConfig.getBroadcastChannelPrefix() + channel; + } + + @Override public void enqueue(Object message, String toChannel) { - Channel channel = getChannel(toChannel); + Channel channel = getChannel(toChannel, true); try { byte[] bytes = getSerializer().serialize(message); channel.basicPublish("", toChannel, MessageProperties.BASIC, bytes); @@ -136,9 +183,10 @@ public class JbootRabbitmqImpl extends JbootmqBase implements Jbootmq { } } + @Override public void publish(Object message, String toChannel) { - Channel channel = getChannel(toChannel); + Channel channel = getChannel(toChannel, false); try { byte[] bytes = getSerializer().serialize(message); channel.basicPublish(toChannel, "", MessageProperties.BASIC, bytes); diff --git a/src/main/java/io/jboot/components/mq/rabbitmq/RabbitmqMessageContext.java b/src/main/java/io/jboot/components/mq/rabbitmq/RabbitmqMessageContext.java new file mode 100644 index 0000000000000000000000000000000000000000..e07f82e292840cd75c0ef545eaa8a715fdaae334 --- /dev/null +++ b/src/main/java/io/jboot/components/mq/rabbitmq/RabbitmqMessageContext.java @@ -0,0 +1,39 @@ +package io.jboot.components.mq.rabbitmq; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Envelope; +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.Jbootmq; + +public class RabbitmqMessageContext extends MessageContext { + + final Channel channel; + final String consumerTag; + final Envelope envelope; + final AMQP.BasicProperties properties; + + public RabbitmqMessageContext(Jbootmq mq, Channel channel, String consumerTag, Envelope envelope, AMQP.BasicProperties properties) { + super(mq); + this.channel = channel; + this.consumerTag = consumerTag; + this.envelope = envelope; + this.properties = properties; + } + + public Channel getChannel() { + return channel; + } + + public String getConsumerTag() { + return consumerTag; + } + + public Envelope getEnvelope() { + return envelope; + } + + public AMQP.BasicProperties getProperties() { + return properties; + } +} diff --git a/src/main/java/io/jboot/components/mq/redismq/JbootmqRedisConfig.java b/src/main/java/io/jboot/components/mq/redismq/JbootRedismqConfig.java similarity index 86% rename from src/main/java/io/jboot/components/mq/redismq/JbootmqRedisConfig.java rename to src/main/java/io/jboot/components/mq/redismq/JbootRedismqConfig.java index 62664e448b3763db9fc25263224309c01cad725c..cdca359114f66784c6b80ff643be58a4c9db597e 100644 --- a/src/main/java/io/jboot/components/mq/redismq/JbootmqRedisConfig.java +++ b/src/main/java/io/jboot/components/mq/redismq/JbootRedismqConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import io.jboot.support.redis.JbootRedisConfig; @ConfigModel(prefix = "jboot.mq.redis") -public class JbootmqRedisConfig extends JbootRedisConfig { +public class JbootRedismqConfig extends JbootRedisConfig { } diff --git a/src/main/java/io/jboot/components/mq/redismq/JbootRedismqImpl.java b/src/main/java/io/jboot/components/mq/redismq/JbootRedismqImpl.java index 89dd80a93e43de8b466756f4c4f4e22696c49329..f1249cd368cef983df8405c5303f030edd8ffeee 100644 --- a/src/main/java/io/jboot/components/mq/redismq/JbootRedismqImpl.java +++ b/src/main/java/io/jboot/components/mq/redismq/JbootRedismqImpl.java @@ -1,99 +1,151 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.components.mq.redismq; - -import com.jfinal.log.Log; -import io.jboot.Jboot; -import io.jboot.components.mq.Jbootmq; -import io.jboot.components.mq.JbootmqBase; -import io.jboot.exception.JbootIllegalConfigException; -import io.jboot.support.redis.JbootRedis; -import io.jboot.support.redis.JbootRedisManager; -import redis.clients.jedis.BinaryJedisPubSub; - - -public class JbootRedismqImpl extends JbootmqBase implements Jbootmq, Runnable { - - private static final Log LOG = Log.getLog(JbootRedismqImpl.class); - - private JbootRedis redis; - private Thread dequeueThread; - - public JbootRedismqImpl() { - super(); - JbootmqRedisConfig redisConfig = Jboot.config(JbootmqRedisConfig.class); - if (redisConfig.isConfigOk()) { - redis = JbootRedisManager.me().getRedis(redisConfig); - } else { - redis = Jboot.getRedis(); - } - - if (redis == null) { - throw new JbootIllegalConfigException("can not use redis mq (redis mq is default), " + - "please config jboot.redis.host=your-host , or use other mq component. "); - } - } - - @Override - protected void onStartListening() { - - String[] channels = this.channels.toArray(new String[]{}); - - redis.subscribe(new BinaryJedisPubSub() { - @Override - public void onMessage(byte[] channel, byte[] message) { - notifyListeners(redis.bytesToKey(channel), getSerializer().deserialize(message)); - } - }, redis.keysToBytesArray(channels)); - - dequeueThread = new Thread(this,"redis-dequeue-thread"); - dequeueThread.start(); - } - - - @Override - public void enqueue(Object message, String toChannel) { - redis.lpush(toChannel, message); - } - - - @Override - public void publish(Object message, String toChannel) { - redis.publish(redis.keyToBytes(toChannel), getSerializer().serialize(message)); - } - - - @Override - public void run() { - for (; ; ) { - try { - doExecuteDequeue(); - Thread.sleep(1); - } catch (Throwable ex) { - LOG.error(ex.toString(), ex); - } - } - } - - private void doExecuteDequeue() { - for (String channel : this.channels) { - Object data = redis.lpop(channel); - if (data != null) { - notifyListeners(channel, data); - } - } - } -} +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.mq.redismq; + +import com.jfinal.log.Log; +import io.jboot.Jboot; +import io.jboot.components.mq.Jbootmq; +import io.jboot.components.mq.JbootmqBase; +import io.jboot.components.mq.JbootmqConfig; +import io.jboot.components.mq.JbootmqMessageListener; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.support.redis.JbootRedis; +import io.jboot.support.redis.JbootRedisManager; +import io.jboot.utils.ConfigUtil; +import io.jboot.utils.StrUtil; +import redis.clients.jedis.BinaryJedisPubSub; + +import java.util.HashMap; +import java.util.Map; + + +public class JbootRedismqImpl extends JbootmqBase implements Jbootmq, Runnable { + + private static final Log LOG = Log.getLog(JbootRedismqImpl.class); + + private JbootRedis redis; + private Thread dequeueThread; + private BinaryJedisPubSub jedisPubSub; + private long interval = 100L; + + private Integer database = 0; + + public JbootRedismqImpl(JbootmqConfig config) { + super(config); + + JbootRedismqConfig redisConfig = null; + String typeName = config.getTypeName(); + if (StrUtil.isNotBlank(typeName)) { + Map configModels = ConfigUtil.getConfigModels(JbootRedismqConfig.class); + if (!configModels.containsKey(typeName)) { + throw new JbootIllegalConfigException("Please config \"jboot.mq.redis." + typeName + ".host\" in your jboot.properties."); + } + redisConfig = configModels.get(typeName); + } else { + redisConfig = Jboot.config(JbootRedismqConfig.class); + } + + database = redisConfig.getDatabase(); + + if (redisConfig.isConfigOk()) { + redis = JbootRedisManager.me().getRedis(redisConfig); + } else { + redis = Jboot.getRedis(); + } + + if (redis == null) { + throw new JbootIllegalConfigException("can not use redis mq (redis mq is default), " + + "please config jboot.redis.host=your-host , or use other mq component. "); + } + } + + private Map outterChannelMap = new HashMap<>(); + + @Override + protected void onStartListening() { + String[] channels = this.channels.toArray(new String[]{}); + jedisPubSub = new BinaryJedisPubSub() { + @Override + public void onMessage(byte[] channel, byte[] message) { + String thisChannel = redis.bytesToKey(channel); + String realChannel = outterChannelMap.get(thisChannel); + if (realChannel == null) { + LOG.warn("Jboot has recevied mq message, But it has no listener to process. channel:" + thisChannel); + } + notifyListeners(realChannel, getSerializer().deserialize(message) + , new RedismqMessageContext(JbootRedismqImpl.this)); + } + }; + + for (int i = 0; i< channels.length; i++) { + outterChannelMap.put(channels[i] + "_" + database, channels[i]); + channels[i] = channels[i] + "_" + database; + } + redis.subscribe(jedisPubSub, redis.keysToBytesArray(channels)); + + dequeueThread = new Thread(this, "redis-dequeue-thread"); + dequeueThread.start(); + } + + @Override + protected void onStopListening() { + if (jedisPubSub != null) { + jedisPubSub.unsubscribe(); + } + dequeueThread.interrupt(); + } + + + @Override + public void enqueue(Object message, String toChannel) { + redis.lpush(toChannel + "_" + database, message); + } + + + @Override + public void publish(Object message, String toChannel) { + redis.publish(redis.keyToBytes(toChannel + "_" + database), getSerializer().serialize(message)); + } + + @Override + public void run() { + while (isStarted) { + try { + doExecuteDequeue(); + Thread.sleep(interval); + } catch (Exception ex) { + LOG.error(ex.toString(), ex); + } + } + } + + public void doExecuteDequeue() { + for (String channel : this.channels) { + Object data = redis.lpop(channel + "_" + database); + if (data != null) { + notifyListeners(channel, data, new RedismqMessageContext(JbootRedismqImpl.this)); + } + } + } + + public long getInterval() { + return interval; + } + + public void setInterval(long interval) { + this.interval = interval; + } +} diff --git a/src/main/java/io/jboot/components/mq/redismq/RedismqMessageContext.java b/src/main/java/io/jboot/components/mq/redismq/RedismqMessageContext.java new file mode 100644 index 0000000000000000000000000000000000000000..170500c2053691720843c59d4c40d6c03ca09718 --- /dev/null +++ b/src/main/java/io/jboot/components/mq/redismq/RedismqMessageContext.java @@ -0,0 +1,13 @@ +package io.jboot.components.mq.redismq; + +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.Jbootmq; + +public class RedismqMessageContext extends MessageContext { + + + public RedismqMessageContext(Jbootmq mq) { + super(mq); + } + +} diff --git a/src/main/java/io/jboot/components/mq/rocketmq/JbootRocketmqConfig.java b/src/main/java/io/jboot/components/mq/rocketmq/JbootRocketmqConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..40d8bc5819d7bb050746f2be80955931a5100135 --- /dev/null +++ b/src/main/java/io/jboot/components/mq/rocketmq/JbootRocketmqConfig.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.mq.rocketmq; + +import io.jboot.app.config.annotation.ConfigModel; + +import java.io.Serializable; + + +@ConfigModel(prefix = "jboot.mq.rocket") +public class JbootRocketmqConfig implements Serializable { + + private String namesrvAddr; + private String namespace; + private String consumerGroup = "jboot_default_consumer_group"; + private Integer consumeMessageBatchMaxSize; + private String broadcastChannelPrefix = "broadcast-"; + private String subscribeSubExpression = "*"; + + private String producerGroup = "jboot_default_producer_group"; + private String instanceName; + private String clientIP; + private String createTopicKey; + private Boolean useTLS; + + private Boolean sendLatencyFaultEnable; + private Boolean sendMessageWithVIPChannel; + private Integer sendMsgTimeout; + + private Boolean retryAnotherBrokerWhenNotStoreOK; + private Integer retryTimesWhenSendAsyncFailed; + private Integer retryTimesWhenSendFailed; + + public String getNamesrvAddr() { + return namesrvAddr; + } + + public void setNamesrvAddr(String namesrvAddr) { + this.namesrvAddr = namesrvAddr; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public Integer getConsumeMessageBatchMaxSize() { + return consumeMessageBatchMaxSize; + } + + public void setConsumeMessageBatchMaxSize(Integer consumeMessageBatchMaxSize) { + this.consumeMessageBatchMaxSize = consumeMessageBatchMaxSize; + } + + public String getBroadcastChannelPrefix() { + return broadcastChannelPrefix; + } + + public void setBroadcastChannelPrefix(String broadcastChannelPrefix) { + this.broadcastChannelPrefix = broadcastChannelPrefix; + } + + public String getSubscribeSubExpression() { + return subscribeSubExpression; + } + + public void setSubscribeSubExpression(String subscribeSubExpression) { + this.subscribeSubExpression = subscribeSubExpression; + } + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = producerGroup; + } + + public String getInstanceName() { + return instanceName; + } + + public void setInstanceName(String instanceName) { + this.instanceName = instanceName; + } + + public String getClientIP() { + return clientIP; + } + + public void setClientIP(String clientIP) { + this.clientIP = clientIP; + } + + public String getCreateTopicKey() { + return createTopicKey; + } + + public void setCreateTopicKey(String createTopicKey) { + this.createTopicKey = createTopicKey; + } + + public Boolean getUseTLS() { + return useTLS; + } + + public void setUseTLS(Boolean useTLS) { + this.useTLS = useTLS; + } + + public Boolean getSendLatencyFaultEnable() { + return sendLatencyFaultEnable; + } + + public void setSendLatencyFaultEnable(Boolean sendLatencyFaultEnable) { + this.sendLatencyFaultEnable = sendLatencyFaultEnable; + } + + public Boolean getSendMessageWithVIPChannel() { + return sendMessageWithVIPChannel; + } + + public void setSendMessageWithVIPChannel(Boolean sendMessageWithVIPChannel) { + this.sendMessageWithVIPChannel = sendMessageWithVIPChannel; + } + + public Integer getSendMsgTimeout() { + return sendMsgTimeout; + } + + public void setSendMsgTimeout(Integer sendMsgTimeout) { + this.sendMsgTimeout = sendMsgTimeout; + } + + public Boolean getRetryAnotherBrokerWhenNotStoreOK() { + return retryAnotherBrokerWhenNotStoreOK; + } + + public void setRetryAnotherBrokerWhenNotStoreOK(Boolean retryAnotherBrokerWhenNotStoreOK) { + this.retryAnotherBrokerWhenNotStoreOK = retryAnotherBrokerWhenNotStoreOK; + } + + public Integer getRetryTimesWhenSendAsyncFailed() { + return retryTimesWhenSendAsyncFailed; + } + + public void setRetryTimesWhenSendAsyncFailed(Integer retryTimesWhenSendAsyncFailed) { + this.retryTimesWhenSendAsyncFailed = retryTimesWhenSendAsyncFailed; + } + + public Integer getRetryTimesWhenSendFailed() { + return retryTimesWhenSendFailed; + } + + public void setRetryTimesWhenSendFailed(Integer retryTimesWhenSendFailed) { + this.retryTimesWhenSendFailed = retryTimesWhenSendFailed; + } +} diff --git a/src/main/java/io/jboot/components/mq/rocketmq/JbootRocketmqImpl.java b/src/main/java/io/jboot/components/mq/rocketmq/JbootRocketmqImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5d0ed0873faebee69aba9d0fe7eaed6f6a047383 --- /dev/null +++ b/src/main/java/io/jboot/components/mq/rocketmq/JbootRocketmqImpl.java @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.mq.rocketmq; + +import com.jfinal.log.Log; +import io.jboot.Jboot; +import io.jboot.components.mq.Jbootmq; +import io.jboot.components.mq.JbootmqBase; +import io.jboot.components.mq.JbootmqConfig; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.ConfigUtil; +import io.jboot.utils.StrUtil; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; + +import java.util.Map; + + +public class JbootRocketmqImpl extends JbootmqBase implements Jbootmq { + + private static final Log LOG = Log.getLog(JbootRocketmqImpl.class); + private JbootRocketmqConfig rocketmqConfig; + private MQProducer mqProducer; + private DefaultMQPushConsumer queueConsumer; + private DefaultMQPushConsumer broadcastConsumer; + + public JbootRocketmqImpl(JbootmqConfig config) { + super(config); + + String typeName = config.getTypeName(); + if (StrUtil.isNotBlank(typeName)) { + Map configModels = ConfigUtil.getConfigModels(JbootRocketmqConfig.class); + if (!configModels.containsKey(typeName)) { + throw new JbootIllegalConfigException("Please config \"jboot.mq.rocket." + typeName + ".namesrvAddr\" in your jboot.properties."); + } + rocketmqConfig = configModels.get(typeName); + } else { + rocketmqConfig = Jboot.config(JbootRocketmqConfig.class); + } + } + + @Override + protected void onStartListening() { + try { + startQueueConsumer(); + startBroadcastConsumer(); + } catch (MQClientException e) { + LOG.error(e.toString(), e); + } + } + + @Override + protected void onStopListening() { + if (queueConsumer != null) { + queueConsumer.shutdown(); + } + + if (broadcastConsumer != null) { + broadcastConsumer.shutdown(); + } + } + + + public void startQueueConsumer() throws MQClientException { + // 实例化消费者 + queueConsumer = new DefaultMQPushConsumer(rocketmqConfig.getConsumerGroup()); + + // 设置NameServer的地址 + queueConsumer.setNamesrvAddr(rocketmqConfig.getNamesrvAddr()); + queueConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + + if (StrUtil.isNotBlank(rocketmqConfig.getNamespace())) { + queueConsumer.setNamespace(rocketmqConfig.getNamespace()); + } + + if (rocketmqConfig.getConsumeMessageBatchMaxSize() != null) { + queueConsumer.setConsumeMessageBatchMaxSize(rocketmqConfig.getConsumeMessageBatchMaxSize()); + } + + for (String channel : channels) { + queueConsumer.subscribe(channel, rocketmqConfig.getSubscribeSubExpression()); + } + + // 注册回调实现类来处理从broker拉取回来的消息 + queueConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + RokectmqMessageContext msgContext = new RokectmqMessageContext(this, msgs, context); + if (msgs != null && !msgs.isEmpty()) { + for (MessageExt messageExt : msgs) { + notifyListeners(messageExt.getTopic(), getSerializer().deserialize(messageExt.getBody()), msgContext); + } + } + + return msgContext.getReturnStatus(); + }); + + + queueConsumer.start(); + + } + + + public void startBroadcastConsumer() throws MQClientException { + // 实例化消费者 + broadcastConsumer = new DefaultMQPushConsumer(rocketmqConfig.getBroadcastChannelPrefix() + rocketmqConfig.getConsumerGroup()); + + // 设置NameServer的地址 + broadcastConsumer.setNamesrvAddr(rocketmqConfig.getNamesrvAddr()); + + if (StrUtil.isNotBlank(rocketmqConfig.getNamespace())) { + broadcastConsumer.setNamespace(rocketmqConfig.getNamespace()); + } + + broadcastConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + broadcastConsumer.setMessageModel(MessageModel.BROADCASTING); + + if (rocketmqConfig.getConsumeMessageBatchMaxSize() != null) { + broadcastConsumer.setConsumeMessageBatchMaxSize(rocketmqConfig.getConsumeMessageBatchMaxSize()); + } + + for (String channel : channels) { + broadcastConsumer.subscribe(rocketmqConfig.getBroadcastChannelPrefix() + channel, rocketmqConfig.getSubscribeSubExpression()); + } + + final int len = rocketmqConfig.getBroadcastChannelPrefix().length(); + broadcastConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + RokectmqMessageContext rokectMqMessageInfo = new RokectmqMessageContext(this, msgs, context); + if (msgs != null && !msgs.isEmpty()) { + for (MessageExt messageExt : msgs) { + String topic = messageExt.getTopic(); + notifyListeners(topic.substring(len), getSerializer().deserialize(messageExt.getBody()), rokectMqMessageInfo); + } + } + return rokectMqMessageInfo.getReturnStatus(); + }); + + broadcastConsumer.start(); + } + + + @Override + public void enqueue(Object message, String toChannel) { + doSendMessage(message, toChannel); + } + + + @Override + public void publish(Object message, String toChannel) { + doSendMessage(message, rocketmqConfig.getBroadcastChannelPrefix() + toChannel); + } + + + public void doSendMessage(Object message, String topic) { + try { + Message sendMsg = null; + if (message instanceof Message) { + sendMsg = (Message) message; + } else { + sendMsg = new Message(topic, getSerializer().serialize(message)); + } + + SendResult result = getMQProducer().send(sendMsg); + // if (result.getSendStatus() != SendStatus.SEND_OK) { + // 只要不等于 null 就是发送成功 + if (result == null) { + LOG.warn("Rockect mq send message fail!!!"); + } + } catch (Exception e) { + LOG.error(e.toString(), e); + } + } + + + public MQProducer getMQProducer() throws MQClientException { + if (mqProducer == null) { + synchronized (this) { + if (mqProducer == null) { + createMqProducer(); + } + } + } + return mqProducer; + } + + + public void createMqProducer() throws MQClientException { + DefaultMQProducer producer = new DefaultMQProducer(rocketmqConfig.getProducerGroup()); + producer.setNamesrvAddr(rocketmqConfig.getNamesrvAddr()); + + if (StrUtil.isNotBlank(rocketmqConfig.getNamespace())) { + producer.setNamespace(rocketmqConfig.getNamespace()); + } + + if (StrUtil.isNotBlank(rocketmqConfig.getInstanceName())) { + producer.setInstanceName(rocketmqConfig.getInstanceName()); + } + + if (StrUtil.isNotBlank(rocketmqConfig.getClientIP())) { + producer.setClientIP(rocketmqConfig.getClientIP()); + } + + if (StrUtil.isNotBlank(rocketmqConfig.getCreateTopicKey())) { + producer.setCreateTopicKey(rocketmqConfig.getCreateTopicKey()); + } + + if (rocketmqConfig.getUseTLS() != null) { + producer.setUseTLS(rocketmqConfig.getUseTLS()); + } + + if (rocketmqConfig.getSendLatencyFaultEnable() != null) { + producer.setSendLatencyFaultEnable(rocketmqConfig.getSendLatencyFaultEnable()); + } + + if (rocketmqConfig.getSendMessageWithVIPChannel() != null) { + producer.setSendMessageWithVIPChannel(rocketmqConfig.getSendMessageWithVIPChannel()); + } + + if (rocketmqConfig.getSendMsgTimeout() != null) { + producer.setSendMsgTimeout(rocketmqConfig.getSendMsgTimeout()); + } + + if (rocketmqConfig.getRetryAnotherBrokerWhenNotStoreOK() != null) { + producer.setRetryAnotherBrokerWhenNotStoreOK(rocketmqConfig.getRetryAnotherBrokerWhenNotStoreOK()); + } + + if (rocketmqConfig.getRetryTimesWhenSendAsyncFailed() != null) { + producer.setRetryTimesWhenSendAsyncFailed(rocketmqConfig.getRetryTimesWhenSendAsyncFailed()); + } + + if (rocketmqConfig.getRetryTimesWhenSendFailed() != null) { + producer.setRetryTimesWhenSendFailed(rocketmqConfig.getRetryTimesWhenSendFailed()); + } + + mqProducer = producer; + producer.start(); + } +} + + diff --git a/src/main/java/io/jboot/components/mq/rocketmq/RokectmqMessageContext.java b/src/main/java/io/jboot/components/mq/rocketmq/RokectmqMessageContext.java new file mode 100644 index 0000000000000000000000000000000000000000..64982294f7d955e0f3d888709443fcf201aee6c0 --- /dev/null +++ b/src/main/java/io/jboot/components/mq/rocketmq/RokectmqMessageContext.java @@ -0,0 +1,38 @@ +package io.jboot.components.mq.rocketmq; + +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.Jbootmq; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; + +public class RokectmqMessageContext extends MessageContext { + + private ConsumeConcurrentlyStatus returnStatus = ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + private final List msgs; + private final ConsumeConcurrentlyContext context; + + public RokectmqMessageContext(Jbootmq jbootmq, List msgs, ConsumeConcurrentlyContext context) { + super(jbootmq); + this.msgs = msgs; + this.context = context; + } + + public ConsumeConcurrentlyStatus getReturnStatus() { + return returnStatus; + } + + public void setReturnStatus(ConsumeConcurrentlyStatus returnStatus) { + this.returnStatus = returnStatus; + } + + public List getMsgs() { + return msgs; + } + + public ConsumeConcurrentlyContext getContext() { + return context; + } +} diff --git a/src/main/java/io/jboot/components/rpc/Jbootrpc.java b/src/main/java/io/jboot/components/rpc/Jbootrpc.java index 50ca881517a8d03145b99022105f3cc3635cfd2b..b52f663be8aa1c5f75ad10d43e8ab682ed0982f3 100644 --- a/src/main/java/io/jboot/components/rpc/Jbootrpc.java +++ b/src/main/java/io/jboot/components/rpc/Jbootrpc.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,11 @@ package io.jboot.components.rpc; public interface Jbootrpc { - public T serviceObtain(Class serviceClass, JbootrpcServiceConfig serviceConfig); + T serviceObtain(Class serviceClass, JbootrpcReferenceConfig referenceConfig); - public boolean serviceExport(Class interfaceClass, Object object, JbootrpcServiceConfig serviceConfig); + boolean serviceExport(Class interfaceClass, Object object, JbootrpcServiceConfig serviceConfig); - public void onInitBefore(); + void onStart(); - public void onInited(); + void onStop(); } diff --git a/src/main/java/io/jboot/components/rpc/JbootrpcBase.java b/src/main/java/io/jboot/components/rpc/JbootrpcBase.java index 614ab3cc341455b4c02ba8401b23413f6cdc08af..4562627f67dc620bc0f7d1f1de17be8a7a941d3d 100644 --- a/src/main/java/io/jboot/components/rpc/JbootrpcBase.java +++ b/src/main/java/io/jboot/components/rpc/JbootrpcBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,71 @@ package io.jboot.components.rpc; -import io.jboot.app.config.JbootConfigManager; +import io.jboot.Jboot; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public abstract class JbootrpcBase implements Jbootrpc { - private final JbootrpcConfig config = JbootConfigManager.me().get(JbootrpcConfig.class); + protected static final Map serviceCache = new ConcurrentHashMap<>(); + protected static JbootrpcConfig rpcConfig = Jboot.config(JbootrpcConfig.class); + private boolean started = false; + + + @Override + public T serviceObtain(Class interfaceClass, JbootrpcReferenceConfig config) { + String key = buildKey(interfaceClass, config); + T service = (T) serviceCache.get(key); + if (service == null) { + synchronized (this) { + service = (T) serviceCache.get(key); + if (service == null) { + // onStart 方法是在 app 启动完成后,Jboot 主动去调用的 + // 但是,在某些场景可能存在没有等 app 启动完成就去获取 Service 的情况 + // 此时,需要主动先调用下 onStart 方法 + invokeOnStartIfNecessary(); + + service = onServiceCreate(interfaceClass, config); + if (service != null) { + serviceCache.put(key, service); + } + } + } + } + return service; + } + + protected String buildKey(Class interfaceClass, JbootrpcReferenceConfig config) { + return interfaceClass.getName() + "@" + config.hashCode(); + } - public JbootrpcConfig getConfig() { - return config; + protected synchronized void invokeOnStartIfNecessary() { + if (!started) { + onStart(); + setStarted(true); + } } + + public abstract T onServiceCreate(Class serviceClass, JbootrpcReferenceConfig config); + @Override - public void onInitBefore() { + public void onStart() { } + @Override - public void onInited() { + public void onStop() { + + } + + public boolean isStarted() { + return started; + } + public void setStarted(boolean started) { + this.started = started; } } diff --git a/src/main/java/io/jboot/components/rpc/JbootrpcConfig.java b/src/main/java/io/jboot/components/rpc/JbootrpcConfig.java index b3876040479911b0a70d9d51e9bb38e6db8b47c5..1df540cd3f269888c966a120d96f3651da0ba63c 100644 --- a/src/main/java/io/jboot/components/rpc/JbootrpcConfig.java +++ b/src/main/java/io/jboot/components/rpc/JbootrpcConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,90 +18,48 @@ package io.jboot.components.rpc; import io.jboot.app.config.annotation.ConfigModel; import io.jboot.utils.StrUtil; -import java.io.IOException; -import java.net.ServerSocket; +import java.util.Map; @ConfigModel(prefix = "jboot.rpc") public class JbootrpcConfig { public static final String TYPE_DUBBO = "dubbo"; - public static final String TYPE_GRPC = "grpc"; public static final String TYPE_MOTAN = "motan"; - public static final String TYPE_THRIFT = "thrift"; public static final String TYPE_LOCAL = "local"; - public static final String REGISTRY_TYPE_CONSUL = "consul"; - public static final String REGISTRY_TYPE_ZOOKEEPER = "zookeeper"; - - - /** - * RPC的调用模式:registry 注册中心,direct直连模式 - */ - public static final String CALL_MODE_REGISTRY = "registry"; - public static final String CALL_MODE_DIRECT = "direct"; - + //使用的 RPC 类型 private String type; - private String callMode = CALL_MODE_REGISTRY; - - private int requestTimeOut = 5000; - /** - * 注册中心的相关调用 - */ - private String registryType = REGISTRY_TYPE_CONSUL; - private String registryAddress = "127.0.0.1:8500"; - private String registryName = "jboot"; - private String registryUserName; - private String registryPassword; - private String registryFile; + //用于直连时的配置,直连一般只用于测试环境 + //com.service.AAAService:127.0.0.1:8080,com.service.XXXService:127.0.0.1:8080 + private Map urls; - /** - * 启动检查 - */ - private boolean registryCheck = false; - private boolean consumerCheck = false; - private boolean providerCheck = false; + //服务的provider指定,可以通过注解 @RPCBean 指定,也可以通过此处指定,此处的配置优先于注解 + //com.service.AAAService:providerName,com.service.XXXService:providerName + private Map providers; + //服务的consumer指定,可以通过注解 @RPCInject 指定,也可以通过此处指定,此处的配置优先于注解 + //com.service.AAAService:providerName,com.service.XXXService:providerName + private Map consumers; - /** - * 直连模式的时候,配置的url - */ - private String directUrl; + //当不配置的时候,默认版本号 + private String defaultVersion = "1.0.0"; + //指定的服务的版本号 + private Map versions; - /** - * 对外暴露服务的相关配置 - */ - private String host; //当有多个IP的时候可以指定某个IP - private Integer defaultPort = 0; //0 为随机可用端口 - private String defaultGroup = "jboot"; - private String defaultVersion = "1.0"; + //当不指定的时候,默认分组 + private String defaultGroup; - private String proxy; + //指定的服务的分组 + private Map groups; - //多个过滤器请用英文逗号(,)隔开,默认添加opentracing过滤器,用于对rpc分布式调用的追踪 - private String filter; - - private String serialization; - - //重试次数,不配置默认使用框架默认配置 motan和dubbo可能不一样 - private Integer retries; - - //本地自动暴露 @RPCBean 的service + //本地自动暴露 @RPCBean 的 service private boolean autoExportEnable = true; - - public String getHost() { - return host; - } - - public void setHost(String host) { - this.host = host; - } - public String getType() { return type; } @@ -110,69 +68,40 @@ public class JbootrpcConfig { this.type = type; } - public int getRequestTimeOut() { - return requestTimeOut; + public Map getUrls() { + return urls; } - public void setRequestTimeOut(int requestTimeOut) { - this.requestTimeOut = requestTimeOut; + public void setUrls(Map urls) { + this.urls = urls; } - public String getRegistryType() { - return registryType; + public String getUrl(String serviceClass) { + return urls == null ? null : urls.get(serviceClass); } - public void setRegistryType(String registryType) { - this.registryType = registryType; + public Map getProviders() { + return providers; } - public String getRegistryAddress() { - return registryAddress; + public void setProviders(Map providers) { + this.providers = providers; } - public void setRegistryAddress(String registryAddress) { - this.registryAddress = registryAddress; + public String getProvider(String serviceClass) { + return providers == null ? null : providers.get(serviceClass); } - public String getRegistryName() { - return registryName; + public Map getConsumers() { + return consumers; } - public void setRegistryName(String registryName) { - this.registryName = registryName; + public void setConsumers(Map consumers) { + this.consumers = consumers; } - public Integer getDefaultPort() { - if (defaultPort == 0) { - ServerSocket serverSocket = null; - try { - serverSocket = new ServerSocket(0); //.getLocalPort(); - return serverSocket.getLocalPort(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (serverSocket != null) { - try { - serverSocket.close(); - } catch (IOException e) { - } - } - } - } - - return defaultPort; - } - - public void setDefaultPort(int defaultPort) { - this.defaultPort = defaultPort; - } - - public String getDefaultGroup() { - return defaultGroup; - } - - public void setDefaultGroup(String defaultGroup) { - this.defaultGroup = defaultGroup; + public String getConsumer(String serviceClass) { + return consumers == null ? null : consumers.get(serviceClass); } public String getDefaultVersion() { @@ -183,111 +112,38 @@ public class JbootrpcConfig { this.defaultVersion = defaultVersion; } - public String getRegistryUserName() { - return registryUserName; - } - - public void setRegistryUserName(String registryUserName) { - this.registryUserName = registryUserName; - } - - public String getRegistryPassword() { - return registryPassword; - } - - public void setRegistryPassword(String registryPassword) { - this.registryPassword = registryPassword; - } - - public String getRegistryFile() { - return registryFile; - } - - public void setRegistryFile(String registryFile) { - this.registryFile = registryFile; - } - - public String getCallMode() { - return callMode; - } - - public void setCallMode(String callMode) { - this.callMode = callMode; - } - - public String getDirectUrl() { - return directUrl; - } - - public void setDirectUrl(String directUrl) { - if (directUrl != null && directUrl.contains(":")) { - this.defaultPort = Integer.valueOf(directUrl.split(":")[1]); - } - this.directUrl = directUrl; - } - - public boolean isDirectCallMode() { - return CALL_MODE_DIRECT.equals(getCallMode()); - } - - public boolean isRegistryCallMode() { - return CALL_MODE_REGISTRY.equals(getCallMode()); - } - - public String getProxy() { - return proxy; + public Map getVersions() { + return versions; } - public void setProxy(String proxy) { - this.proxy = proxy; + public void setVersions(Map versions) { + this.versions = versions; } - public String getFilter() { - return filter; + public String getVersion(String className) { + String version = versions == null || versions.isEmpty() ? null : versions.get(className); + return version == null ? defaultVersion : version; } - public void setFilter(String filter) { - this.filter = filter; - } - - public boolean isRegistryCheck() { - return registryCheck; - } - - public void setRegistryCheck(boolean registryCheck) { - this.registryCheck = registryCheck; - } - - public boolean isConsumerCheck() { - return consumerCheck; - } - - public void setConsumerCheck(boolean consumerCheck) { - this.consumerCheck = consumerCheck; - } - - public boolean isProviderCheck() { - return providerCheck; - } - - public void setProviderCheck(boolean providerCheck) { - this.providerCheck = providerCheck; + public String getDefaultGroup() { + return defaultGroup; } - public String getSerialization() { - return serialization; + public void setDefaultGroup(String defaultGroup) { + this.defaultGroup = defaultGroup; } - public void setSerialization(String serialization) { - this.serialization = serialization; + public Map getGroups() { + return groups; } - public Integer getRetries() { - return retries; + public void setGroups(Map groups) { + this.groups = groups; } - public void setRetries(Integer retries) { - this.retries = retries; + public String getGroup(String className) { + String group = groups == null || groups.isEmpty() ? null : groups.get(className); + return group == null ? defaultGroup : group; } public boolean isAutoExportEnable() { diff --git a/src/main/java/io/jboot/components/rpc/JbootrpcManager.java b/src/main/java/io/jboot/components/rpc/JbootrpcManager.java index 75786959b3380009e083c72c6babb287621d4f19..5720b3c4703b54b598b5de09203a7524b09511e6 100644 --- a/src/main/java/io/jboot/components/rpc/JbootrpcManager.java +++ b/src/main/java/io/jboot/components/rpc/JbootrpcManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import io.jboot.exception.JbootException; import io.jboot.exception.JbootRpcException; import io.jboot.utils.ArrayUtil; import io.jboot.utils.ClassScanner; +import io.jboot.utils.ClassUtil; import java.io.Serializable; import java.util.List; @@ -44,18 +45,20 @@ public class JbootrpcManager { private Jbootrpc jbootrpc; private JbootrpcConfig defaultConfig = Jboot.config(JbootrpcConfig.class); + public Jbootrpc getJbootrpc() { if (jbootrpc == null) { if (!defaultConfig.isConfigOk()) { - throw new JbootRpcException("jboot rpc config is error, please config jboot.rpc.type = xxx in jboot.properties"); + throw new JbootRpcException("Jboot RPC config is error, please set up \"jboot.rpc.type\" config value"); } - jbootrpc = createJbootrpc(defaultConfig.getType()); + jbootrpc = createJbootrpc(defaultConfig); } return jbootrpc; } - private static Class[] default_excludes = new Class[]{ + + private static Class[] default_excludes = new Class[]{ JbootEventListener.class, JbootmqMessageListener.class, Serializable.class @@ -69,13 +72,17 @@ public class JbootrpcManager { } Jbootrpc jbootrpc = getJbootrpc(); - jbootrpc.onInitBefore(); + jbootrpc.onStart(); if (defaultConfig.isAutoExportEnable()) { exportRPCBean(jbootrpc); } + } - jbootrpc.onInited(); + public void stop() { + if (defaultConfig.isConfigOk()) { + getJbootrpc().onStop(); + } } @@ -85,18 +92,18 @@ public class JbootrpcManager { return; } - for (Class clazz : classes) { - RPCBean rpcBean = (RPCBean) clazz.getAnnotation(RPCBean.class); - Class[] inters = clazz.getInterfaces(); - if (inters == null || inters.length == 0) { - throw new JbootException(String.format("class[%s] has no interface, can not use @RPCBean", clazz)); + for (Class clazz : classes) { + RPCBean rpcBean = clazz.getAnnotation(RPCBean.class); + Class[] inters = clazz.getInterfaces(); + if (inters.length == 0) { + throw new JbootException("@RPCBean can not use for class \"" + ClassUtil.getUsefulClass(clazz).getName() + "\", because it has no interface."); } //对某些系统的类 进行排除,例如:Serializable 等 - Class[] excludes = ArrayUtil.concat(default_excludes, rpcBean.exclude()); - for (Class inter : inters) { + Class[] excludes = ArrayUtil.concat(default_excludes, rpcBean.exclude()); + for (Class inter : inters) { boolean isContinue = false; - for (Class ex : excludes) { + for (Class ex : excludes) { if (ex.isAssignableFrom(inter)) { isContinue = true; break; @@ -113,17 +120,20 @@ public class JbootrpcManager { } - public Jbootrpc createJbootrpc(String type) { + public Jbootrpc createJbootrpc(JbootrpcConfig config) { + if(!config.isConfigOk()){ + return null; + } - switch (type) { + switch (config.getType()) { + case JbootrpcConfig.TYPE_DUBBO: + return new JbootDubborpc(); case JbootrpcConfig.TYPE_MOTAN: return new JbootMotanrpc(); case JbootrpcConfig.TYPE_LOCAL: return new JbootLocalrpc(); - case JbootrpcConfig.TYPE_DUBBO: - return new JbootDubborpc(); default: - return JbootSpiLoader.load(Jbootrpc.class, type); + return JbootSpiLoader.load(Jbootrpc.class, config.getType()); } } diff --git a/src/main/java/io/jboot/components/rpc/JbootrpcReferenceConfig.java b/src/main/java/io/jboot/components/rpc/JbootrpcReferenceConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..a0e11bc37bc793f5f7aab4509c233f55c1b997e2 --- /dev/null +++ b/src/main/java/io/jboot/components/rpc/JbootrpcReferenceConfig.java @@ -0,0 +1,293 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.rpc; + +import java.io.Serializable; +import java.util.Objects; + +/** + * @author Michael Yang 杨福海 (fuhai999@gmail.com) + * @version V1.0 + */ +public class JbootrpcReferenceConfig implements Serializable { + + /** + * Service version, default value is empty string + */ + private String version; + + /** + * Service group, default value is empty string + */ + private String group; + + /** + * Service target URL for direct invocation, if this is specified, then registry center takes no effect. + */ + private String url; + + + /** + * Whether to enable generic invocation, default value is false + */ + private Boolean generic; + + + /** + * Check if service provider is available during boot up, default value is true + */ + private Boolean check; + + + /** + * Service invocation retry times + *

+ * see Constants#DEFAULT_RETRIES + */ + private Integer retries; + + + /** + * Load balance strategy, legal values include: random, roundrobin, leastactive + *

+ * see Constants#DEFAULT_LOADBALANCE + */ + private String loadbalance; + + /** + * Whether to enable async invocation, default value is false + */ + private Boolean async; + + /** + * Maximum active requests allowed, default value is 0 + */ + private Integer actives; + + + /** + * Timeout value for service invocation, default value is 0 + */ + private Integer timeout; + + /** + * Application associated name + */ + private String application; + + /** + * Module associated name + */ + private String module; + + + /** + * Consumer associated name + */ + private String consumer; + + /** + * Monitor associated name + */ + private String monitor; + + /** + * Registry associated name + */ + private String registry; + + /** + * the default value is "" + */ + private String protocol; + + /** + * Service tag name + */ + private String tag; + + /** + * The id + *

+ * default value is empty + */ + private String id; + + + public JbootrpcReferenceConfig() { + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public boolean isGeneric() { + return generic; + } + + public void setGeneric(boolean generic) { + this.generic = generic; + } + + public boolean isCheck() { + return check; + } + + public void setCheck(boolean check) { + this.check = check; + } + + public int getRetries() { + return retries; + } + + public void setRetries(int retries) { + this.retries = retries; + } + + public String getLoadbalance() { + return loadbalance; + } + + public void setLoadbalance(String loadbalance) { + this.loadbalance = loadbalance; + } + + public boolean isAsync() { + return async; + } + + public void setAsync(boolean async) { + this.async = async; + } + + public int getActives() { + return actives; + } + + public void setActives(int actives) { + this.actives = actives; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public String getApplication() { + return application; + } + + public void setApplication(String application) { + this.application = application; + } + + public String getModule() { + return module; + } + + public void setModule(String module) { + this.module = module; + } + + public String getConsumer() { + return consumer; + } + + public void setConsumer(String consumer) { + this.consumer = consumer; + } + + public String getMonitor() { + return monitor; + } + + public void setMonitor(String monitor) { + this.monitor = monitor; + } + + public String getRegistry() { + return registry; + } + + public void setRegistry(String registry) { + this.registry = registry; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JbootrpcReferenceConfig that = (JbootrpcReferenceConfig) o; + return Objects.equals(version, that.version) && Objects.equals(group, that.group) && Objects.equals(url, that.url) && Objects.equals(generic, that.generic) && Objects.equals(check, that.check) && Objects.equals(retries, that.retries) && Objects.equals(loadbalance, that.loadbalance) && Objects.equals(async, that.async) && Objects.equals(actives, that.actives) && Objects.equals(timeout, that.timeout) && Objects.equals(application, that.application) && Objects.equals(module, that.module) && Objects.equals(consumer, that.consumer) && Objects.equals(monitor, that.monitor) && Objects.equals(registry, that.registry) && Objects.equals(protocol, that.protocol) && Objects.equals(tag, that.tag) && Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(version, group, url, generic, check, retries, loadbalance, async, actives, timeout, application, module, consumer, monitor, registry, protocol, tag, id); + } +} diff --git a/src/main/java/io/jboot/components/rpc/JbootrpcServiceConfig.java b/src/main/java/io/jboot/components/rpc/JbootrpcServiceConfig.java index 5cd7d48fb2c9bf75279d4d5988d8a5f921b630a9..e256944a6f0fdf04befe36b603a4e68f06d24358 100644 --- a/src/main/java/io/jboot/components/rpc/JbootrpcServiceConfig.java +++ b/src/main/java/io/jboot/components/rpc/JbootrpcServiceConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,188 +15,195 @@ */ package io.jboot.components.rpc; -import io.jboot.Jboot; import io.jboot.components.rpc.annotation.RPCBean; -import io.jboot.components.rpc.annotation.RPCInject; -import io.jboot.utils.AnnotationUtil; import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.core.rpc */ public class JbootrpcServiceConfig implements Serializable { - - private int port = 0; - private String group; + /** + * Service version, default value is empty string + */ private String version; - private Integer timeout; - private Integer retries; - private Integer actives; - private String loadbalance; - private Boolean async; - private Boolean check; + /** + * Service group, default value is empty string + */ + private String group; - private String proxy; - private String filter; + /** + * Service path, default value is empty string + */ + private String path; - // 用于扩展,用户通过SPI扩展获取自定义的 Service的时候, - // 若以字段不满足,此时通过 params 自行扩展 - private Map params; + /** + * Whether to export service, default value is true + */ + private Boolean export; - private static JbootrpcConfig defaultConfig = Jboot.config(JbootrpcConfig.class); + /** + * Service token, default value is false + */ + private String token; + /** + * Whether the service is deprecated, default value is false + */ + private Boolean deprecated; - public JbootrpcServiceConfig() { - this.port = defaultConfig.getDefaultPort(); - this.group = defaultConfig.getDefaultGroup(); - this.version = defaultConfig.getDefaultVersion(); - this.timeout = defaultConfig.getRequestTimeOut(); - this.retries = defaultConfig.getRetries(); - this.proxy = defaultConfig.getProxy(); - this.filter = defaultConfig.getFilter(); - } - public JbootrpcServiceConfig(RPCInject inject) { - this(); + /** + * Whether to register the service to register center, default value is true + */ + private Boolean register; - int port = AnnotationUtil.getInt(inject.port(), -1); - int timeout = AnnotationUtil.getInt(inject.timeout(), -1); - int retries = AnnotationUtil.getInt(inject.retries(), -1); - int actives = AnnotationUtil.getInt(inject.actives(), -1); + /** + * Service weight value, default value is 0 + */ + private Integer weight; - String group = AnnotationUtil.get(inject.group()); - String version = AnnotationUtil.get(inject.version()); - String loadbalance = AnnotationUtil.get(inject.loadbalance()); + /** + * Service doc, default value is "" + */ + private String document; - Boolean async = AnnotationUtil.getBool(inject.async()); - Boolean check = AnnotationUtil.getBool(inject.check()); + /** + * Service invocation retry times + */ + private int retries; - if (port >= 0) { - this.port = port; - } + /** + * Load balance strategy, legal values include: random, roundrobin, leastactive + */ + private String loadbalance; - if (retries >= 0) { - this.retries = retries; - } - if (actives >= 0) { - this.actives = actives; - } + /** + * Application bean name + */ + private String application; - if (timeout >= 0) { - this.timeout = timeout; - } + /** + * Module bean name + */ + private String module; + /** + * Provider bean name + */ + private String provider; - if (group != null) { - this.group = group; - } + /** + * Protocol bean names + */ + private String protocol; - if (version != null) { - this.version = version; - } + /** + * Monitor bean name + */ + private String monitor; - if (loadbalance != null) { - this.loadbalance = loadbalance; - } + /** + * Registry bean name + */ + private String registry; - if (async != null) { - this.async = async; - } + /** + * Service tag name + */ + private String tag; - if (check != null) { - this.check = check; - } + public JbootrpcServiceConfig() { } public JbootrpcServiceConfig(RPCBean bean) { - this(); - - int port = AnnotationUtil.getInt(bean.port(), -1); - int timeout = AnnotationUtil.getInt(bean.timeout(), -1); - int actives = AnnotationUtil.getInt(bean.actives(), -1); + RPCUtil.appendAnnotation(RPCBean.class, bean, this); + } - String group = AnnotationUtil.get(bean.group()); - String version = AnnotationUtil.get(bean.version()); + public String getVersion() { + return version; + } - if (port >= 0) { - this.port = port; - } + public void setVersion(String version) { + this.version = version; + } - if (actives >= 0) { - this.actives = actives; - } + public String getGroup() { + return group; + } - if (timeout >= 0) { - this.timeout = timeout; - } + public void setGroup(String group) { + this.group = group; + } - if (group != null) { - this.group = group; - } + public String getPath() { + return path; + } - if (version != null) { - this.version = version; - } + public void setPath(String path) { + this.path = path; + } + public boolean isExport() { + return export; } + public void setExport(boolean export) { + this.export = export; + } - public String getGroup() { - return group; + public String getToken() { + return token; } - public void setGroup(String group) { - this.group = group; + public void setToken(String token) { + this.token = token; } - public String getVersion() { - return version; + public boolean isDeprecated() { + return deprecated; } - public void setVersion(String version) { - this.version = version; + public void setDeprecated(boolean deprecated) { + this.deprecated = deprecated; } - public int getPort() { - return port; + public boolean isRegister() { + return register; } - public void setPort(int port) { - this.port = port; + public void setRegister(boolean register) { + this.register = register; } - public Integer getTimeout() { - return timeout; + public int getWeight() { + return weight; } - public void setTimeout(Integer timeout) { - this.timeout = timeout; + public void setWeight(int weight) { + this.weight = weight; } - public Integer getRetries() { - return retries; + public String getDocument() { + return document; } - public void setRetries(Integer retries) { - this.retries = retries; + public void setDocument(String document) { + this.document = document; } - public Integer getActives() { - return actives; + public int getRetries() { + return retries; } - public void setActives(Integer actives) { - this.actives = actives; + public void setRetries(int retries) { + this.retries = retries; } public String getLoadbalance() { @@ -207,51 +214,59 @@ public class JbootrpcServiceConfig implements Serializable { this.loadbalance = loadbalance; } - public Boolean getAsync() { - return async; + public String getApplication() { + return application; } - public void setAsync(Boolean async) { - this.async = async; + public void setApplication(String application) { + this.application = application; } - public Boolean getCheck() { - return check; + public String getModule() { + return module; } - public void setCheck(Boolean check) { - this.check = check; + public void setModule(String module) { + this.module = module; } - public Map getParams() { - return params; + public String getProvider() { + return provider; } - public void setParams(Map params) { - this.params = params; + public void setProvider(String provider) { + this.provider = provider; } - public void addParam(Object key, Object value) { - if (params == null) { - params = new HashMap(); - } + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getMonitor() { + return monitor; + } - params.put(key, value); + public void setMonitor(String monitor) { + this.monitor = monitor; } - public String getProxy() { - return proxy; + public String getRegistry() { + return registry; } - public void setProxy(String proxy) { - this.proxy = proxy; + public void setRegistry(String registry) { + this.registry = registry; } - public String getFilter() { - return filter; + public String getTag() { + return tag; } - public void setFilter(String filter) { - this.filter = filter; + public void setTag(String tag) { + this.tag = tag; } } diff --git a/src/main/java/io/jboot/components/rpc/RPCUtil.java b/src/main/java/io/jboot/components/rpc/RPCUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..ed2c2589ae55ef721956c3fecf50386c189ee9d4 --- /dev/null +++ b/src/main/java/io/jboot/components/rpc/RPCUtil.java @@ -0,0 +1,252 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.rpc; + +import io.jboot.Jboot; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.CollectionUtil; +import io.jboot.utils.StrUtil; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/20 + */ +public class RPCUtil { + + /** + * 根据注解来设置对象内容,参考 dubbo 下的 AbstractConfig + * 参考 {{@org.apache.dubbo.config.AbstractConfig#appendAnnotation }} + * + * @param annotationClass + * @param annotation + * @param appendTo + */ + public static void appendAnnotation(Class annotationClass, Object annotation, Object appendTo) { + Method[] methods = annotationClass.getMethods(); + for (Method method : methods) { + if (method.getDeclaringClass() != Object.class + && method.getReturnType() != void.class + && !"toString".equals(method.getName()) + && !"hashCode".equals(method.getName()) + && !"annotationType".equals(method.getName()) + && method.getParameterTypes().length == 0 + && Modifier.isPublic(method.getModifiers()) + && !Modifier.isStatic(method.getModifiers())) { + try { + String property = method.getName(); + if ("interfaceClass".equals(property) || "interfaceName".equals(property)) { + property = "interface"; + } + String setter = "set" + property.substring(0, 1).toUpperCase() + property.substring(1); + Object value = method.invoke(annotation); + if (value != null && !value.equals(method.getDefaultValue())) { + Method setterMethod = null; + if ("filter".equals(property) || "listener".equals(property) || "registry".equals(property)) { + value = StrUtil.join((String[]) value, ","); + setterMethod = getMethod(appendTo.getClass(), setter, String.class); + } else if ("parameters".equals(property)) { + value = CollectionUtil.string2Map((String) value); + setterMethod = getMethod(appendTo.getClass(), setter, Map.class); + } else { + setterMethod = getMethod(appendTo.getClass(), setter, method.getReturnType()); + } + + //fixed : 值内容有 ${} 不生效的问题 + if (value instanceof String) { + value = AnnotationUtil.get((String) value); + } + + if (setterMethod != null) { + setterMethod.invoke(appendTo, value); + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + } + + + /** + * copy object field value to other + * + * @param copyFrom + * @param copyTo + */ + public static void copyDeclaredFields(Object copyFrom, Object copyTo) { + Field[] fields = copyFrom.getClass().getDeclaredFields(); + for (Field field : fields) { + try { + String setterName = "set" + StrUtil.firstCharToUpperCase(field.getName()); + Method method = getMethod(copyTo.getClass(), setterName, field.getType()); + + if (method != null) { + field.setAccessible(true); + Object value = field.get(copyFrom); + if (value != null && !value.equals("0") && !value.equals("")) { + method.invoke(copyTo, value); + } + } + } catch (Exception ex) { + // ignore + } + } + } + + + private static Method getMethod(Class clazz, String methodName, Class type) { + try { + return clazz.getMethod(methodName, getBoxedClass(type)); + } catch (NoSuchMethodException e) { + try { + return clazz.getMethod(methodName, type); + } catch (NoSuchMethodException ex) { + } + } + return null; + } + + + public static void copyNotNullFields(Object copyFrom, Object copyTo, boolean override) { + if (copyFrom == null || copyTo == null) { + return; + } + + Method[] fromObjGetters = copyFrom.getClass().getMethods(); + for (Method getter : fromObjGetters) { + String getterMethodName = getter.getName(); + if (getterMethodName.length() > 3 + && getterMethodName.startsWith("get") + && Modifier.isPublic(getter.getModifiers()) + && getter.getParameterCount() == 0) { + try { + Class returnType = getter.getReturnType(); + if (override) { + Object newData = getter.invoke(copyFrom); + if (newData != null) { + Method setter = copyTo.getClass().getMethod("set" + getterMethodName.substring(3), returnType); + setter.invoke(copyTo, newData); + } + } else { + Object oldData = copyTo.getClass().getMethod(getterMethodName).invoke(copyTo); + if (oldData == null) { + Object newData = getter.invoke(copyFrom); + if (newData != null) { + Method setter = copyTo.getClass().getMethod("set" + getterMethodName.substring(3), returnType); + setter.invoke(copyTo, newData); + } + } + } + } catch (Exception e) { + // doNothing + } + } + } + } + + + public static boolean isDefaultConfigExist(Class clazz, Map ret) { + try { + Field field = clazz.getField("isDefault"); + field.setAccessible(true); + for (Object obj : ret.values()) { + Boolean fieldValue = (Boolean) field.get(obj); + if (fieldValue != null && fieldValue) { + return true; + } + } + } catch (NoSuchFieldException | IllegalAccessException e) { + // do nothing + } + return false; + } + + + + /** + * 设置子节点配置,比如 ProviderConfig 下的 MethodsConfig ,或者 MethodConfig 下的 ArgumentConfig 等 + * + * @param appendTo 要设置的对象 + * @param dataSource 设置子节点的数据源 + * @param prefix 要设置对象的配置前缀(jboot.properties 下的配置) + * @param arrName 要设置对象的属性名 + * @param + * @param + */ + public static void setChildConfig(Map appendTo, Map dataSource, String prefix, String arrName) { + if (appendTo != null && !appendTo.isEmpty()) { + for (Map.Entry entry : appendTo.entrySet()) { + + String configKey = "default".equals(entry.getKey()) + ? prefix + "." + arrName //"jboot.rpc.dubbo.method.argument" + : prefix + "." + entry.getKey() + "." + arrName;//"jboot.rpc.dubbo.method."+entry.getKey()+".argument"; + + String configValue = Jboot.configValue(configKey, "default"); + List argCfgList = new ArrayList<>(); + Set arguments = StrUtil.splitToSetByComma(configValue); + for (String arg : arguments) { + F fillObj = dataSource.get(arg); + if (fillObj != null) { + argCfgList.add(fillObj); + } + } + if (!argCfgList.isEmpty()) { + try { + //setArguments/setMethods/setRegistries + String setterMethodName = arrName.equals("registry") + ? "setRegistries" + : "set" + StrUtil.firstCharToUpperCase(arrName) + "s"; + + Method method = entry.getValue().getClass().getMethod(setterMethodName, List.class); + method.invoke(entry.getValue(), argCfgList); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + + private static Class getBoxedClass(Class c) { + if (c == int.class) { + c = Integer.class; + } else if (c == boolean.class) { + c = Boolean.class; + } else if (c == long.class) { + c = Long.class; + } else if (c == float.class) { + c = Float.class; + } else if (c == double.class) { + c = Double.class; + } else if (c == char.class) { + c = Character.class; + } else if (c == byte.class) { + c = Byte.class; + } else if (c == short.class) { + c = Short.class; + } + return c; + } +} diff --git a/src/main/java/io/jboot/components/rpc/ReferenceConfigCache.java b/src/main/java/io/jboot/components/rpc/ReferenceConfigCache.java new file mode 100644 index 0000000000000000000000000000000000000000..8e2258d9bab6a9f4a4f5746b404db995c6302977 --- /dev/null +++ b/src/main/java/io/jboot/components/rpc/ReferenceConfigCache.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.rpc; + +import io.jboot.components.rpc.annotation.RPCInject; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class ReferenceConfigCache { + + private static Map refrenceConfigCache = new ConcurrentHashMap<>(); + + + public static JbootrpcReferenceConfig get(Class targetClass, RPCInject rpcInject) { + String cacheKey = buildKey(targetClass, rpcInject); + JbootrpcReferenceConfig referenceConfig = refrenceConfigCache.get(cacheKey); + if (referenceConfig == null) { + JbootrpcReferenceConfig config = new JbootrpcReferenceConfig(); + RPCUtil.appendAnnotation(RPCInject.class, rpcInject, config); + refrenceConfigCache.putIfAbsent(cacheKey, config); + + referenceConfig = refrenceConfigCache.get(cacheKey); + } + return referenceConfig; + } + + + + private static String buildKey(Class targetClass, RPCInject rpcInject) { + return targetClass.getName() + "@" + hashCode(rpcInject); + } + + + private static int hashCode(RPCInject a) { + return Objects.hash(a.group(), a.version(), a.url(), a.generic(), a.check(), a.retries(), a.loadbalance(), a.async(), a.actives(), a.timeout() + , a.application(), a.module(), a.consumer(), a.monitor(), Arrays.hashCode(a.registry()), a.protocol(), a.tag(), a.id()); + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/rpc/annotation/RPCBean.java b/src/main/java/io/jboot/components/rpc/annotation/RPCBean.java index 71b173bb3251facccf59c6a70f4f9a8a06b0936d..6d597c5384bce284303d6874a04fa21181fec937 100644 --- a/src/main/java/io/jboot/components/rpc/annotation/RPCBean.java +++ b/src/main/java/io/jboot/components/rpc/annotation/RPCBean.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,15 +22,101 @@ import java.lang.annotation.*; @Target({ElementType.TYPE}) public @interface RPCBean { - String port() default ""; + /** + * Service version, default value is empty string + */ + String version() default ""; - String timeout() default ""; + /** + * Service group, default value is empty string + */ + String group() default ""; - String actives() default ""; + /** + * Service path, default value is empty string + */ + String path() default ""; - String group() default ""; + /** + * Whether to export service, default value is true + */ + boolean export() default true; + + /** + * Service token, default value is false + */ + String token() default ""; + + /** + * Whether the service is deprecated, default value is false + */ + boolean deprecated() default false; + + + /** + * Whether to register the service to register center, default value is true + */ + boolean register() default true; + + /** + * Service weight value, default value is 0 + */ + int weight() default 0; + + /** + * Service doc, default value is "" + */ + String document() default ""; + + + /** + * Service invocation retry times + * + */ + int retries() default 2; + + /** + * Load balance strategy, legal values include: random, roundrobin, leastactive + * + */ + String loadbalance() default "random"; + + + /** + * Application bean name + */ + String application() default ""; + + /** + * Module bean name + */ + String module() default ""; + + /** + * Provider bean name + */ + String provider() default ""; + + /** + * Protocol bean names + */ + String[] protocol() default {}; + + /** + * Monitor bean name + */ + String monitor() default ""; + + /** + * Registry bean name + */ + String[] registry() default {}; + + /** + * Service tag name + */ + String tag() default ""; - String version() default ""; //当一个Service类实现对个接口的时候,可以通过这个排除不暴露某个实现接口 Class[] exclude() default Void.class; diff --git a/src/main/java/io/jboot/components/rpc/annotation/RPCInject.java b/src/main/java/io/jboot/components/rpc/annotation/RPCInject.java index 88e65439688b82dd7fc909805ff8040840c5f7fb..b95344e18f722fded5c0b10bdbc635c3fc8c0425 100644 --- a/src/main/java/io/jboot/components/rpc/annotation/RPCInject.java +++ b/src/main/java/io/jboot/components/rpc/annotation/RPCInject.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,22 +22,111 @@ import java.lang.annotation.*; @Target({ElementType.TYPE, ElementType.FIELD}) public @interface RPCInject { - String port() default ""; - String timeout() default ""; + /** + * Service version, default value is empty string + */ + String version() default ""; - String retries() default ""; + /** + * Service group, default value is empty string + */ + String group() default ""; - String actives() default ""; + /** + * Service target URL for direct invocation, if this is specified, then registry center takes no effect. + */ + String url() default ""; - String group() default ""; - String version() default ""; + /** + * Whether to enable generic invocation, default value is false + */ + boolean generic() default false; + + + /** + * Check if service provider is available during boot up, default value is true + */ + boolean check() default true; + + + /** + * Service invocation retry times + * + * see Constants#DEFAULT_RETRIES + */ + int retries() default 2; + + + /** + * Load balance strategy, legal values include: random, roundrobin, leastactive + * + * see Constants#DEFAULT_LOADBALANCE + */ + String loadbalance() default "random"; + + /** + * Whether to enable async invocation, default value is false + */ + boolean async() default false; + + /** + * Maximum active requests allowed, default value is 0 + */ + int actives() default 0; + + + /** + * Timeout value for service invocation, default value is 0 + */ + int timeout() default 0; + + /** + * Application associated name + */ + String application() default ""; + + /** + * Module associated name + */ + String module() default ""; + + + /** + * Consumer associated name + */ + String consumer() default ""; + + /** + * Monitor associated name + */ + String monitor() default ""; + + /** + * Registry associated name + */ + String[] registry() default {}; - String loadbalance() default ""; + /** + * The communication protocol of Dubbo Service + * + * @return the default value is "" + * @since 2.6.6 + */ + String protocol() default ""; - String async() default "false"; + /** + * Service tag name + */ + String tag() default ""; - String check() default "false"; + /** + * The id + * + * @return default value is empty + * @since 2.7.3 + */ + String id() default ""; } \ No newline at end of file diff --git a/src/main/java/io/jboot/components/rpc/dubbo/DubboUtil.java b/src/main/java/io/jboot/components/rpc/dubbo/DubboUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..66b0dfb88b5157a3916ec3212833fa5cf7d786da --- /dev/null +++ b/src/main/java/io/jboot/components/rpc/dubbo/DubboUtil.java @@ -0,0 +1,306 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.rpc.dubbo; + + +import io.jboot.Jboot; +import io.jboot.app.config.JbootConfigManager; +import io.jboot.utils.ConfigUtil; +import io.jboot.components.rpc.JbootrpcReferenceConfig; +import io.jboot.components.rpc.JbootrpcServiceConfig; +import io.jboot.components.rpc.RPCUtil; +import io.jboot.utils.StrUtil; +import org.apache.dubbo.config.*; +import org.apache.dubbo.config.bootstrap.DubboBootstrap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/19 + */ +class DubboUtil { + + private static Map protocolConfigMap = new ConcurrentHashMap<>(); + private static Map registryConfigMap = new ConcurrentHashMap<>(); + private static Map providerConfigMap = new ConcurrentHashMap<>(); + private static Map consumerConfigMap = new ConcurrentHashMap<>(); + + + public static void stopDubbo() { + DubboBootstrap.getInstance().stop(); + } + + public static void initDubbo() { + DubboBootstrap dubboBootstrap = DubboBootstrap.getInstance(); + + //application 配置 + ApplicationConfig applicationConfig = config(ApplicationConfig.class, "jboot.rpc.dubbo.application"); + if (StrUtil.isBlank(applicationConfig.getName())) { + applicationConfig.setName("jboot"); + } + //默认关闭 qos + if (applicationConfig.getQosEnable() == null) { + applicationConfig.setQosEnable(false); + } + + dubboBootstrap.application(applicationConfig); + + + //ssl 配置 + SslConfig sslConfig = config(SslConfig.class, "jboot.rpc.dubbo.ssl"); + dubboBootstrap.ssl(sslConfig); + + + //monitor 配置 + MonitorConfig monitorConfig = config(MonitorConfig.class, "jboot.rpc.dubbo.monitor"); + dubboBootstrap.monitor(monitorConfig); + + + //monitor 配置 + MetricsConfig metricsConfig = config(MetricsConfig.class, "jboot.rpc.dubbo.metrics"); + dubboBootstrap.metrics(metricsConfig); + + + //module 配置 + ModuleConfig moduleConfig = config(ModuleConfig.class, "jboot.rpc.dubbo.module"); + dubboBootstrap.module(moduleConfig); + + + //元数据 配置 + Map metadataReportConfigs = configs(MetadataReportConfig.class, "jboot.rpc.dubbo.metadata-report"); + if (!metadataReportConfigs.isEmpty()) { + dubboBootstrap.metadataReports(toList(metadataReportConfigs)); + } + + //配置中心配置 + Map configCenterConfigs = configs(ConfigCenterConfig.class, "jboot.rpc.dubbo.config-center"); + if (!configCenterConfigs.isEmpty()) { + dubboBootstrap.configCenters(toList(configCenterConfigs)); + } + + + //协议 配置 + Map protocolConfigs = configs(ProtocolConfig.class, "jboot.rpc.dubbo.protocol"); + if (!protocolConfigs.isEmpty()) { + protocolConfigMap.putAll(protocolConfigs); + dubboBootstrap.protocols(toList(protocolConfigs)); + } + + //服务注册中心 配置 + Map registryConfigs = configs(RegistryConfig.class, "jboot.rpc.dubbo.registry"); + if (!registryConfigs.isEmpty()) { + registryConfigMap.putAll(registryConfigs); + dubboBootstrap.registries(toList(registryConfigs)); + } + //没有配置注册中心,一般只用于希望此服务网提供直连的方式给客户端使用 + else { + RegistryConfig config = new RegistryConfig(); + config.setAddress(RegistryConfig.NO_AVAILABLE); + dubboBootstrap.registry(config); + } + + + //方法参数配置 配置 + Map argumentConfigs = configs(ArgumentConfig.class, "jboot.rpc.dubbo.argument"); + + + //方法配置 配置 + Map methodConfigs = configs(MethodConfig.class, "jboot.rpc.dubbo.method"); + for (MethodConfig methodConfig : methodConfigs.values()) { + Object onreturn = methodConfig.getOnreturn(); + if (onreturn instanceof String && ((String) onreturn).contains(".")) { + String[] objectAndMethod = ((String) onreturn).split("\\."); + methodConfig.setOnreturn(Jboot.getBean(objectAndMethod[0])); + methodConfig.setOnreturnMethod(objectAndMethod[1]); + } + + Object oninvoke = methodConfig.getOninvoke(); + if (oninvoke instanceof String && ((String) oninvoke).contains(".")) { + String[] objectAndMethod = ((String) oninvoke).split("\\."); + methodConfig.setOninvoke(Jboot.getBean(objectAndMethod[0])); + methodConfig.setOninvokeMethod(objectAndMethod[1]); + } + + Object onthrow = methodConfig.getOnthrow(); + if (onthrow instanceof String && ((String) onthrow).contains(".")) { + String[] objectAndMethod = ((String) onthrow).split("\\."); + methodConfig.setOnthrow(Jboot.getBean(objectAndMethod[0])); + methodConfig.setOnthrowMethod(objectAndMethod[1]); + } + } + + RPCUtil.setChildConfig(methodConfigs, argumentConfigs, "jboot.rpc.dubbo.method", "argument"); + + + //消费者 配置 + Map consumerConfigs = configs(ConsumerConfig.class, "jboot.rpc.dubbo.consumer"); + RPCUtil.setChildConfig(consumerConfigs, methodConfigs, "jboot.rpc.dubbo.consumer", "method"); +// RPCUtil.setChildConfig(consumerConfigs, protocolConfigs, "jboot.rpc.dubbo.consumer", "protocol"); + RPCUtil.setChildConfig(consumerConfigs, registryConfigs, "jboot.rpc.dubbo.consumer", "registry"); + + + if (!consumerConfigs.isEmpty()) { + consumerConfigMap.putAll(consumerConfigs); + dubboBootstrap.consumers(toList(consumerConfigs)); + } + + //服务提供者 配置 + Map providerConfigs = configs(ProviderConfig.class, "jboot.rpc.dubbo.provider"); + RPCUtil.setChildConfig(providerConfigs, methodConfigs, "jboot.rpc.dubbo.provider", "method"); + RPCUtil.setChildConfig(providerConfigs, protocolConfigs, "jboot.rpc.dubbo.provider", "protocol"); + RPCUtil.setChildConfig(providerConfigs, registryConfigs, "jboot.rpc.dubbo.provider", "registry"); + + if (!providerConfigs.isEmpty()) { + providerConfigMap.putAll(providerConfigs); + dubboBootstrap.providers(toList(providerConfigs)); + } + } + + + public static ReferenceConfig toReferenceConfig(JbootrpcReferenceConfig jbootReferenceConfig) { + ReferenceConfig referenceConfig = new ReferenceConfig(); + RPCUtil.copyDeclaredFields(jbootReferenceConfig, referenceConfig); + + // reference consumer + if (jbootReferenceConfig.getConsumer() != null) { + referenceConfig.setConsumer(consumerConfigMap.get(jbootReferenceConfig.getConsumer())); + } + // set default consumer + else { + for (ConsumerConfig consumerConfig : consumerConfigMap.values()) { + if (consumerConfig.isDefault() != null && consumerConfig.isDefault()) { + referenceConfig.setConsumer(consumerConfig); + } + } + } + + + //service registry + if (StrUtil.isNotBlank(jbootReferenceConfig.getRegistry())) { + referenceConfig.setRegistryIds(jbootReferenceConfig.getRegistry()); + } + // set default registry + else { + for (RegistryConfig registryConfig : registryConfigMap.values()) { + if (registryConfig.isDefault() != null && registryConfig.isDefault()) { + referenceConfig.setRegistry(registryConfig); + } + } + } + + return referenceConfig; + } + + + public static ServiceConfig toServiceConfig(JbootrpcServiceConfig jbootServiceConfig) { + ServiceConfig serviceConfig = new ServiceConfig(); + RPCUtil.copyDeclaredFields(jbootServiceConfig, serviceConfig); + + // service provider + if (StrUtil.isNotBlank(jbootServiceConfig.getProvider())) { + serviceConfig.setProviderIds(jbootServiceConfig.getProvider()); + } + // set default provider + else { + for (ProviderConfig providerConfig : providerConfigMap.values()) { + if (providerConfig.isDefault() != null && providerConfig.isDefault()) { + serviceConfig.setProvider(providerConfig); + } + } + } + + // service protocol + if (StrUtil.isNotBlank(jbootServiceConfig.getProtocol())) { + serviceConfig.setProtocolIds(jbootServiceConfig.getProtocol()); + } + // set default protocol + else { + for (ProtocolConfig protocolConfig : protocolConfigMap.values()) { + if (protocolConfig.isDefault() != null && protocolConfig.isDefault()) { + serviceConfig.setProtocol(protocolConfig); + } + } + } + + // service registry + if (StrUtil.isNotBlank(jbootServiceConfig.getRegistry())) { + serviceConfig.setRegistryIds(jbootServiceConfig.getRegistry()); + } + // set default registry + else { + for (RegistryConfig registryConfig : registryConfigMap.values()) { + if (registryConfig.isDefault() != null && registryConfig.isDefault()) { + serviceConfig.setRegistry(registryConfig); + } + } + } + + return serviceConfig; + } + + public static ConsumerConfig getConsumer(String name) { + return consumerConfigMap.get(name); + } + + + public static ProviderConfig getProvider(String name) { + return providerConfigMap.get(name); + } + + + private static T config(Class clazz, String prefix) { + return JbootConfigManager.me().get(clazz, prefix, null); + } + + + private static Map configs(Class clazz, String prefix) { + Map ret = ConfigUtil.getConfigModels(clazz, prefix); + + if (ret.size() > 0 && !RPCUtil.isDefaultConfigExist(clazz, ret)) { + for (Map.Entry entry : ret.entrySet()) { + if ("default".equals(entry.getKey())) { + if (entry.getValue() instanceof ProviderConfig) { + ((ProviderConfig) entry.getValue()).setDefault(true); + } else if (entry.getValue() instanceof ConsumerConfig) { + ((ConsumerConfig) entry.getValue()).setDefault(true); + } else if (entry.getValue() instanceof ProtocolConfig) { + ((ProtocolConfig) entry.getValue()).setDefault(true); + } else if (entry.getValue() instanceof RegistryConfig) { + ((RegistryConfig) entry.getValue()).setDefault(true); + } + } + } + } + return ret; + } + + + private static List toList(Map map) { + List list = new ArrayList<>(map.size()); + for (Map.Entry entry : map.entrySet()) { + AbstractConfig config = (AbstractConfig) entry.getValue(); + config.setId(entry.getKey()); + list.add((T) config); + } + return list; + } + + +} diff --git a/src/main/java/io/jboot/components/rpc/dubbo/JbootDubborpc.java b/src/main/java/io/jboot/components/rpc/dubbo/JbootDubborpc.java index c301b35dcc42427f7b416f7853884215f22bf723..52ee88de88e65bd418f4ea2651fbf2766211d98a 100644 --- a/src/main/java/io/jboot/components/rpc/dubbo/JbootDubborpc.java +++ b/src/main/java/io/jboot/components/rpc/dubbo/JbootDubborpc.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,220 +15,88 @@ */ package io.jboot.components.rpc.dubbo; -import io.jboot.Jboot; import io.jboot.components.rpc.JbootrpcBase; +import io.jboot.components.rpc.JbootrpcReferenceConfig; import io.jboot.components.rpc.JbootrpcServiceConfig; -import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.components.rpc.RPCUtil; import io.jboot.utils.StrUtil; -import org.apache.dubbo.config.*; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.config.ServiceConfig; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -/** - * dubbo 官方宣布开始维护了 - * 开始添加dubbo的支持 - */ public class JbootDubborpc extends JbootrpcBase { - private static final Map singletons = new ConcurrentHashMap<>(); - - private JbootDubborpcConfig dubboConfig; - private RegistryConfig registryConfig; - - public JbootDubborpc() { - dubboConfig = Jboot.config(JbootDubborpcConfig.class); - - registryConfig = new RegistryConfig(); - registryConfig.setCheck(getConfig().isRegistryCheck()); - - if (getConfig().getRegistryFile() != null) { - registryConfig.setFile(getConfig().getRegistryFile()); - } - - /** - * 注册中心的调用模式 - */ - if (getConfig().isRegistryCallMode()) { - - registryConfig.setProtocol(getConfig().getRegistryType()); - registryConfig.setAddress(getConfig().getRegistryAddress()); - registryConfig.setUsername(getConfig().getRegistryUserName()); - registryConfig.setPassword(getConfig().getRegistryPassword()); - } - /** - * 直连模式 - */ - else if (getConfig().isDirectCallMode()) { - registryConfig.setAddress(RegistryConfig.NO_AVAILABLE); - } + @Override + public void onStart() { + DubboUtil.initDubbo(); } - - private ApplicationConfig createApplicationConfig(String group) { - ApplicationConfig applicationConfig = new ApplicationConfig(); - applicationConfig.setName(group); - if (dubboConfig.getQosEnable() != null && dubboConfig.getQosEnable()) { - applicationConfig.setQosEnable(true); - applicationConfig.setQosPort(dubboConfig.getQosPort()); - applicationConfig.setQosAcceptForeignIp(dubboConfig.getQosAcceptForeignIp()); - } else { - applicationConfig.setQosEnable(false); - } - - return applicationConfig; + @Override + public void onStop() { + DubboUtil.stopDubbo(); } - @Override - public T serviceObtain(Class serviceClass, JbootrpcServiceConfig serviceConfig) { - - String key = String.format("%s:%s:%s", serviceClass.getName(), serviceConfig.getGroup(), serviceConfig.getVersion()); - - T object = (T) singletons.get(key); - if (object != null) { - return object; + public T onServiceCreate(Class interfaceClass, JbootrpcReferenceConfig config) { + ReferenceConfig reference = DubboUtil.toReferenceConfig(config); + reference.setInterface(interfaceClass); + + String directUrl = rpcConfig.getUrl(interfaceClass.getName()); + if (StrUtil.isNotBlank(directUrl)) { + if (URL.valueOf(directUrl).getProtocol() == null) { + directUrl = "dubbo://" + directUrl; + } + reference.setUrl(directUrl); } + String consumer = rpcConfig.getConsumer(interfaceClass.getName()); + if (consumer != null) { + reference.setConsumer(DubboUtil.getConsumer(consumer)); + } - // 注意:ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接 - // 引用远程服务 - // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏 - ReferenceConfig reference = new ReferenceConfig(); - reference.setApplication(createApplicationConfig(serviceConfig.getGroup())); - reference.setInterface(serviceClass); - reference.setCheck(getConfig().isConsumerCheck()); + //copy consumer config to Refercence + RPCUtil.copyNotNullFields(reference.getConsumer(), reference, false); - initReference(reference, serviceConfig); - /** - * 注册中心的调用模式 - */ - if (getConfig().isRegistryCallMode()) { - reference.setRegistry(registryConfig); // 多个注册中心可以用setRegistries() + if (reference.getGroup() == null) { + reference.setGroup(rpcConfig.getGroup(interfaceClass.getName())); } - /** - * 直连调用模式 - */ - else if (getConfig().isDirectCallMode()) { - if (StrUtil.isBlank(getConfig().getDirectUrl())) { - throw new JbootIllegalConfigException("directUrl must not be blank if you use direct call mode,please config jboot.rpc.directUrl value"); - } - reference.setUrl(getConfig().getDirectUrl()); + if (reference.getVersion() == null) { + reference.setVersion(rpcConfig.getVersion(interfaceClass.getName())); } - // 注意:此代理对象内部封装了所有通讯细节,对象较重,请缓存复用 - object = reference.get(); - - if (object != null) { - singletons.put(key, object); - } - - return object; + return reference.get(); } @Override - public boolean serviceExport(Class interfaceClass, Object object, JbootrpcServiceConfig serviceConfig) { - - ProtocolConfig protocolConfig = dubboConfig.newProtocolConfig(); - - if (protocolConfig.getHost() == null && getConfig().getHost() != null) { - protocolConfig.setHost(getConfig().getHost()); - } - - if (protocolConfig.getSerialization() == null && getConfig().getSerialization() != null) { - protocolConfig.setSerialization(getConfig().getSerialization()); - } - - protocolConfig.setPort(serviceConfig.getPort()); - - //此实例很重,封装了与注册中心的连接,请自行缓存,否则可能造成内存和连接泄漏 - ServiceConfig service = new ServiceConfig(); - service.setApplication(createApplicationConfig(serviceConfig.getGroup())); - service.setRegistry(registryConfig); // 多个注册中心可以用setRegistries() - service.setProtocol(protocolConfig); // 多个协议可以用setProtocols() + public boolean serviceExport(Class interfaceClass, Object object, JbootrpcServiceConfig config) { + ServiceConfig service = DubboUtil.toServiceConfig(config); service.setInterface(interfaceClass); service.setRef((T) object); - initService(service, serviceConfig); - - - // 暴露及注册服务 - service.export(); - - return true; - } - - - private static void initReference(ReferenceConfig reference, JbootrpcServiceConfig config) { - reference.setGroup(config.getGroup()); - reference.setVersion(config.getVersion()); - reference.setTimeout(config.getTimeout()); - - if (config.getRetries() != null) { - reference.setRetries(config.getRetries()); + String provider = rpcConfig.getProvider(interfaceClass.getName()); + if (provider != null) { + service.setProvider(DubboUtil.getProvider(provider)); } - if (config.getActives() != null) { - reference.setActives(config.getActives()); - } + //copy provider config to Service + RPCUtil.copyNotNullFields(service.getProvider(), service, true); - if (config.getLoadbalance() != null) { - reference.setLoadbalance(config.getLoadbalance()); - } - - if (config.getAsync() != null) { - reference.setAsync(config.getAsync()); - } - - if (config.getCheck() != null) { - reference.setCheck(config.getCheck()); - } - if (StrUtil.isNotBlank(config.getProxy())) { - reference.setProxy(config.getProxy()); + if (service.getGroup() == null) { + service.setGroup(rpcConfig.getGroup(interfaceClass.getName())); } - - if (StrUtil.isNotBlank(config.getFilter())) { - reference.setFilter(config.getFilter()); + if (service.getVersion() == null) { + service.setVersion(rpcConfig.getVersion(interfaceClass.getName())); } - } - - private static void initService(ServiceConfig service, JbootrpcServiceConfig config) { - - service.setGroup(config.getGroup()); - service.setVersion(config.getVersion()); - service.setTimeout(config.getTimeout()); - - if (config.getRetries() != null) { - service.setRetries(config.getRetries()); - } - - if (config.getActives() != null) { - service.setActives(config.getActives()); - } - - if (config.getLoadbalance() != null) { - service.setLoadbalance(config.getLoadbalance()); - } - - if (config.getAsync() != null) { - service.setAsync(config.getAsync()); - } - - if (StrUtil.isNotBlank(config.getProxy())) { - service.setProxy(config.getProxy()); - } - - if (StrUtil.isNotBlank(config.getFilter())) { - service.setFilter(config.getFilter()); - } + service.export(); + return true; } - } diff --git a/src/main/java/io/jboot/components/rpc/dubbo/JbootDubborpcConfig.java b/src/main/java/io/jboot/components/rpc/dubbo/JbootDubborpcConfig.java deleted file mode 100644 index bf0c5bd1a61b5d9209a7e4a225971fc993029995..0000000000000000000000000000000000000000 --- a/src/main/java/io/jboot/components/rpc/dubbo/JbootDubborpcConfig.java +++ /dev/null @@ -1,485 +0,0 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.components.rpc.dubbo; - -import io.jboot.app.config.annotation.ConfigModel; -import org.apache.dubbo.config.ProtocolConfig; - - -@ConfigModel(prefix = "jboot.rpc.dubbo") -public class JbootDubborpcConfig { - - - private String protocolName = "dubbo"; //default is dubbo - private String protocolServer = "netty"; //default is netty - private String protocolContextPath; - private String protocolTransporter; - private Integer protocolThreads; - - private Boolean qosEnable = false; - private Integer qosPort; - private Boolean qosAcceptForeignIp; - - - private String protocolHost; - - // service port - private Integer protocolPort; - - // context path - private String protocolContextpath; - - // thread pool - private String protocolThreadpool; - - // thread pool size (fixed size) - - // IO thread pool size (fixed size) - private Integer protocolIothreads; - - // thread pool's queue length - private Integer protocolQueues; - - // max acceptable connections - private Integer protocolAccepts; - - // protocol codec - private String protocolCodec; - - // serialization - private String protocolSerialization; - - // charset - private String protocolCharset; - - // payload max length - private Integer protocolPayload; - - // buffer size - private Integer protocolBuffer; - - // heartbeat interval - private Integer protocolHeartbeat; - - // access log - private String protocolAccesslog; - - - // how information is exchanged - private String protocolExchanger; - - // thread dispatch mode - private String protocolDispatcher; - - // networker - private String protocolNetworker; - - // client impl - private String protocolClient; - - // supported telnet commands, separated with comma. - private String protocolTelnet; - - // command line prompt - private String protocolPrompt; - - // status check - private String protocolStatus; - - // whether to register - private Boolean protocolRegister; - - // parameters - // 是否长连接 - // TODO add this to provider config - private Boolean protocolKeepAlive; - - // TODO add this to provider config - private String protocolOptimizer; - - private String protocolExtension; - - // if it's default - private Boolean protocolIsDefault; - - - public String getProtocolName() { - return protocolName; - } - - public void setProtocolName(String protocolName) { - this.protocolName = protocolName; - } - - public String getProtocolServer() { - return protocolServer; - } - - public void setProtocolServer(String protocolServer) { - this.protocolServer = protocolServer; - } - - public String getProtocolContextPath() { - return protocolContextPath; - } - - public void setProtocolContextPath(String protocolContextPath) { - this.protocolContextPath = protocolContextPath; - } - - public String getProtocolTransporter() { - return protocolTransporter; - } - - public void setProtocolTransporter(String protocolTransporter) { - this.protocolTransporter = protocolTransporter; - } - - public int getProtocolThreads() { - return protocolThreads; - } - - public void setProtocolThreads(int protocolThreads) { - this.protocolThreads = protocolThreads; - } - - public Boolean getQosEnable() { - return qosEnable; - } - - public void setQosEnable(Boolean qosEnable) { - this.qosEnable = qosEnable; - } - - public Integer getQosPort() { - return qosPort; - } - - public void setQosPort(Integer qosPort) { - this.qosPort = qosPort; - } - - public Boolean getQosAcceptForeignIp() { - return qosAcceptForeignIp; - } - - public void setQosAcceptForeignIp(Boolean qosAcceptForeignIp) { - this.qosAcceptForeignIp = qosAcceptForeignIp; - } - - public String getProtocolHost() { - return protocolHost; - } - - public void setProtocolHost(String protocolHost) { - this.protocolHost = protocolHost; - } - - public Integer getProtocolPort() { - return protocolPort; - } - - public void setProtocolPort(Integer protocolPort) { - this.protocolPort = protocolPort; - } - - public String getProtocolContextpath() { - return protocolContextpath; - } - - public void setProtocolContextpath(String protocolContextpath) { - this.protocolContextpath = protocolContextpath; - } - - public String getProtocolThreadpool() { - return protocolThreadpool; - } - - public void setProtocolThreadpool(String protocolThreadpool) { - this.protocolThreadpool = protocolThreadpool; - } - - public Integer getProtocolIothreads() { - return protocolIothreads; - } - - public void setProtocolIothreads(Integer protocolIothreads) { - this.protocolIothreads = protocolIothreads; - } - - public Integer getProtocolQueues() { - return protocolQueues; - } - - public void setProtocolQueues(Integer protocolQueues) { - this.protocolQueues = protocolQueues; - } - - public Integer getProtocolAccepts() { - return protocolAccepts; - } - - public void setProtocolAccepts(Integer protocolAccepts) { - this.protocolAccepts = protocolAccepts; - } - - public String getProtocolCodec() { - return protocolCodec; - } - - public void setProtocolCodec(String protocolCodec) { - this.protocolCodec = protocolCodec; - } - - public String getProtocolSerialization() { - return protocolSerialization; - } - - public void setProtocolSerialization(String protocolSerialization) { - this.protocolSerialization = protocolSerialization; - } - - public String getProtocolCharset() { - return protocolCharset; - } - - public void setProtocolCharset(String protocolCharset) { - this.protocolCharset = protocolCharset; - } - - public Integer getProtocolPayload() { - return protocolPayload; - } - - public void setProtocolPayload(Integer protocolPayload) { - this.protocolPayload = protocolPayload; - } - - public Integer getProtocolBuffer() { - return protocolBuffer; - } - - public void setProtocolBuffer(Integer protocolBuffer) { - this.protocolBuffer = protocolBuffer; - } - - public Integer getProtocolHeartbeat() { - return protocolHeartbeat; - } - - public void setProtocolHeartbeat(Integer protocolHeartbeat) { - this.protocolHeartbeat = protocolHeartbeat; - } - - public String getProtocolAccesslog() { - return protocolAccesslog; - } - - public void setProtocolAccesslog(String protocolAccesslog) { - this.protocolAccesslog = protocolAccesslog; - } - - public String getProtocolExchanger() { - return protocolExchanger; - } - - public void setProtocolExchanger(String protocolExchanger) { - this.protocolExchanger = protocolExchanger; - } - - public String getProtocolDispatcher() { - return protocolDispatcher; - } - - public void setProtocolDispatcher(String protocolDispatcher) { - this.protocolDispatcher = protocolDispatcher; - } - - public String getProtocolNetworker() { - return protocolNetworker; - } - - public void setProtocolNetworker(String protocolNetworker) { - this.protocolNetworker = protocolNetworker; - } - - public String getProtocolClient() { - return protocolClient; - } - - public void setProtocolClient(String protocolClient) { - this.protocolClient = protocolClient; - } - - public String getProtocolTelnet() { - return protocolTelnet; - } - - public void setProtocolTelnet(String protocolTelnet) { - this.protocolTelnet = protocolTelnet; - } - - public String getProtocolPrompt() { - return protocolPrompt; - } - - public void setProtocolPrompt(String protocolPrompt) { - this.protocolPrompt = protocolPrompt; - } - - public String getProtocolStatus() { - return protocolStatus; - } - - public void setProtocolStatus(String protocolStatus) { - this.protocolStatus = protocolStatus; - } - - public Boolean getProtocolRegister() { - return protocolRegister; - } - - public void setProtocolRegister(Boolean protocolRegister) { - this.protocolRegister = protocolRegister; - } - - public Boolean getProtocolKeepAlive() { - return protocolKeepAlive; - } - - public void setProtocolKeepAlive(Boolean protocolKeepAlive) { - this.protocolKeepAlive = protocolKeepAlive; - } - - public String getProtocolOptimizer() { - return protocolOptimizer; - } - - public void setProtocolOptimizer(String protocolOptimizer) { - this.protocolOptimizer = protocolOptimizer; - } - - public String getProtocolExtension() { - return protocolExtension; - } - - public void setProtocolExtension(String protocolExtension) { - this.protocolExtension = protocolExtension; - } - - public Boolean getProtocolIsDefault() { - return protocolIsDefault; - } - - public void setProtocolIsDefault(Boolean protocolIsDefault) { - this.protocolIsDefault = protocolIsDefault; - } - - public ProtocolConfig newProtocolConfig() { - - ProtocolConfig config = new ProtocolConfig(); - - if (this.protocolDispatcher != null) { - config.setDispatcher(this.protocolDispatcher); - } - if (this.protocolIsDefault != null) { - config.setDefault(this.protocolIsDefault); - } - if (this.protocolClient != null) { - config.setClient(this.protocolClient); - } - if (this.protocolCharset != null) { - config.setCharset(this.protocolCharset); - } - if (this.protocolAccepts != null) { - config.setAccepts(this.protocolAccepts); - } - if (this.protocolAccesslog != null) { - config.setAccesslog(this.protocolAccesslog); - } - if (this.protocolBuffer != null) { - config.setBuffer(this.protocolBuffer); - } - if (this.protocolCodec != null) { - config.setCodec(this.protocolCodec); - } - if (this.protocolContextpath != null) { - config.setContextpath(this.protocolContextpath); - } - if (this.protocolExchanger != null) { - config.setExchanger(this.protocolExchanger); - } - if (this.protocolExtension != null) { - config.setExtension(this.protocolExtension); - } - if (this.protocolHeartbeat != null) { - config.setHeartbeat(this.protocolHeartbeat); - } - if (this.protocolHost != null) { - config.setHost(this.protocolHost); - } - if (this.protocolIothreads != null) { - config.setIothreads(this.protocolIothreads); - } - if (this.protocolKeepAlive != null) { - config.setKeepAlive(this.protocolKeepAlive); - } - if (this.protocolName != null) { - config.setName(this.protocolName); - } - if (this.protocolNetworker != null) { - config.setNetworker(this.protocolNetworker); - } - if (this.protocolOptimizer != null) { - config.setOptimizer(this.protocolOptimizer); - } - if (this.protocolPayload != null) { - config.setPayload(this.protocolPayload); - } - if (this.protocolPort != null) { - config.setPort(this.protocolPort); - } - if (this.protocolPrompt != null) { - config.setPrompt(this.protocolPrompt); - } - if (this.protocolQueues != null) { - config.setQueues(this.protocolQueues); - } - if (this.protocolRegister != null) { - config.setRegister(this.protocolRegister); - } - if (this.protocolSerialization != null) { - config.setSerialization(this.protocolSerialization); - } - if (this.protocolServer != null) { - config.setServer(this.protocolServer); - } - if (this.protocolStatus != null) { - config.setStatus(this.protocolStatus); - } - if (this.protocolTelnet != null) { - config.setTelnet(this.protocolTelnet); - } - if (this.protocolThreadpool != null) { - config.setThreadpool(this.protocolThreadpool); - } - if (this.protocolThreads != null) { - config.setThreads(this.protocolThreads); - } - - return config; - } -} diff --git a/src/main/java/io/jboot/components/rpc/local/JbootLocalrpc.java b/src/main/java/io/jboot/components/rpc/local/JbootLocalrpc.java index c68b0d358f54cc6485122b7a21f047940a133f60..6874ffb80521ad42e0269aaef81a881b3695b38b 100644 --- a/src/main/java/io/jboot/components/rpc/local/JbootLocalrpc.java +++ b/src/main/java/io/jboot/components/rpc/local/JbootLocalrpc.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package io.jboot.components.rpc.local; import com.jfinal.aop.Aop; import com.jfinal.aop.AopManager; import io.jboot.components.rpc.JbootrpcBase; +import io.jboot.components.rpc.JbootrpcReferenceConfig; import io.jboot.components.rpc.JbootrpcServiceConfig; import io.jboot.utils.ClassUtil; @@ -25,15 +26,18 @@ import io.jboot.utils.ClassUtil; public class JbootLocalrpc extends JbootrpcBase { @Override - public T serviceObtain(Class serviceClass, JbootrpcServiceConfig serviceConfig) { + public T serviceObtain(Class serviceClass, JbootrpcReferenceConfig referenceConfig) { return Aop.get(serviceClass); } @Override - public boolean serviceExport(Class interfaceClass, Object object, JbootrpcServiceConfig serviceConfig) { + public T onServiceCreate(Class serviceClass, JbootrpcReferenceConfig config) { + return null; + } + @Override + public boolean serviceExport(Class interfaceClass, Object object, JbootrpcServiceConfig serviceConfig) { AopManager.me().addMapping(interfaceClass, ClassUtil.getUsefulClass(object.getClass())); - return true; } } diff --git a/src/main/java/io/jboot/components/rpc/motan/JbootMotanrpc.java b/src/main/java/io/jboot/components/rpc/motan/JbootMotanrpc.java index a2606fe0b46dfc167960e96d9d7107ab7f397653..21f899daa6464ea7c81793c719a4a84e253fc0ac 100644 --- a/src/main/java/io/jboot/components/rpc/motan/JbootMotanrpc.java +++ b/src/main/java/io/jboot/components/rpc/motan/JbootMotanrpc.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,137 +16,80 @@ package io.jboot.components.rpc.motan; import com.weibo.api.motan.common.MotanConstants; -import com.weibo.api.motan.config.*; +import com.weibo.api.motan.config.RefererConfig; +import com.weibo.api.motan.config.ServiceConfig; import com.weibo.api.motan.util.MotanSwitcherUtil; +import io.jboot.Jboot; import io.jboot.components.rpc.JbootrpcBase; +import io.jboot.components.rpc.JbootrpcReferenceConfig; import io.jboot.components.rpc.JbootrpcServiceConfig; -import io.jboot.exception.JbootIllegalConfigException; import io.jboot.utils.StrUtil; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - public class JbootMotanrpc extends JbootrpcBase { + private MotanrpcConfig defaultConfig = Jboot.config(MotanrpcConfig.class); - private RegistryConfig registryConfig; - private ProtocolConfig protocolConfig; - - private static final Map singletons = new ConcurrentHashMap<>(); - - public JbootMotanrpc() { - registryConfig = new RegistryConfig(); - registryConfig.setCheck(String.valueOf(getConfig().isRegistryCheck())); - - /** - * 注册中心的调用模式 - */ - if (getConfig().isRegistryCallMode()) { - - registryConfig.setRegProtocol(getConfig().getRegistryType()); - registryConfig.setAddress(getConfig().getRegistryAddress()); - registryConfig.setName(getConfig().getRegistryName()); - } - - /** - * 直连模式 - */ - else if (getConfig().isDirectCallMode()) { - registryConfig.setRegProtocol("local"); - } - - - protocolConfig = new ProtocolConfig(); - protocolConfig.setId("motan"); - protocolConfig.setName("motan"); - - if (StrUtil.isNotBlank(getConfig().getProxy())) { - protocolConfig.setFilter(getConfig().getProxy()); - } - - if (StrUtil.isNotBlank(getConfig().getSerialization())) { - protocolConfig.setSerialization(getConfig().getSerialization()); - } - + @Override + public void onStart() { + MotanUtil.initMotan(); } - @Override - public T serviceObtain(Class serviceClass, JbootrpcServiceConfig serviceConfig) { - - String key = String.format("%s:%s:%s", serviceClass.getName(), serviceConfig.getGroup(), serviceConfig.getVersion()); + public T onServiceCreate(Class interfaceClass, JbootrpcReferenceConfig config) { + RefererConfig referer = MotanUtil.toRefererConfig(config); + referer.setInterface(interfaceClass); - T object = (T) singletons.get(key); - if (object != null) { - return object; + String directUrl = rpcConfig.getUrl(interfaceClass.getName()); + if (StrUtil.isNotBlank(directUrl)) { + referer.setDirectUrl(directUrl); } - RefererConfig refererConfig = new RefererConfig(); - - // 设置接口及实现类 - refererConfig.setProtocol(protocolConfig); - refererConfig.setInterface(serviceClass); - refererConfig.setCheck(String.valueOf(getConfig().isConsumerCheck())); - - initInterface(refererConfig, serviceConfig); - - /** - * 注册中心模式 - */ - if (getConfig().isRegistryCallMode()) { - refererConfig.setRegistry(registryConfig); + String consumer = rpcConfig.getConsumer(interfaceClass.getName()); + if (consumer != null) { + referer.setBasicReferer(MotanUtil.getBaseReferer(consumer)); } - /** - * 直连模式 - */ - else if (getConfig().isDirectCallMode()) { - if (StrUtil.isBlank(getConfig().getDirectUrl())) { - throw new JbootIllegalConfigException("directUrl must not be blank if you use direct call mode,please config jboot.rpc.directUrl value"); - } - refererConfig.setDirectUrl(getConfig().getDirectUrl()); + if (referer.getGroup() == null) { + referer.setGroup(rpcConfig.getGroup(interfaceClass.getName())); } - - object = refererConfig.getRef(); - - if (object != null) { - singletons.put(key, object); + if (referer.getVersion() == null) { + referer.setVersion(rpcConfig.getVersion(interfaceClass.getName())); } - return object; + + return referer.getRef(); } @Override - public boolean serviceExport(Class interfaceClass, Object object, JbootrpcServiceConfig serviceConfig) { + public boolean serviceExport(Class interfaceClass, Object object, JbootrpcServiceConfig config) { synchronized (this) { MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, false); - ServiceConfig motanServiceConfig = new ServiceConfig(); - motanServiceConfig.setRegistry(registryConfig); - motanServiceConfig.setProtocol(protocolConfig); - - // 设置接口及实现类 - motanServiceConfig.setInterface(interfaceClass); - motanServiceConfig.setRef((T) object); + ServiceConfig service = MotanUtil.toServiceConfig(config); + service.setInterface(interfaceClass); + service.setRef((T) object); + service.setShareChannel(true); + service.setExport(defaultConfig.getExport(interfaceClass.getName())); + service.setHost(defaultConfig.getHost(interfaceClass.getName())); - if (StrUtil.isNotBlank(getConfig().getHost())) { - motanServiceConfig.setHost(getConfig().getHost()); + String provider = rpcConfig.getProvider(interfaceClass.getName()); + if (provider != null) { + service.setBasicService(MotanUtil.getBaseService(provider)); } + if (service.getGroup() == null) { + service.setGroup(rpcConfig.getGroup(interfaceClass.getName())); + } - motanServiceConfig.setShareChannel(true); - motanServiceConfig.setExport(String.format("motan:%s", serviceConfig.getPort())); - motanServiceConfig.setCheck(String.valueOf(getConfig().isProviderCheck())); - - initInterface(motanServiceConfig, serviceConfig); - - - motanServiceConfig.export(); + if (service.getVersion() == null) { + service.setVersion(rpcConfig.getVersion(interfaceClass.getName())); + } + service.export(); MotanSwitcherUtil.setSwitcherValue(MotanConstants.REGISTRY_HEARTBEAT_SWITCHER, true); } @@ -154,37 +97,4 @@ public class JbootMotanrpc extends JbootrpcBase { } - private static void initInterface(AbstractInterfaceConfig interfaceConfig, JbootrpcServiceConfig config) { - - interfaceConfig.setGroup(config.getGroup()); - interfaceConfig.setVersion(config.getVersion()); - interfaceConfig.setRequestTimeout(config.getTimeout()); - - - if (config.getActives() != null) { - interfaceConfig.setActives(config.getActives()); - } - - if (config.getAsync() != null) { - interfaceConfig.setAsync(config.getAsync()); - } - - if (config.getRetries() != null) { - interfaceConfig.setRetries(config.getRetries()); - } - - if (config.getCheck() != null) { - interfaceConfig.setCheck(config.getCheck().toString()); - } - - if (StrUtil.isNotBlank(config.getProxy())) { - interfaceConfig.setProxy(config.getProxy()); - } - - if (StrUtil.isNotBlank(config.getFilter())) { - interfaceConfig.setFilter(config.getFilter()); - } - } - - } diff --git a/src/main/java/io/jboot/components/rpc/motan/MotanUtil.java b/src/main/java/io/jboot/components/rpc/motan/MotanUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..44b66242d1c82dedd0f6f7cde961d176afbd879c --- /dev/null +++ b/src/main/java/io/jboot/components/rpc/motan/MotanUtil.java @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.rpc.motan; + +import com.weibo.api.motan.config.*; +import com.weibo.api.motan.util.MotanFrameworkUtil; +import io.jboot.utils.ConfigUtil; +import io.jboot.components.rpc.JbootrpcReferenceConfig; +import io.jboot.components.rpc.JbootrpcServiceConfig; +import io.jboot.components.rpc.RPCUtil; +import io.jboot.utils.StrUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/20 + */ +public class MotanUtil { + + private static Map protocolConfigMap = new ConcurrentHashMap<>(); + private static Map registryConfigMap = new ConcurrentHashMap<>(); + private static Map baseRefererConfigMap = new ConcurrentHashMap<>(); + private static Map baseServiceConfigMap = new ConcurrentHashMap<>(); + + + public static void initMotan() { + + //protocol 配置 + Map protocolConfigs = configs(ProtocolConfig.class, "jboot.rpc.motan.protocol"); + if (!protocolConfigs.isEmpty()) { + protocolConfigMap.putAll(protocolConfigs); + } else { + protocolConfigMap.put("default", MotanFrameworkUtil.getDefaultProtocolConfig()); + } + + //registry 配置 + Map registryConfigs = configs(RegistryConfig.class, "jboot.rpc.motan.registry"); + if (!registryConfigs.isEmpty()) { + registryConfigMap.putAll(registryConfigs); + } else { + registryConfigMap.put("default", MotanFrameworkUtil.getDefaultRegistryConfig()); + } + + //methodConfig 配置 + Map methodConfigs = configs(MethodConfig.class, "jboot.rpc.motan.method"); + + + //baseService 配置 + Map serviceConfigs = configs(BasicServiceInterfaceConfig.class, "jboot.rpc.motan.service"); + RPCUtil.setChildConfig(serviceConfigs, methodConfigs, "jboot.rpc.motan.service", "method"); + RPCUtil.setChildConfig(serviceConfigs, protocolConfigs, "jboot.rpc.motan.service", "protocol"); + RPCUtil.setChildConfig(serviceConfigs, registryConfigs, "jboot.rpc.motan.service", "registry"); + + + if (!serviceConfigs.isEmpty()) { + baseServiceConfigMap.putAll(serviceConfigs); + } + + //baseReferer 配置 + Map refererConfigs = configs(BasicRefererInterfaceConfig.class, "jboot.rpc.motan.referer"); + RPCUtil.setChildConfig(refererConfigs, methodConfigs, "jboot.rpc.motan.referer", "method"); + RPCUtil.setChildConfig(refererConfigs, protocolConfigs, "jboot.rpc.motan.referer", "protocol"); + RPCUtil.setChildConfig(refererConfigs, registryConfigs, "jboot.rpc.motan.referer", "registry"); + + if (!refererConfigs.isEmpty()) { + baseRefererConfigMap.putAll(refererConfigs); + } + + + } + + + public static RefererConfig toRefererConfig(JbootrpcReferenceConfig jbootrpcReferenceConfig) { + RefererConfig refererConfig = new RefererConfig(); + RPCUtil.copyDeclaredFields(jbootrpcReferenceConfig, refererConfig); + + + // reference consumer + if (jbootrpcReferenceConfig.getConsumer() != null) { + refererConfig.setBasicReferer(baseRefererConfigMap.get(jbootrpcReferenceConfig.getConsumer())); + } + // set default consumer + else { + for (BasicRefererInterfaceConfig baseConfig : baseRefererConfigMap.values()) { + if (baseConfig.isDefault() != null && baseConfig.isDefault()) { + refererConfig.setBasicReferer(baseConfig); + } + } + } + + + //referer protocol + if (StrUtil.isNotBlank(jbootrpcReferenceConfig.getProtocol())) { + List protocolConfigs = new ArrayList<>(); + Set protocolNames = StrUtil.splitToSetByComma(jbootrpcReferenceConfig.getProtocol()); + for (String protocalName : protocolNames) { + ProtocolConfig registryConfig = protocolConfigMap.get(protocalName); + if (registryConfig != null) { + protocolConfigs.add(registryConfig); + } + } + if (!protocolConfigs.isEmpty()) { + refererConfig.setProtocols(protocolConfigs); + } + } else { + refererConfig.setProtocols(toList(protocolConfigMap)); + } + + + //referer registry + if (StrUtil.isNotBlank(jbootrpcReferenceConfig.getRegistry())) { + List registryConfigs = new ArrayList<>(); + Set registryNames = StrUtil.splitToSetByComma(jbootrpcReferenceConfig.getRegistry()); + for (String registryName : registryNames) { + RegistryConfig registryConfig = registryConfigMap.get(registryName); + if (registryConfig != null) { + registryConfigs.add(registryConfig); + } + } + if (!registryConfigs.isEmpty()) { + refererConfig.setRegistries(registryConfigs); + } + } else { + refererConfig.setRegistries(toList(registryConfigMap)); + } + + + return refererConfig; + } + + + public static ServiceConfig toServiceConfig(JbootrpcServiceConfig jbootrpcServiceConfig) { + ServiceConfig serviceConfig = new ServiceConfig(); + RPCUtil.copyDeclaredFields(jbootrpcServiceConfig, serviceConfig); + + + // reference consumer + if (jbootrpcServiceConfig.getProvider() != null) { + serviceConfig.setBasicService(baseServiceConfigMap.get(jbootrpcServiceConfig.getProvider())); + } + // set default consumer + else { + for (BasicServiceInterfaceConfig baseConfig : baseServiceConfigMap.values()) { + if (baseConfig.isDefault() != null && baseConfig.isDefault()) { + serviceConfig.setBasicService(baseConfig); + } + } + } + + + //service protocol + if (StrUtil.isNotBlank(jbootrpcServiceConfig.getProtocol())) { + List protocolConfigs = new ArrayList<>(); + Set protocolNames = StrUtil.splitToSetByComma(jbootrpcServiceConfig.getProtocol()); + for (String protocalName : protocolNames) { + ProtocolConfig registryConfig = protocolConfigMap.get(protocalName); + if (registryConfig != null) { + protocolConfigs.add(registryConfig); + } + } + if (!protocolConfigs.isEmpty()) { + serviceConfig.setProtocols(protocolConfigs); + } + } else { + serviceConfig.setProtocols(toList(protocolConfigMap)); + } + + + //service registry + if (StrUtil.isNotBlank(jbootrpcServiceConfig.getRegistry())) { + List registryConfigs = new ArrayList<>(); + Set registryNames = StrUtil.splitToSetByComma(jbootrpcServiceConfig.getRegistry()); + for (String registryName : registryNames) { + RegistryConfig registryConfig = registryConfigMap.get(registryName); + if (registryConfig != null) { + registryConfigs.add(registryConfig); + } + } + if (!registryConfigs.isEmpty()) { + serviceConfig.setRegistries(registryConfigs); + } + } else { + serviceConfig.setRegistries(toList(registryConfigMap)); + } + + + return serviceConfig; + } + + + public static BasicRefererInterfaceConfig getBaseReferer(String name) { + return baseRefererConfigMap.get(name); + } + + + public static BasicServiceInterfaceConfig getBaseService(String name) { + return baseServiceConfigMap.get(name); + } + + private static Map configs(Class clazz, String prefix) { + Map ret = ConfigUtil.getConfigModels(clazz, prefix); + + if (ret.size() > 0 + && (clazz == BasicServiceInterfaceConfig.class || clazz == BasicRefererInterfaceConfig.class) + && !RPCUtil.isDefaultConfigExist(clazz, ret)) { + + for (Map.Entry entry : ret.entrySet()) { + if ("default".equals(entry.getKey())) { + if (entry.getValue() instanceof BasicServiceInterfaceConfig) { + ((BasicServiceInterfaceConfig) entry.getValue()).setDefault(true); + } else if (entry.getValue() instanceof BasicRefererInterfaceConfig) { + ((BasicRefererInterfaceConfig) entry.getValue()).setDefault(true); + } + } + } + } + return ret; + } + + private static List toList(Map map) { + List list = new ArrayList<>(map.size()); + for (Map.Entry entry : map.entrySet()) { + AbstractConfig config = (AbstractConfig) entry.getValue(); + config.setId(entry.getKey()); + list.add((T) config); + } + return list; + } +} diff --git a/src/main/java/io/jboot/components/rpc/motan/MotanrpcConfig.java b/src/main/java/io/jboot/components/rpc/motan/MotanrpcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..e3726cb1080971979e6af08c3a2c8c3f330a7e14 --- /dev/null +++ b/src/main/java/io/jboot/components/rpc/motan/MotanrpcConfig.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.rpc.motan; + +import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.utils.StrUtil; + +import java.util.Map; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/31 + * @see com.weibo.api.motan.config.AbstractServiceConfig + */ +@ConfigModel(prefix = "jboot.rpc.motan") +public class MotanrpcConfig { + + /** + * 一个service可以按多个protocol提供服务,不同protocol使用不同port 利用export来设置protocol和port,格式如下: + * protocol1:port1,protocol2:port2 + **/ + private String defaultExport; + + + /** + * 一般不用设置,由服务自己获取,但如果有多个ip,而只想用指定ip,则可以在此处指定 + */ + private String defaultHost; + + + /** + * motan 对每个 service 设置的 export + */ + private Map exports; + + /** + * 某个服务暴露自己的主机 + */ + private Map hosts; + + + public String getDefaultExport() { + return defaultExport; + } + + public void setDefaultExport(String defaultExport) { + this.defaultExport = defaultExport; + } + + public String getDefaultHost() { + return defaultHost; + } + + public void setDefaultHost(String defaultHost) { + this.defaultHost = defaultHost; + } + + public Map getExports() { + return exports; + } + + public void setExports(Map exports) { + this.exports = exports; + } + + public String getExport(String className) { + String export = exports == null || exports.isEmpty() ? null : exports.get(className); + return StrUtil.isNotBlank(export) ? export : defaultExport; + } + + public Map getHosts() { + return hosts; + } + + public void setHosts(Map hosts) { + this.hosts = hosts; + } + + public String getHost(String className) { + String host = hosts == null || hosts.isEmpty() ? null : hosts.get(className); + return StrUtil.isNotBlank(host) ? host : defaultHost; + } +} diff --git a/src/main/java/io/jboot/components/schedule/JbooScheduleConfig.java b/src/main/java/io/jboot/components/schedule/JbooScheduleConfig.java index b7b6c4e1c2b1bc0f631ff2c9306b0738a695adb0..28c1d8d01be06c1e0dcc4ff8d92b35360f7ebc80 100644 --- a/src/main/java/io/jboot/components/schedule/JbooScheduleConfig.java +++ b/src/main/java/io/jboot/components/schedule/JbooScheduleConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/schedule/JbootCron4jPlugin.java b/src/main/java/io/jboot/components/schedule/JbootCron4jPlugin.java index 7dcbe99eea6bee85a2ba924d582ac55138cd3711..9f311ca678fcd9b3dea7b99905fc3f5efbbece46 100644 --- a/src/main/java/io/jboot/components/schedule/JbootCron4jPlugin.java +++ b/src/main/java/io/jboot/components/schedule/JbootCron4jPlugin.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -92,7 +92,7 @@ public class JbootCron4jPlugin implements IPlugin { throw new IllegalArgumentException("Task 必须是 Runnable、ITask、ProcessTask 或者 Task 类型"); } - boolean taskDaemon = configProp.getBoolean(taskName + ".daemon", true); + boolean taskDaemon = configProp.getBoolean(taskName + ".daemon", false); boolean taskEnable = configProp.getBoolean(taskName + ".enable", true); taskInfoList.add(new JbootCron4jPlugin.TaskInfo(taskCron, taskObj, taskDaemon, taskEnable)); } @@ -108,7 +108,7 @@ public class JbootCron4jPlugin implements IPlugin { } public JbootCron4jPlugin addTask(String cron, Runnable task) { - return addTask(cron, task, true, true); + return addTask(cron, task, false, true); } public JbootCron4jPlugin addTask(String cron, ProcessTask processTask, boolean daemon, boolean enable) { @@ -121,7 +121,7 @@ public class JbootCron4jPlugin implements IPlugin { } public JbootCron4jPlugin addTask(String cron, ProcessTask processTask) { - return addTask(cron, processTask, true, true); + return addTask(cron, processTask, false, true); } public JbootCron4jPlugin addTask(String cron, Task task, boolean daemon, boolean enable) { @@ -134,7 +134,7 @@ public class JbootCron4jPlugin implements IPlugin { } public JbootCron4jPlugin addTask(String cron, Task task) { - return addTask(cron, task, true, true); + return addTask(cron, task, false, true); } @Override @@ -150,9 +150,8 @@ public class JbootCron4jPlugin implements IPlugin { @Override public boolean stop() { - for (JbootCron4jPlugin.TaskInfo taskInfo : taskInfoList) { - taskInfo.stop(); - } + taskInfoList.forEach(TaskInfo::stop); + taskInfoList.clear(); return true; } diff --git a/src/main/java/io/jboot/components/schedule/JbootDistributedRunnable.java b/src/main/java/io/jboot/components/schedule/JbootDistributedRunnable.java index 1ace88865bd5014526ea8b1da08929bee165cec6..d5d7a756a75c0373e9e00c766a3f88f3d4a6a42b 100644 --- a/src/main/java/io/jboot/components/schedule/JbootDistributedRunnable.java +++ b/src/main/java/io/jboot/components/schedule/JbootDistributedRunnable.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,135 +15,178 @@ */ package io.jboot.components.schedule; +import com.jfinal.kit.LogKit; import com.jfinal.log.Log; import io.jboot.Jboot; import io.jboot.support.redis.JbootRedis; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.QuietlyUtil; +import io.jboot.utils.StrUtil; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 * @Title: 分布式任务 * @Description: 在分布式应用中,处理分布式应用,基于redis。 - * + *

* 特点: * 1、简单,无需依赖数据库。 * 2、高可用,不存在单点故障 * 3、一致性,在集群环境中,只有一个任务在执行。 * 4、Failover,支持故障转移 - * @Package io.jboot.schedule */ public class JbootDistributedRunnable implements Runnable { private static final Log LOG = Log.getLog(JbootDistributedRunnable.class); private JbootRedis redis; - private int expire = 50 * 1000; // 单位毫秒 + + // 锁的过期时间,单位毫秒,默认 1 分钟, + // 因此,配置的定时任务,每次执行任务的时间间隔,必须大于 1 分钟以上 + private int expire = 60 * 1000; private String key; private Runnable runnable; - public JbootDistributedRunnable() { - - this.redis = Jboot.getRedis(); - this.key = "jbootRunnable:" + this.getClass().getName(); + public JbootDistributedRunnable(Runnable runnable, String key, int expireSeconds) { + this.runnable = runnable; - if (redis == null) { - LOG.warn("redis is null, " + - "can not use @EnableDistributedRunnable in your Class[" + this.getClass().getName() + "], " + - "or config redis info in jboot.properties"); + if (StrUtil.isNotBlank(key)) { + this.key = key; + } else { + this.key = "jboot-distributed-key:" + ClassUtil.getUsefulClass(runnable.getClass()).getName(); } - } - - public JbootDistributedRunnable(Runnable runnable) { - this.runnable = runnable; - this.key = "jbootRunnable:" + runnable.getClass().getName(); - this.redis = Jboot.getRedis(); - if (redis == null) { - LOG.warn("redis is null, " + - "can not use @EnableDistributedRunnable in your Class[" + runnable.getClass().getName() + "], " + - "or config redis info in jboot.properties"); + if (expireSeconds > 0) { + this.expire = expireSeconds * 1000; } - } - public JbootDistributedRunnable(Runnable runnable, int expire) { - this.expire = (expire - 1) * 1000; - this.runnable = runnable; - this.key = "jbootRunnable:" + runnable.getClass().getName(); this.redis = Jboot.getRedis(); + if (redis == null) { - LOG.warn("redis is null, " + - "can not use @EnableDistributedRunnable in your Class[" + runnable.getClass().getName() + "], " + - "or config redis info in jboot.properties"); + LOG.warn("Redis is null, Can not use @EnableDistributedRunnable in your class: " + + ClassUtil.getUsefulClass(runnable.getClass()).getName() + + ", Please config redis info in jboot.properties"); } + } @Override public void run() { - if (redis == null) { + // 当未配置 redis 的时候,默认使用本地任务的方式进行执行 + runnable.run(); return; } - Long result = null; + + Long setTimeMillis = System.currentTimeMillis(); + + //要设置的值 + String setValue = setTimeMillis + ":" + StrUtil.uuid(); + + + boolean locked = false; for (int i = 0; i < 5; i++) { - Long setTimeMillis = System.currentTimeMillis(); - result = redis.setnx(key, setTimeMillis); + //setnx: 只在键 key 不存在的情况下, 将键 key 的值设置为 value, 若键 key 已经存在, 则 SETNX 命令不做任何动作。 + //result: 设置成功,返回 1,设置失败,返回 0 + Long result = redis.setnx(key, setValue); - //error + //error 一般不会出现这种情况,除非是网络异常等原因 if (result == null) { - quietSleep(); + quietlySleep(); + continue; } - //setnx fail + + //setnx success 设置成功 + if (result == 1) { + String value = redis.get(key); + + //在分布式的场景下,可能自己设置成功后,又被其他节点删除重新设置的情况 + //需要判断是否是当前节点(或者线程)设置的 + if (setValue.equals(value)) { + locked = true; + break; + } + // 可能被其他节点删除,或重置了 + else { + quietlySleep(); + continue; + } + } + + //setnx fail,可能已经被其他 server 优先设置了,也有可能是 自己的server 在上一次任务里设置了 else if (result == 0) { - Long saveTimeMillis = redis.get(key); - if (saveTimeMillis == null) { + + String value = null; + + try { + value = redis.get(key); + } catch (Exception ex) { + LogKit.logNothing(ex); + } + + + //获取不到,一般不会出现这种情况,除非是网络异常等原因 + //或者是使用了已经存在的 key,但是此 key 已经有其他序列化方式的值导致异常 + if (value == null) { reset(); + quietlySleep(); + continue; } - long ttl = System.currentTimeMillis() - saveTimeMillis; - if (ttl > expire) { - //防止死锁 + + String[] split = value.split(":"); + + //被其他节点,或者手动操作 redis 的方式给设置了这个key值 + if (split.length != 2) { reset(); + continue; } - // 休息 2 秒钟,重新去抢,因为可能别的应用执行失败了 - quietSleep(); + //获取设置的时间 + long savedTimeMillis = 0; - } + try { + savedTimeMillis = Long.parseLong(split[0]); + } catch (NumberFormatException ex) { + LogKit.logNothing(ex); + } + + //redis 存储的内容有问题,可能是被手动设置 redis 的方式设置了这个 key 值 + if (savedTimeMillis == 0) { + reset(); + continue; + } - //set success - else if (result == 1) { - break; + if ((System.currentTimeMillis() - savedTimeMillis) > expire) { + //若设置锁的时间以及过期了 + //说明是上一次任务配置的,此时需要删除这个过期的 key,然后重新去抢 + reset(); + } + // 若锁没有过期,休息后重新去抢,因为抢走的线程可能会重新释放 + else { + quietlySleep(); + } } + } //抢了5次都抢不到,证明已经被别的应用抢走了 - if (result == null || result == 0) { + if (!locked) { return; } try { - - if (runnable != null) { - runnable.run(); - } else { - boolean runSuccess = execute(); - - //run()执行失败,让别的分布式应用APP去执行 - //如果run()执行的时间很长(超过30秒),那么别的分布式应用可能也抢不到了,只能等待下次轮休 - //作用:故障转移 - if (!runSuccess) { - reset(); - } - } + runnable.run(); } // 如果 run() 执行异常,让别的分布式应用APP去执行 + // 但如果 run() 执行的时间很长(超过30秒),而且失败了,那么别的分布式应用可能也抢不到了,只能等待下次任务 // 作用:故障转移 catch (Throwable ex) { LOG.error(ex.toString(), ex); @@ -153,14 +196,18 @@ public class JbootDistributedRunnable implements Runnable { /** - * 重置分布式的key + * 重置分布式的 key */ private void reset() { - redis.del(key); + try { + redis.del(key); + } catch (Exception ex) { + LogKit.logNothing(ex); + } } - public void quietSleep() { + public void quietlySleep() { int millis = 2000; if (this.expire <= 2000) { millis = 100; @@ -170,16 +217,7 @@ public class JbootDistributedRunnable implements Runnable { millis = 1000; } - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - //for override - public boolean execute() { - return true; + QuietlyUtil.sleepQuietly(millis); } diff --git a/src/main/java/io/jboot/components/schedule/JbootSafeRunnable.java b/src/main/java/io/jboot/components/schedule/JbootSafeRunnable.java new file mode 100644 index 0000000000000000000000000000000000000000..94270c8831f59ea8733303cc3b55717e90626a55 --- /dev/null +++ b/src/main/java/io/jboot/components/schedule/JbootSafeRunnable.java @@ -0,0 +1,28 @@ +package io.jboot.components.schedule; + +import com.jfinal.log.Log; + +/** + * 使用 try catch 包裹业务代码,防止业务现场抛出异常导致 ScheduledThreadPoolExecutor 终止调度 + * + * @author orangej + * @since 2021-9-26 + */ +public class JbootSafeRunnable implements Runnable { + private static final Log LOG = Log.getLog(JbootSafeRunnable.class); + + private Runnable job; + + public JbootSafeRunnable(Runnable job) { + this.job = job; + } + + @Override + public void run() { + try { + job.run(); + } catch (Throwable ex) { + LOG.error(ex.toString(), ex); + } + } +} diff --git a/src/main/java/io/jboot/components/schedule/JbootScheduleManager.java b/src/main/java/io/jboot/components/schedule/JbootScheduleManager.java index b8978e17d08c3cb5e473ba49b79d611566bef02e..d1d384f99d887c92d278be01cd60bd53fb6567fc 100644 --- a/src/main/java/io/jboot/components/schedule/JbootScheduleManager.java +++ b/src/main/java/io/jboot/components/schedule/JbootScheduleManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,21 +46,19 @@ public class JbootScheduleManager { private ScheduledThreadPoolExecutor fixedScheduler; private JbooScheduleConfig config; - private Map scheduleRunnableCache = new ConcurrentHashMap<>(); + private Map, Runnable> scheduleRunnableCache = new ConcurrentHashMap<>(); // add by lixin 08.08, 用于 remove fixedScheduler - private Map scheduleFutureCache = new ConcurrentHashMap<>(); + private Map, ScheduledFuture> scheduleFutureCache = new ConcurrentHashMap<>(); public JbootScheduleManager() { config = Jboot.config(JbooScheduleConfig.class); - fixedScheduler = new ScheduledThreadPoolExecutor(config.getPoolSize(),new NamedThreadFactory("jboot-scheduler")); + fixedScheduler = new ScheduledThreadPoolExecutor(config.getPoolSize(), new NamedThreadFactory("jboot-scheduler")); File cron4jProperties = new File(PathKit.getRootClassPath(), config.getCron4jFile()); cron4jPlugin = cron4jProperties.exists() ? new JbootCron4jPlugin(new Prop(config.getCron4jFile())) : new JbootCron4jPlugin(); - LOG.info("Init JbootScheduleManager"); - } @@ -81,21 +79,27 @@ public class JbootScheduleManager { private void initSchedules() { List> runnableClass = ClassScanner.scanSubClass(Runnable.class, true); - if (runnableClass != null) - for (Class rc : runnableClass) addSchedule(rc); + runnableClass.forEach(this::addSchedule); } + public void addSchedule(Class runnableClass) { FixedDelay fixedDelayJob = runnableClass.getAnnotation(FixedDelay.class); if (fixedDelayJob != null) { Runnable runnable = ClassUtil.newInstance(runnableClass); - Runnable executeRunnable = runnableClass.getAnnotation(EnableDistributedRunnable.class) == null + + EnableDistributedRunnable enableDistributedRunnable = runnableClass.getAnnotation(EnableDistributedRunnable.class); + Runnable executeRunnable = enableDistributedRunnable == null ? runnable - : new JbootDistributedRunnable(runnable, fixedDelayJob.period()); + : new JbootDistributedRunnable(runnable, AnnotationUtil.get(enableDistributedRunnable.redisKey()), enableDistributedRunnable.expireSeconds()); + + // ScheduledThreadPoolExecutor 线程池在遇到未捕获的异常时会终止调度 + // JbootSafeRunnable 可以捕捉业务代码异常,防止线程池意外终止调度 + executeRunnable = new JbootSafeRunnable(executeRunnable); try { scheduleRunnableCache.put(runnableClass, executeRunnable); // modified by lixin 08.08, 用于remove fixedScheduler - ScheduledFuture sf = fixedScheduler.scheduleWithFixedDelay(executeRunnable, fixedDelayJob.initialDelay(), fixedDelayJob.period(), TimeUnit.SECONDS); + ScheduledFuture sf = fixedScheduler.scheduleWithFixedDelay(executeRunnable, fixedDelayJob.initialDelay(), fixedDelayJob.period(), TimeUnit.SECONDS); scheduleFutureCache.put(runnableClass, sf); } catch (Exception e) { LOG.error(e.toString(), e); @@ -105,14 +109,19 @@ public class JbootScheduleManager { FixedRate fixedRateJob = runnableClass.getAnnotation(FixedRate.class); if (fixedRateJob != null) { Runnable runnable = ClassUtil.newInstance(runnableClass); - Runnable executeRunnable = runnableClass.getAnnotation(EnableDistributedRunnable.class) == null + + EnableDistributedRunnable enableDistributedRunnable = runnableClass.getAnnotation(EnableDistributedRunnable.class); + Runnable executeRunnable = enableDistributedRunnable == null ? runnable - : new JbootDistributedRunnable(runnable, fixedRateJob.period()); + : new JbootDistributedRunnable(runnable, AnnotationUtil.get(enableDistributedRunnable.redisKey()), enableDistributedRunnable.expireSeconds()); + + + executeRunnable = new JbootSafeRunnable(executeRunnable); try { scheduleRunnableCache.put(runnableClass, executeRunnable); - // modified by lixin 08.08, 用于remove fixedScheduler - ScheduledFuture sf = fixedScheduler.scheduleAtFixedRate(executeRunnable, fixedRateJob.initialDelay(), fixedRateJob.period(), TimeUnit.SECONDS); - scheduleFutureCache.put(runnableClass, sf); + // modified by lixin 08.08, 用于 remove fixedScheduler + ScheduledFuture future = fixedScheduler.scheduleAtFixedRate(executeRunnable, fixedRateJob.initialDelay(), fixedRateJob.period(), TimeUnit.SECONDS); + scheduleFutureCache.put(runnableClass, future); } catch (Exception e) { LOG.error(e.toString(), e); } @@ -123,9 +132,12 @@ public class JbootScheduleManager { if (cron != null) { String value = AnnotationUtil.get(cron.value()); Runnable runnable = ClassUtil.newInstance(runnableClass); - Runnable executeRunnable = runnableClass.getAnnotation(EnableDistributedRunnable.class) == null + + EnableDistributedRunnable enableDistributedRunnable = runnableClass.getAnnotation(EnableDistributedRunnable.class); + Runnable executeRunnable = enableDistributedRunnable == null ? runnable - : new JbootDistributedRunnable(runnable); + : new JbootDistributedRunnable(runnable, AnnotationUtil.get(enableDistributedRunnable.redisKey()), enableDistributedRunnable.expireSeconds()); + scheduleRunnableCache.put(runnableClass, executeRunnable); cron4jPlugin.addTask(value, executeRunnable, cron.daemon()); } @@ -140,14 +152,14 @@ public class JbootScheduleManager { scheduleRunnableCache.remove(removeClass); } - //add by lixin 08.08, 用于remove fixedScheduler - ScheduledFuture sf = scheduleFutureCache.get(removeClass); + //add by lixin 08.08, 用于 remove fixedScheduler + ScheduledFuture sf = scheduleFutureCache.remove(removeClass); if (sf != null) { sf.cancel(true); } } - public Map getScheduleRunnableCache() { + public Map, Runnable> getScheduleRunnableCache() { return scheduleRunnableCache; } @@ -159,5 +171,11 @@ public class JbootScheduleManager { return fixedScheduler; } + public JbooScheduleConfig getConfig() { + return config; + } + public Map, ScheduledFuture> getScheduleFutureCache() { + return scheduleFutureCache; + } } diff --git a/src/main/java/io/jboot/components/schedule/annotation/Cron.java b/src/main/java/io/jboot/components/schedule/annotation/Cron.java index 4b34691333c4935c9997891439474322de235eab..11b70cb42155c809978c305384b3263990c04031 100644 --- a/src/main/java/io/jboot/components/schedule/annotation/Cron.java +++ b/src/main/java/io/jboot/components/schedule/annotation/Cron.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/schedule/annotation/EnableDistributedRunnable.java b/src/main/java/io/jboot/components/schedule/annotation/EnableDistributedRunnable.java index 6c414615cd98c06c508ef11c37a3cddd74135ccd..596f51f0ce6f1d88df9d783da538887e5e1c2247 100644 --- a/src/main/java/io/jboot/components/schedule/annotation/EnableDistributedRunnable.java +++ b/src/main/java/io/jboot/components/schedule/annotation/EnableDistributedRunnable.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,4 +21,18 @@ import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface EnableDistributedRunnable { + + /** + * 分布式任务用的 redisKey,不配置默认使用 Runnable 实现类的类名 + * + * @return + */ + String redisKey() default ""; + + /** + * redisKey 持有时间,不配置默认为 1 分钟 + * + * @return + */ + int expireSeconds() default 0; } \ No newline at end of file diff --git a/src/main/java/io/jboot/components/schedule/annotation/FixedDelay.java b/src/main/java/io/jboot/components/schedule/annotation/FixedDelay.java index 00482f8763dad443152d420c34ac05903309ecdf..e7a8b1aa71a93e9cabf2538890a00074f12a4f49 100644 --- a/src/main/java/io/jboot/components/schedule/annotation/FixedDelay.java +++ b/src/main/java/io/jboot/components/schedule/annotation/FixedDelay.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/schedule/annotation/FixedRate.java b/src/main/java/io/jboot/components/schedule/annotation/FixedRate.java index b8f8044768403ebf93189b9dfe3eb736a478084d..92d1b63f133bdd1a755e42b28e977b7e3fa9310d 100644 --- a/src/main/java/io/jboot/components/schedule/annotation/FixedRate.java +++ b/src/main/java/io/jboot/components/schedule/annotation/FixedRate.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/components/serializer/FastjsonSerializer.java b/src/main/java/io/jboot/components/serializer/FastJsonSerializer.java similarity index 44% rename from src/main/java/io/jboot/components/serializer/FastjsonSerializer.java rename to src/main/java/io/jboot/components/serializer/FastJsonSerializer.java index 382064d5733d8096bb9774fb51fcdb0df7d5fbc1..4de321ecae69e96cd0bec6d1d1a9df5752ef8b9d 100644 --- a/src/main/java/io/jboot/components/serializer/FastjsonSerializer.java +++ b/src/main/java/io/jboot/components/serializer/FastJsonSerializer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,33 @@ package io.jboot.components.serializer; import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.parser.ParserConfig; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.util.IOUtils; import com.jfinal.log.Log; -public class FastjsonSerializer implements JbootSerializer { +public class FastJsonSerializer implements JbootSerializer { - private static final Log LOG = Log.getLog(FastjsonSerializer.class); + private static final Log LOG = Log.getLog(FastJsonSerializer.class); + + private final static ParserConfig autoTypeSupportConfig = new ParserConfig(); + static { + autoTypeSupportConfig.setAutoTypeSupport(true); + } @Override public byte[] serialize(Object obj) { if (obj == null) { return null; } - FastJsonCacheObject object = new FastJsonCacheObject(obj.getClass(), obj); - String string = JSON.toJSONString(object); - return string.getBytes(); + + return JSON.toJSONBytes(obj + , SerializerFeature.WriteClassName + , SerializerFeature.SkipTransientField + , SerializerFeature.IgnoreErrorGetter +// , SerializerFeature.IgnoreNonFieldGetter + ); } @Override @@ -39,46 +50,15 @@ public class FastjsonSerializer implements JbootSerializer { if (bytes == null || bytes.length == 0) { return null; } - String json = new String(bytes); - JSONObject jsonObject = JSON.parseObject(json); - Class clazz = null; + try { - clazz = Class.forName(jsonObject.getString("clazz")); - } catch (ClassNotFoundException e) { +// return JSON.parse(bytes, Feature.SupportAutoType); + return JSON.parse(new String(bytes, IOUtils.UTF8), autoTypeSupportConfig); + } catch (Exception e) { LOG.error(e.toString(), e); - return null; - } - return jsonObject.getObject("object", clazz); - } - - - public static class FastJsonCacheObject { - private Class clazz; - private Object object; - - public FastJsonCacheObject() { } - public FastJsonCacheObject(Class clazz, Object object) { - this.clazz = clazz; - this.object = object; - } - - public Class getClazz() { - return clazz; - } - - public void setClazz(Class clazz) { - this.clazz = clazz; - } - - public Object getObject() { - return object; - } - - public void setObject(Object object) { - this.object = object; - } + return null; } diff --git a/src/main/java/io/jboot/components/serializer/FstSerializer.java b/src/main/java/io/jboot/components/serializer/FstSerializer.java index 9e17018251045008708cd7c1033d905ddfb263c7..eb1bb8b3193dca5a1124a4b75fb0b4e405aac5e9 100644 --- a/src/main/java/io/jboot/components/serializer/FstSerializer.java +++ b/src/main/java/io/jboot/components/serializer/FstSerializer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,8 +40,8 @@ public class FstSerializer implements JbootSerializer { } try { return fst.asObject(bytes); - } catch (Throwable ex) { - LOG.error(ex.toString(), ex); + } catch (Exception ex) { + LOG.error("FstSerializer deserialize fail!", ex); } return null; } diff --git a/src/main/java/io/jboot/components/serializer/JbootSerializer.java b/src/main/java/io/jboot/components/serializer/JbootSerializer.java index 979c5f4832c6fa55d3a5bdfcdeedabfde7d30602..28011aca1f748f48a2b60444b71c961227ba482f 100644 --- a/src/main/java/io/jboot/components/serializer/JbootSerializer.java +++ b/src/main/java/io/jboot/components/serializer/JbootSerializer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package io.jboot.components.serializer; public interface JbootSerializer { - public byte[] serialize(Object obj); + byte[] serialize(Object obj); - public Object deserialize(byte[] bytes); + Object deserialize(byte[] bytes); } diff --git a/src/main/java/io/jboot/components/serializer/JbootSerializerConfig.java b/src/main/java/io/jboot/components/serializer/JbootSerializerConfig.java index c949eb3f2e82975d8014d9c5eeadd87b5840a8c7..040bf4488c2aac4ee834d55f4df1a1645f2d80b6 100644 --- a/src/main/java/io/jboot/components/serializer/JbootSerializerConfig.java +++ b/src/main/java/io/jboot/components/serializer/JbootSerializerConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,9 @@ */ package io.jboot.components.serializer; +import io.jboot.app.JdkUtil; import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.utils.StrUtil; @ConfigModel(prefix = "jboot.serializer") @@ -25,12 +27,21 @@ public class JbootSerializerConfig { public static final String FASTJSON = "fastjson"; public static final String KRYO = "kryo"; - public String type = FST; + public String type; + public String getType() { + if (StrUtil.isBlank(type)){ + type = initType(); + } return type; } + private String initType() { + return JdkUtil.isJdk11To19() ? FASTJSON : FST; + } + + public void setType(String type) { this.type = type; } diff --git a/src/main/java/io/jboot/components/serializer/JbootSerializerManager.java b/src/main/java/io/jboot/components/serializer/JbootSerializerManager.java index 0ffeeb4ba5ae4d78a91861e7e2f890744c086398..83e180909261c9609eea5b73065ec091e1183470 100644 --- a/src/main/java/io/jboot/components/serializer/JbootSerializerManager.java +++ b/src/main/java/io/jboot/components/serializer/JbootSerializerManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ package io.jboot.components.serializer; import io.jboot.Jboot; import io.jboot.core.spi.JbootSpiLoader; -import io.jboot.exception.JbootException; -import io.jboot.utils.ClassUtil; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.StrUtil; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -27,64 +27,54 @@ import java.util.concurrent.ConcurrentHashMap; public class JbootSerializerManager { - private static JbootSerializerManager me; + private static JbootSerializerManager me = new JbootSerializerManager(); private static Map serializerCaches = new ConcurrentHashMap<>(); public static JbootSerializerManager me() { - if (me == null) { - me = ClassUtil.singleton(JbootSerializerManager.class); - } return me; } public JbootSerializer getSerializer() { JbootSerializerConfig config = Jboot.config(JbootSerializerConfig.class); + if (StrUtil.isBlank(config.getType())) { + throw new JbootIllegalConfigException("can not get serializer config, please set jboot.serializer value to jboot.proerties"); + } return getSerializer(config.getType()); } - public JbootSerializer getSerializer(String serializerString) { - - JbootSerializer serializer = serializerCaches.get(serializerString); + public JbootSerializer getSerializer(String serializerName) { + JbootSerializer serializer = serializerCaches.get(serializerName); if (serializer == null) { - - serializer = buildSerializer(serializerString); - serializerCaches.put(serializerString, serializer); + synchronized (this) { + serializer = serializerCaches.get(serializerName); + if (serializer == null) { + serializer = buildSerializer(serializerName); + serializerCaches.put(serializerName, serializer); + } + } } return serializer; } - public JbootSerializer buildSerializer(String serializerString) { - - if (serializerString == null){ - throw new JbootException("can not get serializer config, please set jboot.serializer value to jboot.proerties"); + public JbootSerializer buildSerializer(String serializerName) { + if (serializerName == null) { + throw new NullPointerException("SerializerName must not be null"); } - /** - * 可能是某个类名 - */ - if (serializerString != null && serializerString.contains(".")) { - JbootSerializer serializer = ClassUtil.newInstance(serializerString); - if (serializer != null) { - return serializer; - } - } - - - switch (serializerString) { + switch (serializerName.toLowerCase()) { case JbootSerializerConfig.KRYO: return new KryoSerializer(); case JbootSerializerConfig.FST: return new FstSerializer(); case JbootSerializerConfig.FASTJSON: - return new FastjsonSerializer(); - + return new FastJsonSerializer(); default: - return JbootSpiLoader.load(JbootSerializer.class, serializerString); + return JbootSpiLoader.load(JbootSerializer.class, serializerName); } } diff --git a/src/main/java/io/jboot/components/serializer/KryoSerializer.java b/src/main/java/io/jboot/components/serializer/KryoSerializer.java index 57776a8cdf3620a19a4a65bfffe84f968d7474e3..81421496925383422ba97ab642d6b396b2d7addb 100644 --- a/src/main/java/io/jboot/components/serializer/KryoSerializer.java +++ b/src/main/java/io/jboot/components/serializer/KryoSerializer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import java.io.ByteArrayOutputStream; * @version V1.0 * @Title: Kryo 序列化 * @Description: 性能和 fst一样 - * @Package io.jboot.core.serializer */ public class KryoSerializer implements JbootSerializer { diff --git a/src/main/java/io/jboot/components/valid/ValidErrorRender.java b/src/main/java/io/jboot/components/valid/ValidErrorRender.java new file mode 100644 index 0000000000000000000000000000000000000000..ffea6e57f638604af73094b03917845b86220577 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/ValidErrorRender.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid; + +import com.jfinal.kit.JsonKit; +import com.jfinal.kit.Ret; +import com.jfinal.render.Render; +import com.jfinal.render.RenderException; +import io.jboot.utils.RequestUtil; + +/** + * 数据验证错误的渲染器,可以通过实现 JbootRenderFactory: {@link io.jboot.web.render.JbootRenderFactory} 来实现自定义渲染验证错误 + */ +public class ValidErrorRender extends Render { + + protected static final String htmlContentType = "text/html;charset=" + getEncoding(); + protected static final String jsonContentType = "application/json;charset=" + getEncoding(); + protected static final String html_header = "Invalid parameter" + + "

Invalid parameter

" + + "
"; + + protected static final String poweredBy = "
Powered by Jboot
"; + protected static final String html_footer = "
" + poweredBy + ""; + + protected int errorCode = ValidUtil.getErrorCode(); + protected final ValidException validException; + + public ValidErrorRender(ValidException validException) { + this.validException = validException; + } + + @Override + public void render() { + try { + if (RequestUtil.isJsonContentType(request) || RequestUtil.isAjaxRequest(request)) { + response.setStatus(200); + response.setContentType(jsonContentType); + response.getWriter().write(getErrorJson()); + } else { + response.setStatus(errorCode); + response.setContentType(htmlContentType); + response.getWriter().write(getErrorHtml()); + } + } catch (Exception ex) { + throw new RenderException(ex); + } + } + + public String getErrorHtml() { + StringBuilder html = new StringBuilder(html_header); + html.append(validException.getFormName() == null ? "" : (validException.getFormName() + ": ")); + html.append(validException.getMessage()).append("
"); + return html.append(html_footer).toString(); + } + + public String getErrorJson() { + Ret ret = Ret.fail(validException.getMessage()).set("errorCode", errorCode); + ret.set("throwable", validException.getClass().getName() + ": " + this.validException.getMessage()); + ret.set("errorMessage", validException.getReason()); + ret.set("formName", validException.getFormName()); + return JsonKit.toJson(ret); + } +} diff --git a/src/main/java/io/jboot/components/valid/ValidException.java b/src/main/java/io/jboot/components/valid/ValidException.java new file mode 100644 index 0000000000000000000000000000000000000000..9221f687ed59d3de3a3ef9f297fa78b9a8593c8f --- /dev/null +++ b/src/main/java/io/jboot/components/valid/ValidException.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid; + +public class ValidException extends RuntimeException { + + private String message; + private String reason; + private String formName; + + public ValidException() { + } + + + public ValidException(String message, String reason, String fieldName) { + this.message = message; + this.reason = reason; + this.formName = fieldName; + } + + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public String getFormName() { + return formName; + } + + public void setFormName(String formName) { + this.formName = formName; + } +} diff --git a/src/main/java/io/jboot/components/valid/ValidUtil.java b/src/main/java/io/jboot/components/valid/ValidUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..c69c20cdcd8bd32995c1daedbf79db9c4b3627f8 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/ValidUtil.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid; + +import com.jfinal.kit.Ret; +import io.jboot.components.valid.interceptor.SimpleContext; +import org.hibernate.validator.HibernateValidator; + +import javax.validation.*; +import java.util.Set; + +public class ValidUtil { + + /** + * ValidatorFactory + */ + private static ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) + .buildValidatorFactory(); + /** + * 验证器:用于数据验证 + */ + private static Validator validator = validatorFactory.getValidator(); + + /** + * 消息处理器: 用于校验消息处理 + */ + private static MessageInterpolator messageInterpolator = validatorFactory.getMessageInterpolator(); + + private static int errorCode = 400; + + + public static Validator getValidator() { + return validator; + } + + public static void setValidator(Validator validator) { + ValidUtil.validator = validator; + } + + + public static int getErrorCode() { + return errorCode; + } + + public static void setErrorCode(int errorCode) { + ValidUtil.errorCode = errorCode; + } + + public static Set> validate(Object object) { + return validator.validate(object); + } + + + public static void throwValidException(String fieldName, String message, String reason) { + throwValidException(fieldName, message, null, reason); + } + + + public static void throwValidException(String fieldName, String message, Ret paras, String reason) { + if (isParaMessage(message)) { + message = fieldName + " " + messageInterpolator.interpolate(message, new SimpleContext(paras)); + } + + throw new ValidException(message, reason, fieldName); + } + + + private static boolean isParaMessage(String message) { + return message != null && message.startsWith("{") && message.endsWith("}"); + } +} diff --git a/src/main/java/io/jboot/components/valid/interceptor/DecimalMaxInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/DecimalMaxInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..63b5dfc82ea3919165ac91c8153157a24a9098d3 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/DecimalMaxInterceptor.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.kit.Ret; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.DecimalMax; +import java.lang.reflect.Parameter; +import java.math.BigDecimal; +import java.math.BigInteger; + +public class DecimalMaxInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + + for (int index = 0; index < parameters.length; index++) { + DecimalMax decimalMax = parameters[index].getAnnotation(DecimalMax.class); + if (decimalMax == null) { + continue; + } + + Object validObject = inv.getArg(index); + if (validObject != null && !matches(decimalMax, validObject)) { + String reason = parameters[index].getName() + " max value is " + decimalMax.value() + + ", but current value is " + validObject + " at method: " + ClassUtil.buildMethodString(inv.getMethod()); + Ret paras = Ret.by("value", decimalMax.value()); + ValidUtil.throwValidException(parameters[index].getName(), decimalMax.message(), paras, reason); + } + } + + inv.invoke(); + } + + private boolean matches(DecimalMax decimalMax, Object validObject) { + if (validObject instanceof BigInteger) { + return ((BigInteger) validObject).compareTo(new BigInteger(decimalMax.value())) <= 0; + } else if (validObject instanceof BigDecimal) { + return ((BigDecimal) validObject).compareTo(new BigDecimal(decimalMax.value())) <= 0; + } else if (validObject instanceof CharSequence) { + return (new BigDecimal(validObject.toString())).compareTo(new BigDecimal(decimalMax.value())) <= 0; + } else if (validObject instanceof Float) { + return ((Float) validObject) <= Float.parseFloat(decimalMax.value()); + } else if (validObject instanceof Double) { + return ((Double) validObject) <= Double.parseDouble(decimalMax.value()); + } else if (validObject instanceof Number) { + return ((Number) validObject).longValue() <= Long.parseLong(decimalMax.value()); + } + return false; + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/DecimalMinInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/DecimalMinInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..789ba443047a1206cb81d3ff559180c09a28d87a --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/DecimalMinInterceptor.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.kit.Ret; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.DecimalMin; +import java.lang.reflect.Parameter; +import java.math.BigDecimal; +import java.math.BigInteger; + +public class DecimalMinInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + + for (int index = 0; index < parameters.length; index++) { + DecimalMin decimalMin = parameters[index].getAnnotation(DecimalMin.class); + if (decimalMin == null) { + continue; + } + Object validObject = inv.getArg(index); + if (validObject != null && !matches(decimalMin, validObject)) { + String reason = parameters[index].getName() + " min value is " + decimalMin.value() + ", but current value is " + validObject + " at method: " + ClassUtil.buildMethodString(inv.getMethod()); + Ret paras = Ret.by("value", decimalMin.value()); + ValidUtil.throwValidException(parameters[index].getName(), decimalMin.message(), paras, reason); + } + } + + inv.invoke(); + } + + + private boolean matches(DecimalMin decimalMax, Object validObject) { + if (validObject instanceof BigInteger) { + return ((BigInteger) validObject).compareTo(new BigInteger(decimalMax.value())) >= 0; + } else if (validObject instanceof BigDecimal) { + return ((BigDecimal) validObject).compareTo(new BigDecimal(decimalMax.value())) >= 0; + } else if (validObject instanceof CharSequence) { + return (new BigDecimal(validObject.toString())).compareTo(new BigDecimal(decimalMax.value())) >= 0; + } else if (validObject instanceof Float) { + return ((Float) validObject) >= Float.parseFloat(decimalMax.value()); + } else if (validObject instanceof Double) { + return ((Double) validObject) >= Double.parseDouble(decimalMax.value()); + } else if (validObject instanceof Number) { + return ((Number) validObject).longValue() >= Long.parseLong(decimalMax.value()); + } + return false; + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/DigitsInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/DigitsInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..40f307d580a4eccce44baf2b0cb9ac1331f41129 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/DigitsInterceptor.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.kit.Ret; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.Digits; +import java.lang.reflect.Parameter; + +public class DigitsInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + Digits digits = parameters[index].getAnnotation(Digits.class); + if (digits == null) { + continue; + } + + Object validObject = inv.getArg(index); + if (validObject != null && !matchesDigits(digits, validObject)) { + String reason = parameters[index].getName() + " not matches @Digits at method: " + ClassUtil.buildMethodString(inv.getMethod()); + Ret paras = Ret.by("integer", digits.integer()).set("fraction", digits.fraction()); + ValidUtil.throwValidException(parameters[index].getName(), digits.message(), paras, reason); + } + } + + inv.invoke(); + } + + private boolean matchesDigits(Digits digits, Object validObject) { + String validString = validObject.toString(); + String[] valids = validString.split("\\."); + + int integer = removeStartZero(valids[0]).length(); + int fraction = valids[0].length() == 1 ? 0 : removeEndZero(valids[1]).length(); + + return integer <= digits.integer() && fraction <= digits.fraction(); + } + + private String removeStartZero(String string) { + while (string.startsWith("0")) { + string = string.substring(1); + } + return string; + } + + + private String removeEndZero(String string) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + return string; + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/EmailInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/EmailInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..37611a71f43556b23c9a8038d14dcc592adeff5b --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/EmailInterceptor.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; +import io.jboot.web.validate.Regex; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Pattern; +import java.lang.reflect.Parameter; + +public class EmailInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + Email email = parameters[index].getAnnotation(Email.class); + if (email == null) { + continue; + } + + Object validObject = inv.getArg(index); + if (validObject == null || !matches(email, validObject.toString())) { + String reason = parameters[index].getName() + " is not email at method: " + ClassUtil.buildMethodString(inv.getMethod()); + ValidUtil.throwValidException(parameters[index].getName(), email.message(), reason); + } + } + + + inv.invoke(); + } + + private static boolean matches(Email email, String value) { + Pattern.Flag[] flags = email.flags(); + String regexp = ".*".equals(email.regexp()) ? Regex.EMAIL : email.regexp(); + if (flags.length == 0) { + return value.matches(regexp); + } + + int intFlag = 0; + for (Pattern.Flag flag : flags) { + intFlag = intFlag | flag.getValue(); + } + + java.util.regex.Pattern p = java.util.regex.Pattern.compile(regexp, intFlag); + return p.matcher(value).matches(); + + } + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/MaxInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/MaxInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..942648b49a78e896769001799a978bd99ac9e766 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/MaxInterceptor.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.kit.Ret; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.Max; +import java.lang.reflect.Parameter; + +public class MaxInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + + for (int index = 0; index < parameters.length; index++) { + Max max = parameters[index].getAnnotation(Max.class); + if (max == null) { + continue; + } + + Object validObject = inv.getArg(index); + if (validObject == null) { + continue; + } + + long objectLen = Util.getObjectLen(validObject); + + if (max.value() < objectLen) { + String reason = parameters[index].getName() + " max value is " + max.value() + ", but current value is " + validObject + " at method: " + ClassUtil.buildMethodString(inv.getMethod()); + Ret paras = Ret.by("value", max.value()); + ValidUtil.throwValidException(parameters[index].getName(), max.message(), paras, reason); + } + } + + inv.invoke(); + } + + + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/MinInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/MinInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..ff60f7eb4d1760aae8cc814aee28c589e51df7b6 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/MinInterceptor.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.kit.Ret; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.Min; +import java.lang.reflect.Parameter; + +public class MinInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + + for (int index = 0; index < parameters.length; index++) { + Min min = parameters[index].getAnnotation(Min.class); + if (min == null) { + continue; + } + + Object validObject = inv.getArg(index); + if (validObject == null) { + continue; + } + + long objectLen = Util.getObjectLen(validObject); + + if (min.value() > objectLen) { + String reason = parameters[index].getName() + " min value is " + min.value() + ", but current value is " + validObject + " at method: " + ClassUtil.buildMethodString(inv.getMethod()); + Ret paras = Ret.by("value", min.value()); + ValidUtil.throwValidException(parameters[index].getName(), min.message(), paras, reason); + } + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/NegativeInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/NegativeInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..1069aa0eefaa4adde0e747059193c0b32f35af53 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/NegativeInterceptor.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.Negative; +import java.lang.reflect.Parameter; + +public class NegativeInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + Negative negative = parameters[index].getAnnotation(Negative.class); + if (negative == null) { + continue; + } + + Object validObject = inv.getArg(index); + + if (!(validObject instanceof Number) || ((Number) validObject).longValue() >= 0) { + String reason = parameters[index].getName() + " is null or not negative at method: " + ClassUtil.buildMethodString(inv.getMethod()); + ValidUtil.throwValidException(parameters[index].getName(), negative.message(), reason); + } + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/NegativeOrZeroInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/NegativeOrZeroInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..9a2d88d85968021ecc80109215d492a62466ffae --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/NegativeOrZeroInterceptor.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.NegativeOrZero; +import java.lang.reflect.Parameter; + +public class NegativeOrZeroInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + NegativeOrZero negativeOrZero = parameters[index].getAnnotation(NegativeOrZero.class); + if (negativeOrZero == null) { + continue; + } + + Object validObject = inv.getArg(index); + + if (!(validObject instanceof Number) || ((Number) validObject).longValue() > 0) { + String reason = parameters[index].getName() + " is null or greater than 0 at method: " + ClassUtil.buildMethodString(inv.getMethod()); + ValidUtil.throwValidException(parameters[index].getName(), negativeOrZero.message(), reason); + } + + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/NotBlankInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/NotBlankInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..b44c78fd56d553d6682ea6ebfbb606cfbaad1906 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/NotBlankInterceptor.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.StrUtil; + +import javax.validation.constraints.NotBlank; +import java.lang.reflect.Parameter; + +public class NotBlankInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + NotBlank notBlank = parameters[index].getAnnotation(NotBlank.class); + if (notBlank == null) { + continue; + } + Object validObject = inv.getArg(index); + + if (validObject == null || (validObject instanceof String && StrUtil.isBlank((String) validObject))) { + String msg = parameters[index].getName() + " is blank at method: " + ClassUtil.buildMethodString(inv.getMethod()); + ValidUtil.throwValidException(parameters[index].getName(), notBlank.message(), msg); + } + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/NotEmptyInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/NotEmptyInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..3ea515fd2e6244fef5f0f62ac124e53239f5695d --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/NotEmptyInterceptor.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.NotEmpty; +import java.lang.reflect.Parameter; + +public class NotEmptyInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + NotEmpty notEmpty = parameters[index].getAnnotation(NotEmpty.class); + if (notEmpty == null) { + continue; + } + Object validObject = inv.getArg(index); + if (validObject == null || Util.getObjectLen(validObject) <= 0) { + String reason = parameters[index].getName() + " is null or empty at method: " + ClassUtil.buildMethodString(inv.getMethod()); + ValidUtil.throwValidException(parameters[index].getName(), notEmpty.message(), reason); + } + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/NotNullInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/NotNullInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..60aba7b6515ac782b467d35cfa8c88769b5824a7 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/NotNullInterceptor.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.NotNull; +import java.lang.reflect.Parameter; + +public class NotNullInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + + for (int index = 0; index < parameters.length; index++) { + NotNull notNull = parameters[index].getAnnotation(NotNull.class); + if (notNull != null && inv.getArg(index) == null) { + String reason = parameters[index].getName() + " is null at method: " + ClassUtil.buildMethodString(inv.getMethod()); + ValidUtil.throwValidException(parameters[index].getName(), notNull.message(), reason); + } + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/PatternInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/PatternInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..fe9e1d6e7bf2df82615d554feb9e798259782dc6 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/PatternInterceptor.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.kit.Ret; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.Pattern; +import java.lang.reflect.Parameter; + +public class PatternInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + Pattern pattern = parameters[index].getAnnotation(Pattern.class); + if (pattern == null) { + continue; + } + + Object validObject = inv.getArg(index); + if (validObject == null || !matches(pattern, validObject.toString())) { + String reason = parameters[index].getName() + " is null or not matches the regex at method: " + ClassUtil.buildMethodString(inv.getMethod()); + Ret paras = Ret.by("regexp", pattern.regexp()); + ValidUtil.throwValidException(parameters[index].getName(), pattern.message(), paras, reason); + } + } + + + inv.invoke(); + } + + private static boolean matches(Pattern pattern, String value) { + Pattern.Flag[] flags = pattern.flags(); + if (flags.length == 0) { + return value.matches(pattern.regexp()); + } + + int intFlag = 0; + for (Pattern.Flag flag : flags) { + intFlag = intFlag | flag.getValue(); + } + + java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern.regexp(), intFlag); + return p.matcher(value).matches(); + + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/PositiveInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/PositiveInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..11eb8ff63c0d57a47c4a5c81209651b811ac6c84 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/PositiveInterceptor.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.Positive; +import java.lang.reflect.Parameter; + +public class PositiveInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + for (int index = 0; index < parameters.length; index++) { + Positive positive = parameters[index].getAnnotation(Positive.class); + if (positive == null) { + continue; + } + + Object validObject = inv.getArg(index); + if (!(validObject instanceof Number) || ((Number) validObject).longValue() <= 0) { + String reason = parameters[index].getName() + " is null or not positive at method: " + ClassUtil.buildMethodString(inv.getMethod()); + ValidUtil.throwValidException(parameters[index].getName(), positive.message(), reason); + } + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/PositiveOrZeroInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/PositiveOrZeroInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..6d7c2ff5ecad90eaa31f3c7b249f6ebb6018f465 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/PositiveOrZeroInterceptor.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.constraints.PositiveOrZero; +import java.lang.reflect.Parameter; + +public class PositiveOrZeroInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + + for (int index = 0; index < parameters.length; index++) { + PositiveOrZero positiveOrZero = parameters[index].getAnnotation(PositiveOrZero.class); + if (positiveOrZero == null) { + continue; + } + Object validObject = inv.getArg(index); + if (!(validObject instanceof Number) || ((Number) validObject).longValue() < 0) { + String reason = parameters[index].getName() + " is null or less than 0 at method: " + ClassUtil.buildMethodString(inv.getMethod()); + ValidUtil.throwValidException(parameters[index].getName(), positiveOrZero.message(), reason); + } + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/SimpleContext.java b/src/main/java/io/jboot/components/valid/interceptor/SimpleContext.java new file mode 100644 index 0000000000000000000000000000000000000000..50ae5614f52c3185863a5a40ddb539ea72c62da0 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/SimpleContext.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.kit.Ret; + +import javax.validation.ConstraintTarget; +import javax.validation.ConstraintValidator; +import javax.validation.MessageInterpolator; +import javax.validation.Payload; +import javax.validation.metadata.ConstraintDescriptor; +import javax.validation.metadata.ValidateUnwrappedValue; +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class SimpleContext implements MessageInterpolator.Context{ + + private Map attrs = new HashMap(); + private ConstraintDescriptor constraintDescriptor = new ConstraintDescriptor() { + @Override + public Annotation getAnnotation() { + return null; + } + + @Override + public String getMessageTemplate() { + return null; + } + + @Override + public Set> getGroups() { + return null; + } + + @Override + public Set> getPayload() { + return null; + } + + @Override + public ConstraintTarget getValidationAppliesTo() { + return null; + } + + @Override + public List> getConstraintValidatorClasses() { + return null; + } + + @Override + public Map getAttributes() { + return attrs; + } + + @Override + public Set> getComposingConstraints() { + return null; + } + + @Override + public boolean isReportAsSingleViolation() { + return false; + } + + @Override + public ValidateUnwrappedValue getValueUnwrapping() { + return null; + } + + @Override + public Object unwrap(Class type) { + return null; + } + }; + + public SimpleContext() { + } + + public SimpleContext(Ret attrs) { + if (attrs != null){ + this.attrs.putAll(attrs); + } + } + + + @Override + public ConstraintDescriptor getConstraintDescriptor() { + return constraintDescriptor; + } + + @Override + public Object getValidatedValue() { + return null; + } + + @Override + public T unwrap(Class type) { + return null; + } + + +} diff --git a/src/main/java/io/jboot/components/valid/interceptor/SizeInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/SizeInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..64592764c19832a91049c177576cdea2c97f4758 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/SizeInterceptor.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.kit.Ret; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.StrUtil; + +import javax.validation.constraints.Size; +import java.lang.reflect.Parameter; + + +public class SizeInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + Parameter[] parameters = inv.getMethod().getParameters(); + + for (int index = 0; index < parameters.length; index++) { + Size size = parameters[index].getAnnotation(Size.class); + if (size == null) { + continue; + } + + Object validObject = inv.getArg(index); + + //不指定 @Size(min=xxx),值配置了 max,则跳过空数据的内容 + if (size.min() == 0 && (validObject == null || (validObject instanceof String && StrUtil.isBlank((String) validObject)))) { + continue; + } + + if (validObject == null) { + String reason = parameters[index].getName() + " need size is " + size.min() + " ~ " + size.max() + + ", but current value is null at method: " + ClassUtil.buildMethodString(inv.getMethod()); + Ret paras = Ret.by("max", size.max()).set("min", size.min()); + ValidUtil.throwValidException(parameters[index].getName(), size.message(), paras, reason); + return; + } + + long len = Util.getObjectLen(validObject); + + if (len < size.min() || len > size.max()) { + String reason = parameters[index].getName() + " need size is " + size.min() + " ~ " + size.max() + + ", but current value size (or length) is " + len + " at method: " + ClassUtil.buildMethodString(inv.getMethod()); + Ret paras = Ret.by("max", size.max()).set("min", size.min()); + ValidUtil.throwValidException(parameters[index].getName(), size.message(), paras, reason); + } + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/Util.java b/src/main/java/io/jboot/components/valid/interceptor/Util.java new file mode 100644 index 0000000000000000000000000000000000000000..281c3430cd3656cc9620bc1bff8ea2a409b43c9f --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/Util.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Map; + +class Util { + + public static long getObjectLen(Object validObject) { + if (validObject == null) { + return 0; + } + if (validObject instanceof Number) { + return ((Number) validObject).longValue(); + } + if (validObject instanceof CharSequence) { + return ((CharSequence) validObject).length(); + } + if (validObject instanceof Map) { + return ((Map) validObject).size(); + } + if (validObject instanceof Collection) { + return ((Collection) validObject).size(); + } + if (validObject.getClass().isArray()) { + return Array.getLength(validObject); + } + throw new IllegalArgumentException("Can not get object length for class: " + validObject.getClass().getName()); + } + +} diff --git a/src/main/java/io/jboot/components/valid/interceptor/ValidInterceptor.java b/src/main/java/io/jboot/components/valid/interceptor/ValidInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..a52467b8035fdc9d909c69de31ae5a8cdda4b175 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/ValidInterceptor.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.components.valid.ValidUtil; +import io.jboot.utils.ClassUtil; + +import javax.validation.ConstraintViolation; +import javax.validation.Valid; +import java.lang.reflect.Parameter; +import java.util.Set; + +public class ValidInterceptor implements Interceptor { + + + @Override + public void intercept(Invocation inv) { + + Parameter[] parameters = inv.getMethod().getParameters(); + + for (int index = 0; index < parameters.length; index++) { + if (parameters[index].getAnnotation(Valid.class) != null) { + Object validObject = inv.getArg(index); + if (validObject == null) { + continue; + } + Set> constraintViolations = ValidUtil.validate(validObject); + if (constraintViolations != null && constraintViolations.size() > 0) { + StringBuilder msg = new StringBuilder(); + for (ConstraintViolation cv : constraintViolations) { + msg.append(cv.getRootBeanClass().getName()) + .append(".") + .append(cv.getPropertyPath()) + .append(cv.getMessage()); + } + String reason = parameters[index].getName() + " is valid failed at method: " + ClassUtil.buildMethodString(inv.getMethod()); + ValidUtil.throwValidException(parameters[index].getName(), msg.toString(), reason); + + } + } + } + + inv.invoke(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/components/valid/interceptor/ValidInterceptorBuilder.java b/src/main/java/io/jboot/components/valid/interceptor/ValidInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..0dd81c98abc7153936a61745e748df082b660ef2 --- /dev/null +++ b/src/main/java/io/jboot/components/valid/interceptor/ValidInterceptorBuilder.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.components.valid.interceptor; + +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.core.weight.Weight; + +import javax.validation.Valid; +import javax.validation.constraints.*; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +@AutoLoad +@Weight(100) +public class ValidInterceptorBuilder implements InterceptorBuilder { + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + Parameter[] parameters = method.getParameters(); + if (parameters != null && parameters.length > 0) { + for (Parameter p : parameters) { + + if (p.getAnnotation(DecimalMax.class) != null) { + interceptors.addIfNotExist(DecimalMaxInterceptor.class); + } + + if (p.getAnnotation(DecimalMin.class) != null) { + interceptors.addIfNotExist(DecimalMinInterceptor.class); + } + + if (p.getAnnotation(Digits.class) != null) { + interceptors.addIfNotExist(DigitsInterceptor.class); + } + + if (p.getAnnotation(Email.class) != null) { + interceptors.addIfNotExist(EmailInterceptor.class); + } + + if (p.getAnnotation(Max.class) != null) { + interceptors.addIfNotExist(MaxInterceptor.class); + } + + if (p.getAnnotation(Min.class) != null) { + interceptors.addIfNotExist(MinInterceptor.class); + } + + if (p.getAnnotation(Negative.class) != null) { + interceptors.addIfNotExist(NegativeInterceptor.class); + } + + if (p.getAnnotation(NegativeOrZero.class) != null) { + interceptors.addIfNotExist(NegativeOrZeroInterceptor.class); + } + + if (p.getAnnotation(NotBlank.class) != null) { + interceptors.addIfNotExist(NotBlankInterceptor.class); + } + + if (p.getAnnotation(NotEmpty.class) != null) { + interceptors.addIfNotExist(NotEmptyInterceptor.class); + } + + if (p.getAnnotation(NotNull.class) != null) { + interceptors.addIfNotExist(NotNullInterceptor.class); + } + + if (p.getAnnotation(Pattern.class) != null) { + interceptors.addIfNotExist(PatternInterceptor.class); + } + + if (p.getAnnotation(Positive.class) != null) { + interceptors.addIfNotExist(PositiveInterceptor.class); + } + + if (p.getAnnotation(PositiveOrZero.class) != null) { + interceptors.addIfNotExist(PositiveOrZeroInterceptor.class); + } + + if (p.getAnnotation(Size.class) != null) { + interceptors.addIfNotExist(SizeInterceptor.class); + } + + if (p.getAnnotation(Valid.class) != null) { + interceptors.addIfNotExist(ValidInterceptor.class); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/core/JbootCoreConfig.java b/src/main/java/io/jboot/core/JbootCoreConfig.java index 93b1e52a3e4540022369572de10f33e2c6462a0b..14e113a443addbf1f23841c1a2e2ebb66c8710dc 100644 --- a/src/main/java/io/jboot/core/JbootCoreConfig.java +++ b/src/main/java/io/jboot/core/JbootCoreConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,111 +19,160 @@ import com.jfinal.aop.Aop; import com.jfinal.aop.AopManager; import com.jfinal.config.*; import com.jfinal.core.Controller; -import com.jfinal.json.JsonManager; -import com.jfinal.kit.LogKit; +import com.jfinal.core.Path; +import com.jfinal.core.converter.TypeConverter; +import com.jfinal.kit.PathKit; +import com.jfinal.kit.PropKit; import com.jfinal.plugin.activerecord.ActiveRecordPlugin; +import com.jfinal.template.Directive; import com.jfinal.template.Engine; -import com.jfinal.weixin.sdk.api.ApiConfig; -import com.jfinal.weixin.sdk.api.ApiConfigKit; import io.jboot.Jboot; import io.jboot.aop.JbootAopFactory; -import io.jboot.aop.JbootAopInterceptor; import io.jboot.aop.jfinal.JfinalHandlers; import io.jboot.aop.jfinal.JfinalPlugins; -import io.jboot.app.config.support.apollo.ApolloConfigManager; -import io.jboot.app.config.support.apollo.ApolloServerConfig; -import io.jboot.app.config.support.nacos.NacosConfigManager; +import io.jboot.app.ApplicationUtil; +import io.jboot.components.cache.support.JbootCaptchaCache; +import io.jboot.components.cache.support.JbootTokenCache; +import io.jboot.components.gateway.JbootGatewayHandler; +import io.jboot.components.gateway.JbootGatewayManager; import io.jboot.components.limiter.LimiterManager; +import io.jboot.components.mq.JbootmqManager; import io.jboot.components.rpc.JbootrpcManager; import io.jboot.components.schedule.JbootScheduleManager; import io.jboot.core.listener.JbootAppListenerManager; -import io.jboot.core.log.Slf4jLogFactory; +import io.jboot.core.log.JbootLogFactory; import io.jboot.db.ArpManager; +import io.jboot.support.metric.JbootMetricConfig; +import io.jboot.support.metric.MetricServletHandler; +import io.jboot.support.metric.request.JbootRequestMetricHandler; import io.jboot.support.seata.JbootSeataManager; -import io.jboot.support.sentinel.SentinelManager; +import io.jboot.support.sentinel.JbootSentinelManager; +import io.jboot.support.sentinel.SentinelConfig; +import io.jboot.support.sentinel.SentinelHandler; import io.jboot.support.shiro.JbootShiroManager; import io.jboot.support.swagger.JbootSwaggerConfig; import io.jboot.support.swagger.JbootSwaggerController; import io.jboot.support.swagger.JbootSwaggerManager; import io.jboot.utils.*; -import io.jboot.web.JbootJson; +import io.jboot.web.JbootActionMapping; +import io.jboot.web.JbootWebConfig; +import io.jboot.web.PathVariableActionMapping; +import io.jboot.web.attachment.AttachmentHandler; +import io.jboot.web.attachment.LocalAttachmentContainerConfig; import io.jboot.web.controller.JbootControllerManager; +import io.jboot.web.controller.annotation.GetMapping; +import io.jboot.web.controller.annotation.PostMapping; import io.jboot.web.controller.annotation.RequestMapping; -import io.jboot.web.directive.annotation.JFinalDirective; -import io.jboot.web.directive.annotation.JFinalSharedMethod; -import io.jboot.web.directive.annotation.JFinalSharedObject; -import io.jboot.web.directive.annotation.JFinalSharedStaticMethod; -import io.jboot.web.fixedinterceptor.FixedInterceptors; +import io.jboot.web.converter.ArrayConverters; +import io.jboot.web.converter.TypeConverterFunc; +import io.jboot.web.directive.JbootOutputDirectiveFactory; +import io.jboot.web.directive.SharedEnumObject; +import io.jboot.web.directive.annotation.*; import io.jboot.web.handler.JbootActionHandler; -import io.jboot.web.handler.JbootFilterHandler; import io.jboot.web.handler.JbootHandler; +import io.jboot.web.handler.PathVariableActionHandler; +import io.jboot.web.json.JbootJson; import io.jboot.web.render.JbootRenderFactory; -import io.jboot.wechat.JbootAccessTokenCache; -import io.jboot.wechat.JbootWechatConfig; +import io.jboot.web.xss.XSSHandler; +import io.jboot.wechat.WechatSupport; -import java.sql.Driver; -import java.sql.DriverManager; +import java.io.File; import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; +import java.util.Properties; public class JbootCoreConfig extends JFinalConfig { private List routeList = new ArrayList<>(); + public JbootCoreConfig() { initSystemProperties(); - ApolloConfigManager.me().init(); - NacosConfigManager.me().init(); - + // 自动为 Interceptor 和 Controller 等添加依赖注入 AopManager.me().setInjectDependency(true); AopManager.me().setAopFactory(JbootAopFactory.me()); + Aop.inject(this); + initWebRootPath(); + JbootAppListenerManager.me().onInit(); } /** - * 设置必要的系统参数: - * 有些组件,比如 apollo、sentinel 等配置需要通过 System Properites来进行配置的 + * apollo、sentinel 等配置需要通过 System Properites 来进行配置的 + * 而 System Properties 的配置需要在启动的时候同 java -D 添加配置,极为不方便 + * 此时,可以添加在 jboot-system.properties 里添加,来代替 java -D 的情况 */ private void initSystemProperties() { - - //apollo 配置 - ApolloServerConfig apolloConfig = Jboot.config(ApolloServerConfig.class); - if (apolloConfig.isEnable() && apolloConfig.isConfigOk()){ - System.setProperty("app.id",apolloConfig.getAppId()); - System.setProperty("apollo.meta",apolloConfig.getMeta()); + //加载 jboot-system.properties 代替启动参数的 -D 配置 + File systemPropFile = new File(PathKit.getRootClassPath(), "jboot-system.properties"); + if (systemPropFile.exists() && systemPropFile.isFile()) { + Properties properties = PropKit.use(systemPropFile).getProperties(); + if (properties != null && !properties.isEmpty()) { + for (Object key : properties.keySet()) { + if (StrUtil.isNotBlank(key)) { + String newKey = key.toString().trim(); + String systemValue = System.getProperty(newKey); + if (StrUtil.isNotBlank(systemValue)) { + continue; + } + String newValue = properties.getProperty(newKey); + if (StrUtil.isNotBlank(newValue)) { + System.setProperty(newKey, newValue.trim()); + } + } + } + } } + } + + /** + * 在 JFinal.initPathKit() 这个方法中,如果 webRootPath 会为 null + * 其会去通过 PathKit.detectWebRootPath() 去初始化一个错误的路径 + * 此方法的目的是为了防止 webRootPath 为 null + */ + private void initWebRootPath() { + String webRootPath = ReflectUtil.getStaticFieldValue(PathKit.class, "webRootPath"); + if (webRootPath == null) { + PathKit.setWebRootPath(PathKit.getRootClassPath()); + } } @Override public void configConstant(Constants constants) { + JbootAppListenerManager.me().onConstantConfigBefore(constants); + constants.setRenderFactory(JbootRenderFactory.me()); constants.setDevMode(Jboot.isDevMode()); - ApiConfigKit.setDevMode(Jboot.isDevMode()); - JbootWechatConfig config = Jboot.config(JbootWechatConfig.class); - ApiConfig apiConfig = config.getApiConfig(); - if (apiConfig != null) { - ApiConfigKit.putApiConfig(apiConfig); - } - - constants.setLogFactory(new Slf4jLogFactory()); + constants.setLogFactory(new JbootLogFactory()); constants.setMaxPostSize(1024 * 1024 * 2000); constants.setReportAfterInvocation(false); constants.setControllerFactory(JbootControllerManager.me()); - constants.setJsonFactory(() -> new JbootJson()); + constants.setJsonFactory(JbootJson::new); constants.setInjectDependency(true); + constants.setTokenCache(new JbootTokenCache()); + constants.setCaptchaCache(new JbootCaptchaCache()); + + constants.setBaseUploadPath(LocalAttachmentContainerConfig.getInstance().buildUploadAbsolutePath()); + constants.setJsonDatePattern(DateUtil.datetimePattern); + + if (JbootWebConfig.getInstance().isPathVariableEnable()) { + constants.setActionMapping(PathVariableActionMapping::new); + } else { + constants.setActionMapping(JbootActionMapping::new); + } + JbootAppListenerManager.me().onConstantConfig(constants); } @@ -137,22 +186,9 @@ public class JbootCoreConfig extends JFinalConfig { List> controllerClassList = ClassScanner.scanSubClass(Controller.class); if (ArrayUtil.isNotEmpty(controllerClassList)) { for (Class clazz : controllerClassList) { - RequestMapping mapping = clazz.getAnnotation(RequestMapping.class); - if (mapping == null) { - continue; - } - - String value = AnnotationUtil.get(mapping.value()); - if (value == null) { - continue; - } - - String viewPath = AnnotationUtil.get(mapping.viewPath()); - - if (StrUtil.isNotBlank(viewPath)) { - routes.add(value, clazz, viewPath); - } else { - routes.add(value, clazz); + String[] valueAndViewPath = getMappingAndViewPath(clazz); + if (valueAndViewPath != null) { + initRoutes(routes, clazz, valueAndViewPath[0], valueAndViewPath[1]); } } } @@ -165,36 +201,113 @@ public class JbootCoreConfig extends JFinalConfig { JbootAppListenerManager.me().onRouteConfig(routes); for (Routes.Route route : routes.getRouteItemList()) { - JbootControllerManager.me().setMapping(route.getControllerKey(), route.getControllerClass()); + JbootControllerManager.me().setMapping(route.getControllerPath(), route.getControllerClass()); } routeList.addAll(routes.getRouteItemList()); } + private String removeLastSlash(String path) { + while (path.endsWith("/") && path.length() > 1) { + path = path.substring(0, path.length() - 1); + } + return path; + } + + public static String[] getMappingAndViewPath(Class clazz) { + RequestMapping rm = clazz.getAnnotation(RequestMapping.class); + if (rm != null) { + return new String[]{AnnotationUtil.get(rm.value()), AnnotationUtil.get(rm.viewPath())}; + } + + Path path = clazz.getAnnotation(Path.class); + if (path != null) { + return new String[]{AnnotationUtil.get(path.value()), AnnotationUtil.get(path.viewPath())}; + } + + GetMapping gp = clazz.getAnnotation(GetMapping.class); + if (gp != null) { + return new String[]{AnnotationUtil.get(gp.value()), AnnotationUtil.get(gp.viewPath())}; + } + + PostMapping pp = clazz.getAnnotation(PostMapping.class); + if (pp != null) { + return new String[]{AnnotationUtil.get(pp.value()), AnnotationUtil.get(pp.viewPath())}; + } + + return null; + } + + + private void initRoutes(Routes routes, Class controllerClass, String path, String viewPath) { + if (StrUtil.isBlank(path)) { + return; + } else { + path = AnnotationUtil.get(path); + } + + path = removeLastSlash(path); + viewPath = AnnotationUtil.get(viewPath); + + if (Path.NULL_VIEW_PATH.equals(viewPath)) { + routes.add(path, controllerClass); + } else { + routes.add(path, controllerClass, viewPath); + } + } + + @Override public void configEngine(Engine engine) { + engine.setOutputDirectiveFactory(JbootOutputDirectiveFactory.me); + + //通过 java -jar xxx.jar 在单独的jar里运行 + if (ApplicationUtil.runInFatjar()) { + engine.setToClassPathSourceFactory(); + engine.setBaseTemplatePath("webapp"); + } + List directiveClasses = ClassScanner.scanClass(); - for (Class clazz : directiveClasses) { - JFinalDirective directive = (JFinalDirective) clazz.getAnnotation(JFinalDirective.class); - if (directive != null) { - engine.addDirective(AnnotationUtil.get(directive.value()), clazz); + for (Class clazz : directiveClasses) { + + if (Directive.class.isAssignableFrom(clazz)) { + JFinalDirective directive = clazz.getAnnotation(JFinalDirective.class); + if (directive != null) { + String name = AnnotationUtil.get(directive.value()); + if (directive.override()) { + //remove old directive + engine.removeDirective(name); + } + engine.addDirective(name, (Class) clazz); + } + } else if (clazz.isEnum()) { + JFinalSharedEnum sharedEnum = clazz.getAnnotation(JFinalSharedEnum.class); + if (sharedEnum != null) { + String name = AnnotationUtil.get(sharedEnum.value(), clazz.getSimpleName()); + if (sharedEnum.override()) { + engine.removeSharedObject(name); + } + engine.addSharedObject(name, SharedEnumObject.create((Class>) clazz)); + } } - JFinalSharedMethod sharedMethod = (JFinalSharedMethod) clazz.getAnnotation(JFinalSharedMethod.class); + JFinalSharedMethod sharedMethod = clazz.getAnnotation(JFinalSharedMethod.class); if (sharedMethod != null) { engine.addSharedMethod(ClassUtil.newInstance(clazz)); } - JFinalSharedStaticMethod sharedStaticMethod = (JFinalSharedStaticMethod) clazz.getAnnotation(JFinalSharedStaticMethod.class); + JFinalSharedStaticMethod sharedStaticMethod = clazz.getAnnotation(JFinalSharedStaticMethod.class); if (sharedStaticMethod != null) { engine.addSharedStaticMethod(clazz); } - JFinalSharedObject sharedObject = (JFinalSharedObject) clazz.getAnnotation(JFinalSharedObject.class); + JFinalSharedObject sharedObject = clazz.getAnnotation(JFinalSharedObject.class); if (sharedObject != null) { engine.addSharedObject(AnnotationUtil.get(sharedObject.value()), ClassUtil.newInstance(clazz)); } + + } JbootAppListenerManager.me().onEngineConfig(engine); @@ -216,26 +329,56 @@ public class JbootCoreConfig extends JFinalConfig { @Override public void configInterceptor(Interceptors interceptors) { - - interceptors.addGlobalServiceInterceptor(new JbootAopInterceptor()); - + // 拦截器的 inject 通过 AopManager.me().setInjectDependency(true); 去配置 JbootAppListenerManager.me().onInterceptorConfig(interceptors); - JbootAppListenerManager.me().onFixedInterceptorConfig(FixedInterceptors.me()); } @Override public void configHandler(Handlers handlers) { //先添加用户的handler,再添加jboot自己的handler - //用户的handler优先于jboot的handler执行 + //用户的 handler 优先于 jboot 的 handler 执行 JbootAppListenerManager.me().onHandlerConfig(new JfinalHandlers(handlers)); - handlers.add(new JbootFilterHandler()); + //一般的项目没必要添加门户网关的 Gateway + //在某些情况下,必须要添加的,可以自行添加 + if (JbootGatewayManager.me().isConfigOk()) { + handlers.add(new JbootGatewayHandler()); + } + + handlers.add(new AttachmentHandler()); + + SentinelConfig sentinelConfig = SentinelConfig.get(); + if (sentinelConfig.isEnable() && sentinelConfig.isReqeustEnable()) { + handlers.add(new SentinelHandler()); + } + + //metrics 处理 + JbootMetricConfig metricsConfig = Jboot.config(JbootMetricConfig.class); + if (metricsConfig.isEnable() && metricsConfig.isConfigOk()) { + + if (StrUtil.isNotBlank(metricsConfig.getAdminServletMapping())) { + handlers.add(new MetricServletHandler(metricsConfig.getAdminServletMapping())); + } + + if (metricsConfig.isRequestMetricEnable()) { + handlers.add(new JbootRequestMetricHandler()); + } + } + + if (JbootWebConfig.getInstance().isEscapeParasEnable()) { + handlers.add(new XSSHandler()); + } + handlers.add(new JbootHandler()); //若用户自己没配置 ActionHandler,默认使用 JbootActionHandler if (handlers.getActionHandler() == null) { - handlers.setActionHandler(new JbootActionHandler()); + if (JbootWebConfig.getInstance().isPathVariableEnable()) { + handlers.setActionHandler(new PathVariableActionHandler()); + } else { + handlers.setActionHandler(new JbootActionHandler()); + } } } @@ -245,42 +388,44 @@ public class JbootCoreConfig extends JFinalConfig { JbootAppListenerManager.me().onStartBefore(); - /** - * 配置微信accessToken的缓存 - */ - ApiConfigKit.setAccessTokenCache(new JbootAccessTokenCache()); - JsonManager.me().setDefaultDatePattern("yyyy-MM-dd HH:mm:ss"); - - /** - * 初始化 - */ + // 初始化 Jboot 内置组件 JbootrpcManager.me().init(); JbootShiroManager.me().init(routeList); JbootScheduleManager.me().init(); JbootSwaggerManager.me().init(); LimiterManager.me().init(); JbootSeataManager.me().init(); - SentinelManager.me().init(); + JbootSentinelManager.me().init(); + + if (ClassUtil.hasClass("com.jfinal.weixin.sdk.api.ApiConfigKit")) { + new WechatSupport().autoSupport(); + } JbootAppListenerManager.me().onStart(); + + //自定义参数转换方法 + TypeConverter.me().setConvertFunc(new TypeConverterFunc()); + ArrayConverters.init(); + + //一般情况下,各个模块会在 onStart 进行添加监听器 + //此时可以主动去启动下 mq + JbootmqManager.me().init(); + + //使用场景:需要等所有组件 onStart() 完成之后,再去执行某些工作的时候 + JbootAppListenerManager.me().onStartFinish(); + + } @Override public void onStop() { - Enumeration drivers = DriverManager.getDrivers(); - if (drivers != null) { - while (drivers.hasMoreElements()) { - try { - Driver driver = drivers.nextElement(); - DriverManager.deregisterDriver(driver); - } catch (Exception e) { - LogKit.error(e.toString(), e); - } - } - } JbootAppListenerManager.me().onStop(); + JbootScheduleManager.me().stop(); JbootSeataManager.me().stop(); + JbootrpcManager.me().stop(); + + JbootmqManager.me().stop(); } diff --git a/src/main/java/io/jboot/core/listener/JbootAppListener.java b/src/main/java/io/jboot/core/listener/JbootAppListener.java index fe3c4984fd4c0928924843b4a0d5edbe717663e0..1a14be172e4f9e9d62eee20da7c0c9d0d0b40607 100644 --- a/src/main/java/io/jboot/core/listener/JbootAppListener.java +++ b/src/main/java/io/jboot/core/listener/JbootAppListener.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,31 +21,32 @@ import com.jfinal.config.Routes; import com.jfinal.template.Engine; import io.jboot.aop.jfinal.JfinalHandlers; import io.jboot.aop.jfinal.JfinalPlugins; -import io.jboot.web.fixedinterceptor.FixedInterceptors; public interface JbootAppListener { - public void onInit(); + default void onInit(){} - public void onConstantConfig(Constants constants); + default void onConstantConfigBefore(Constants constants){} - public void onRouteConfig(Routes routes); + default void onConstantConfig(Constants constants){} - public void onEngineConfig(Engine engine); + default void onRouteConfig(Routes routes){} - public void onPluginConfig(JfinalPlugins plugins); + default void onEngineConfig(Engine engine){} - public void onInterceptorConfig(Interceptors interceptors); + default void onPluginConfig(JfinalPlugins plugins){} - public void onFixedInterceptorConfig(FixedInterceptors fixedInterceptors); + default void onInterceptorConfig(Interceptors interceptors){} - public void onHandlerConfig(JfinalHandlers handlers); + default void onHandlerConfig(JfinalHandlers handlers){} - public void onStartBefore(); + default void onStartBefore(){} - public void onStart(); + default void onStart(){} - public void onStop(); + default void onStartFinish(){} + + default void onStop(){} } diff --git a/src/main/java/io/jboot/core/listener/JbootAppListenerBase.java b/src/main/java/io/jboot/core/listener/JbootAppListenerBase.java index f624849201a0f8b1809d4c3e0ae9c555760a388f..533e9c573b878eb176b3a8820a3d074f3be4a8f5 100644 --- a/src/main/java/io/jboot/core/listener/JbootAppListenerBase.java +++ b/src/main/java/io/jboot/core/listener/JbootAppListenerBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,12 @@ */ package io.jboot.core.listener; -import com.jfinal.config.*; +import com.jfinal.config.Constants; +import com.jfinal.config.Interceptors; +import com.jfinal.config.Routes; import com.jfinal.template.Engine; import io.jboot.aop.jfinal.JfinalHandlers; import io.jboot.aop.jfinal.JfinalPlugins; -import io.jboot.web.fixedinterceptor.FixedInterceptors; public class JbootAppListenerBase implements JbootAppListener { @@ -56,22 +57,22 @@ public class JbootAppListenerBase implements JbootAppListener { } @Override - public void onFixedInterceptorConfig(FixedInterceptors fixedInterceptors) { + public void onHandlerConfig(JfinalHandlers handlers) { } @Override - public void onHandlerConfig(JfinalHandlers handlers) { + public void onStartBefore() { } @Override - public void onStartBefore() { + public void onStart() { } @Override - public void onStart() { + public void onStartFinish() { } diff --git a/src/main/java/io/jboot/core/listener/JbootAppListenerManager.java b/src/main/java/io/jboot/core/listener/JbootAppListenerManager.java index 63301b2b2f92c1f9d678c3c8a808966b41ae836c..94cd7d5baab7d6991f9fa5b18a805adc2b7be3aa 100644 --- a/src/main/java/io/jboot/core/listener/JbootAppListenerManager.java +++ b/src/main/java/io/jboot/core/listener/JbootAppListenerManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,171 +18,187 @@ package io.jboot.core.listener; import com.jfinal.config.Constants; import com.jfinal.config.Interceptors; import com.jfinal.config.Routes; -import com.jfinal.log.Log; +import com.jfinal.kit.LogKit; import com.jfinal.template.Engine; import io.jboot.aop.jfinal.JfinalHandlers; import io.jboot.aop.jfinal.JfinalPlugins; +import io.jboot.app.JbootApplicationConfig; import io.jboot.core.weight.WeightUtil; import io.jboot.utils.ClassScanner; import io.jboot.utils.ClassUtil; -import io.jboot.web.fixedinterceptor.FixedInterceptors; +import io.jboot.utils.StrUtil; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; public class JbootAppListenerManager implements JbootAppListener { - private static Log log = Log.getLog(JbootAppListenerManager.class); private static JbootAppListenerManager me = new JbootAppListenerManager(); - public static JbootAppListenerManager me() { return me; } - - List listeners = new ArrayList<>(); + private List listeners = new ArrayList<>(); private JbootAppListenerManager() { - List> allListeners = ClassScanner.scanSubClass(JbootAppListener.class, true); - if (allListeners == null || allListeners.size() == 0) { - return; + + String listener = JbootApplicationConfig.get().getListener(); + + String listenerPackage = JbootApplicationConfig.get().getListenerPackage(); + Set packages = StrUtil.isNotBlank(listenerPackage) && !"*".equals(listenerPackage.trim()) + ? StrUtil.splitToSet(listenerPackage, ";") : new HashSet<>(); + + if (StrUtil.isBlank(listener) || "*".equals(listener.trim())) { + List> allListeners = ClassScanner.scanSubClass(JbootAppListener.class, true); + allListeners.removeIf((Predicate>) c -> + c == JbootAppListenerManager.class || c == JbootAppListenerBase.class); + + allListeners.forEach(clazz -> { + if (isMatchedPackage(packages, clazz.getCanonicalName())) { + JbootAppListener appListener = ClassUtil.newInstance(clazz); + if (appListener != null) { + listeners.add(appListener); + } + } + }); + } else { + StrUtil.splitToSet(listener, ";").forEach(className -> { + if (isMatchedPackage(packages, className)) { + JbootAppListener appListener = ClassUtil.newInstance(className); + if (appListener != null) { + listeners.add(appListener); + } else { + // log 组件还未配置,无法使用 log 组件输出 + System.err.println("Can not create JbootAppListener by class: " + className); + } + } + }); } - for (Class clazz : allListeners) { - if (JbootAppListenerManager.class == clazz || JbootAppListenerBase.class == clazz) { - continue; - } + WeightUtil.sort(listeners); + } - JbootAppListener listener = ClassUtil.newInstance(clazz, false); - if (listener != null) { - listeners.add(listener); - } + private boolean isMatchedPackage(Set packages, String className) { + //matched all + if (packages == null || packages.isEmpty()) { + return true; } - WeightUtil.sort(listeners); + for (String packageString : packages) { + if (className.startsWith(packageString)) { + return true; + } + } + return false; } + public List getListeners() { + return listeners; + } @Override public void onInit() { - for (JbootAppListener listener : listeners) { - try { + eachListeners(new TriggerThrowable() { + @Override + public void trigger(JbootAppListener listener) { listener.onInit(); - } catch (Throwable ex) { - log.error(ex.toString(), ex); } - } + + @Override + public void throwable(Throwable ex) { + //在 onConstantConfigBefore 的时候, log 组件未初始化,无法使用 + ex.printStackTrace(); + } + }); } @Override - public void onConstantConfig(Constants constants) { - for (JbootAppListener listener : listeners) { - try { - listener.onConstantConfig(constants); - } catch (Throwable ex) { - log.error(ex.toString(), ex); + public void onConstantConfigBefore(Constants constants) { + eachListeners(new TriggerThrowable() { + @Override + public void trigger(JbootAppListener listener) { + listener.onConstantConfigBefore(constants); } - } + + @Override + public void throwable(Throwable ex) { + //在 onConstantConfigBefore 的时候, log 组件未初始化,无法使用 + ex.printStackTrace(); + } + }); + } + + @Override + public void onConstantConfig(Constants constants) { + eachListeners(listener -> listener.onConstantConfig(constants)); } @Override public void onRouteConfig(Routes routes) { - for (JbootAppListener listener : listeners) { - try { - listener.onRouteConfig(routes); - } catch (Throwable ex) { - log.error(ex.toString(), ex); - } - } + eachListeners(listener -> listener.onRouteConfig(routes)); } @Override public void onEngineConfig(Engine engine) { - for (JbootAppListener listener : listeners) { - try { - listener.onEngineConfig(engine); - } catch (Throwable ex) { - log.error(ex.toString(), ex); - } - } + eachListeners(listener -> listener.onEngineConfig(engine)); } @Override public void onPluginConfig(JfinalPlugins plugins) { - for (JbootAppListener listener : listeners) { - try { - listener.onPluginConfig(plugins); - } catch (Throwable ex) { - log.error(ex.toString(), ex); - } - } + eachListeners(listener -> listener.onPluginConfig(plugins)); } @Override public void onInterceptorConfig(Interceptors interceptors) { - for (JbootAppListener listener : listeners) { - try { - listener.onInterceptorConfig(interceptors); - } catch (Throwable ex) { - log.error(ex.toString(), ex); - } - } + eachListeners(listener -> listener.onInterceptorConfig(interceptors)); } - @Override - public void onFixedInterceptorConfig(FixedInterceptors fixedInterceptors) { - for (JbootAppListener listener : listeners) { - try { - listener.onFixedInterceptorConfig(fixedInterceptors); - } catch (Throwable ex) { - log.error(ex.toString(), ex); - } - } - } @Override public void onHandlerConfig(JfinalHandlers handlers) { - for (JbootAppListener listener : listeners) { - try { - listener.onHandlerConfig(handlers); - } catch (Throwable ex) { - log.error(ex.toString(), ex); - } - } + eachListeners(listener -> listener.onHandlerConfig(handlers)); } @Override public void onStartBefore() { - for (JbootAppListener listener : listeners) { - try { - listener.onStartBefore(); - } catch (Throwable ex) { - log.error(ex.toString(), ex); - } - } + eachListeners(JbootAppListener::onStartBefore); } @Override public void onStart() { - for (JbootAppListener listener : listeners) { - try { - listener.onStart(); - } catch (Throwable ex) { - log.error(ex.toString(), ex); - } - } + eachListeners(JbootAppListener::onStart); + } + + @Override + public void onStartFinish() { + eachListeners(JbootAppListener::onStartFinish); } @Override public void onStop() { + eachListeners(JbootAppListener::onStop); + } + + private void eachListeners(TriggerThrowable triggerThrowable) { for (JbootAppListener listener : listeners) { try { - listener.onStop(); + triggerThrowable.trigger(listener); } catch (Throwable ex) { - log.error(ex.toString(), ex); + triggerThrowable.throwable(ex); } } } + @FunctionalInterface + private interface TriggerThrowable { + void trigger(JbootAppListener listener); + + default void throwable(Throwable ex) { + LogKit.error(ex.toString(), ex); + } + } } diff --git a/src/main/java/io/jboot/core/log/Slf4jLogFactory.java b/src/main/java/io/jboot/core/log/JbootLogFactory.java similarity index 46% rename from src/main/java/io/jboot/core/log/Slf4jLogFactory.java rename to src/main/java/io/jboot/core/log/JbootLogFactory.java index 87b760d7df98180a04258a54e5985d7279cc9cbd..6c427db132a1311d7c9b5641325b3d467c172d50 100644 --- a/src/main/java/io/jboot/core/log/Slf4jLogFactory.java +++ b/src/main/java/io/jboot/core/log/JbootLogFactory.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -16,25 +16,57 @@ package io.jboot.core.log; import com.jfinal.log.Log; +import org.slf4j.ILoggerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.helpers.NOPLoggerFactory; import org.slf4j.spi.LocationAwareLogger; /** * @author michael */ -public class Slf4jLogFactory extends com.jfinal.log.Slf4jLogFactory { +public class JbootLogFactory extends com.jfinal.log.Slf4jLogFactory { + private boolean useJdkLogger = false; + + public JbootLogFactory() { + boolean hasStaticLoggerBinder = false; + try { + Class.forName("org.slf4j.impl.StaticLoggerBinder"); + hasStaticLoggerBinder = true; + } catch (ClassNotFoundException e) {} + + if (!hasStaticLoggerBinder) { + useJdkLogger = true; + } else { + ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory(); + if (loggerFactory.getClass() == NOPLoggerFactory.class){ + useJdkLogger = true; + }else { + System.out.println("Jboot LoggerFactory: " + loggerFactory.getClass().getName()); + } + } + } @Override public Log getLog(Class clazz) { + if (useJdkLogger) { + return new JdkLogger(clazz); + } + Logger log = LoggerFactory.getLogger(clazz); - return log instanceof LocationAwareLogger ? new Slf4jLogger((LocationAwareLogger)log) : new Slf4jSimpleLogger(log); + return log instanceof LocationAwareLogger ? new Slf4jLogger((LocationAwareLogger) log) : new Slf4jSimpleLogger(log); } @Override public Log getLog(String name) { + if (useJdkLogger) { + return new JdkLogger(name); + } + Logger log = LoggerFactory.getLogger(name); - return log instanceof LocationAwareLogger ? new Slf4jLogger((LocationAwareLogger)log) : new Slf4jSimpleLogger(log); + return log instanceof LocationAwareLogger ? new Slf4jLogger((LocationAwareLogger) log) : new Slf4jSimpleLogger(log); } + + } diff --git a/src/main/java/io/jboot/core/log/JdkLogger.java b/src/main/java/io/jboot/core/log/JdkLogger.java new file mode 100644 index 0000000000000000000000000000000000000000..880bba3fdd17fdb204b4ed8c41dd8224bc7c92db --- /dev/null +++ b/src/main/java/io/jboot/core/log/JdkLogger.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.core.log; + +import com.jfinal.log.Log; +import com.jfinal.log.LogInfo; +import io.jboot.exception.JbootExceptionHolder; + +import java.util.logging.Level; + +/** + * JdkLogger from JdkLog. + */ +public class JdkLogger extends Log { + + private java.util.logging.Logger log; + private String clazzName; + + JdkLogger(Class clazz) { + log = java.util.logging.Logger.getLogger(clazz.getName()); + clazzName = clazz.getName(); + } + + JdkLogger(String name) { + log = java.util.logging.Logger.getLogger(name); + clazzName = name; + } + + @Override + public void trace(String message) { + log.logp(Level.FINEST, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message); + } + + @Override + public void trace(String message, Throwable t) { + log.logp(Level.FINEST, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message, t); + } + + @Override + public void debug(String message) { + log.logp(Level.FINE, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message); + } + + @Override + public void debug(String message, Throwable t) { + log.logp(Level.FINE, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message, t); + } + + @Override + public void info(String message) { + log.logp(Level.INFO, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message); + } + + @Override + public void info(String message, Throwable t) { + log.logp(Level.INFO, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message, t); + } + + @Override + public void warn(String message) { + log.logp(Level.WARNING, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message); + } + + @Override + public void warn(String message, Throwable t) { + log.logp(Level.WARNING, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message, t); + } + + @Override + public void error(String message) { + JbootExceptionHolder.hold(message,null); + log.logp(Level.SEVERE, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message); + } + + @Override + public void error(String message, Throwable t) { + JbootExceptionHolder.hold(message, t); + log.logp(Level.SEVERE, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message, t); + } + + /** + * JdkLog fatal is the same as the error. + */ + @Override + public void fatal(String message) { + log.logp(Level.SEVERE, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message); + } + + /** + * JdkLog fatal is the same as the error. + */ + @Override + public void fatal(String message, Throwable t) { + JbootExceptionHolder.hold(message, t); + log.logp(Level.SEVERE, clazzName, Thread.currentThread().getStackTrace()[1].getMethodName(), message, t); + } + + @Override + public boolean isTraceEnabled() { + return log.isLoggable(Level.FINEST); + } + + @Override + public boolean isDebugEnabled() { + return log.isLoggable(Level.FINE); + } + + @Override + public boolean isInfoEnabled() { + return log.isLoggable(Level.INFO); + } + + @Override + public boolean isWarnEnabled() { + return log.isLoggable(Level.WARNING); + } + + @Override + public boolean isErrorEnabled() { + return log.isLoggable(Level.SEVERE); + } + + @Override + public boolean isFatalEnabled() { + return log.isLoggable(Level.SEVERE); + } + + // ------------------------------------------------------- + + /* + * 以下方法与前面的两个 trace 方法必须覆盖父类中的实现,否则日志中的类名为 + * com.jfinal.log.Log 而非所需要的日志发生地点的类名 + */ + + @Override + public void trace(String format, Object... args) { + if (isTraceEnabled()) { + if (endsWithThrowable(args)) { + LogInfo li = parse(format, args); + trace(li.message, li.throwable); + } else { + trace(String.format(format, args)); + } + } + } + + @Override + public void debug(String format, Object... args) { + if (isDebugEnabled()) { + if (endsWithThrowable(args)) { + LogInfo li = parse(format, args); + debug(li.message, li.throwable); + } else { + debug(String.format(format, args)); + } + } + } + + @Override + public void info(String format, Object... args) { + if (isInfoEnabled()) { + if (endsWithThrowable(args)) { + LogInfo li = parse(format, args); + info(li.message, li.throwable); + } else { + info(String.format(format, args)); + } + } + } + + @Override + public void warn(String format, Object... args) { + if (isWarnEnabled()) { + if (endsWithThrowable(args)) { + LogInfo li = parse(format, args); + warn(li.message, li.throwable); + } else { + warn(String.format(format, args)); + } + } + } + + @Override + public void error(String format, Object... args) { + if (isErrorEnabled()) { + if (endsWithThrowable(args)) { + LogInfo li = parse(format, args); + error(li.message, li.throwable); + } else { + error(String.format(format, args)); + } + } + } + + @Override + public void fatal(String format, Object... args) { + if (isFatalEnabled()) { + if (endsWithThrowable(args)) { + LogInfo li = parse(format, args); + fatal(li.message, li.throwable); + } else { + fatal(String.format(format, args)); + } + } + } +} + + + diff --git a/src/main/java/io/jboot/core/log/Slf4jLogger.java b/src/main/java/io/jboot/core/log/Slf4jLogger.java index 90393380b2540117fea45ada4774cccf01271226..0bbf8e763ae341874ec9e58df62dc4bce2fc06f9 100644 --- a/src/main/java/io/jboot/core/log/Slf4jLogger.java +++ b/src/main/java/io/jboot/core/log/Slf4jLogger.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package io.jboot.core.log; import com.jfinal.log.Log; -import com.jfinal.log.Slf4jLog; import io.jboot.exception.JbootExceptionHolder; import org.slf4j.helpers.FormattingTuple; import org.slf4j.helpers.MessageFormatter; @@ -30,7 +29,7 @@ public class Slf4jLogger extends Log { private LocationAwareLogger log; private static final Object[] NULL_ARGS = new Object[0]; - private static final String callerFQCN = Slf4jLog.class.getName(); + private static final String callerFQCN = Slf4jLogger.class.getName(); public Slf4jLogger(LocationAwareLogger log) { this.log = log; @@ -78,6 +77,7 @@ public class Slf4jLogger extends Log { @Override public void error(String message) { + JbootExceptionHolder.hold(message,null); log.log(null, callerFQCN, LocationAwareLogger.ERROR_INT, message, NULL_ARGS, null); } diff --git a/src/main/java/io/jboot/core/log/Slf4jSimpleLogger.java b/src/main/java/io/jboot/core/log/Slf4jSimpleLogger.java index d9ebc19fdf07552b2582c04abf9bcf22c7c72cb8..6f4c055463d2fbe690e65668117ebea772fe8788 100644 --- a/src/main/java/io/jboot/core/log/Slf4jSimpleLogger.java +++ b/src/main/java/io/jboot/core/log/Slf4jSimpleLogger.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2019, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, diff --git a/src/main/java/io/jboot/core/spi/JbootSpi.java b/src/main/java/io/jboot/core/spi/JbootSpi.java index c6c3d221f166ebfb51ce057a4945a94ce175c414..d623b387e7f09b5df361890c442c3fc7c7601bcc 100644 --- a/src/main/java/io/jboot/core/spi/JbootSpi.java +++ b/src/main/java/io/jboot/core/spi/JbootSpi.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/core/spi/JbootSpiLoader.java b/src/main/java/io/jboot/core/spi/JbootSpiLoader.java index de67eed75d33bfc570f57ce9e407aa9e6f122489..edc47bdc2d3f895263025213f5ec415f33ac7116 100644 --- a/src/main/java/io/jboot/core/spi/JbootSpiLoader.java +++ b/src/main/java/io/jboot/core/spi/JbootSpiLoader.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import io.jboot.utils.StrUtil; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.ServiceLoader; /** @@ -40,6 +41,34 @@ import java.util.ServiceLoader; public class JbootSpiLoader { + /** + * 通过 SPI 去加载相应的扩展子类 + * + * @param clazz + * @param spiName + * @param + * @return + */ + public static T load(Class clazz, String spiName, Object... paras) { + List> classes = ClassScanner.scanSubClass(clazz, true); + if (classes.isEmpty()) { + return null; + } + + for (Class c : classes) { + JbootSpi spiConfig = c.getAnnotation(JbootSpi.class); + if (spiConfig != null && spiName.equals(AnnotationUtil.get(spiConfig.value()))) { + return ClassUtil.newInstance(c, paras); + } + //support config class name + else if (spiName.equals(c.getName())) { + return ClassUtil.newInstance(c, paras); + } + } + + return null; + } + /** * 通过 SPI 去加载相应的扩展子类 * @@ -50,23 +79,30 @@ public class JbootSpiLoader { */ public static T load(Class clazz, String spiName) { T returnObject = loadByServiceLoader(clazz, spiName); - if (returnObject != null) return returnObject; + if (returnObject != null) { + return returnObject; + } if (StrUtil.isBlank(spiName)) { return null; } - List> classes = ClassScanner.scanSubClass(clazz,true); - if (classes == null || classes.isEmpty()) { + List> classes = ClassScanner.scanSubClass(clazz, true); + if (classes.isEmpty()) { return null; } for (Class c : classes) { JbootSpi spiConfig = c.getAnnotation(JbootSpi.class); - if (spiConfig != null && spiName.equals(AnnotationUtil.get(spiConfig.value()))) { + if (spiConfig != null && Objects.equals(spiName, AnnotationUtil.get(spiConfig.value()))) { + return ClassUtil.newInstance(c); + } + //support config class name + else if (Objects.equals(spiName, c.getName())) { return ClassUtil.newInstance(c); } } + return null; } diff --git a/src/main/java/io/jboot/core/weight/Weight.java b/src/main/java/io/jboot/core/weight/Weight.java index 9a12f909a701a5d143ca1ad8760aa438f7becd38..e3d258aa8fe23730e71becdbc605a3fcd063e634 100644 --- a/src/main/java/io/jboot/core/weight/Weight.java +++ b/src/main/java/io/jboot/core/weight/Weight.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,11 @@ import java.lang.annotation.*; /** * @author michael yang */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -@Documented public @interface Weight { - int value() default 0; + int value(); } diff --git a/src/main/java/io/jboot/core/weight/WeightUtil.java b/src/main/java/io/jboot/core/weight/WeightUtil.java index 160e7937cefd6dd9d40dc7a4ec1911e9f1561ea8..38393eb51d6b534f3ff9ef4b430f261131b91365 100644 --- a/src/main/java/io/jboot/core/weight/WeightUtil.java +++ b/src/main/java/io/jboot/core/weight/WeightUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,8 @@ import java.util.List; */ public class WeightUtil { - public static void sort(List list) { - if (list == null && list.isEmpty()) { + public static void sort(List list) { + if (list == null || list.isEmpty()) { return; } list.sort((o1, o2) -> { diff --git a/src/main/java/io/jboot/db/ArpManager.java b/src/main/java/io/jboot/db/ArpManager.java index 837c3ac7cd7980352ef0279afdc8362f76cb1e94..589f1d1c6911aa0dfbe4e444ecb03024e7c00541 100644 --- a/src/main/java/io/jboot/db/ArpManager.java +++ b/src/main/java/io/jboot/db/ArpManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,10 @@ */ package io.jboot.db; -import com.jfinal.plugin.activerecord.*; +import com.jfinal.plugin.activerecord.ActiveRecordPlugin; +import com.jfinal.plugin.activerecord.CaseInsensitiveContainerFactory; +import com.jfinal.plugin.activerecord.IDbProFactory; +import com.jfinal.plugin.activerecord.Model; import com.jfinal.plugin.activerecord.dialect.Dialect; import io.jboot.Jboot; import io.jboot.components.cache.JbootCache; @@ -26,7 +29,6 @@ import io.jboot.db.dbpro.JbootDbProFactory; import io.jboot.db.dialect.*; import io.jboot.exception.JbootException; import io.jboot.exception.JbootIllegalConfigException; -import io.jboot.utils.ArrayUtil; import io.jboot.utils.ClassUtil; import io.jboot.utils.StrUtil; @@ -40,52 +42,68 @@ import java.util.*; */ public class ArpManager { - private static ArpManager manager; + private static ArpManager instance; private List activeRecordPlugins = new ArrayList<>(); public static ArpManager me() { - if (manager == null) { - manager = new ArpManager(); + if (instance == null) { + instance = new ArpManager(); } - return manager; + return instance; } - public ArpManager() { + private ArpManager() { + Map datasourceConfigs = DataSourceConfigManager.me().getDatasourceConfigs(); + createdRecordPlugins(datasourceConfigs); + } + private void createdRecordPlugins(Map allConfigs) { - Map allDatasourceConfigs = DataSourceConfigManager.me().getDatasourceConfigs(); + Map dsCache = new HashMap<>(); - // 包含了指定表配置的数据源 - Map hasTableDatasourceConfigs = new HashMap<>(); + // 优先初始化 默认数据源 + DataSourceConfig mainDataSourceConfig = allConfigs.remove(DataSourceConfig.NAME_DEFAULT); + initRecordPlugin(dsCache, mainDataSourceConfig); - Iterator> it = allDatasourceConfigs.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - if (StrUtil.isNotBlank(entry.getValue().getTable())) { - hasTableDatasourceConfigs.put(entry.getKey(), entry.getValue()); - it.remove(); - } + // 初始化默认数据源后,再开始初始化其他数据库 + for (Map.Entry entry : allConfigs.entrySet()) { + initRecordPlugin(dsCache, entry.getValue()); } - // 优先创建有指定表的数据源的 activeRecordPlugin - // 表一旦附着到 activeRecordPlugin, 就不会被其他 activeRecordPlugin 包含了 - createRecordPlugin(hasTableDatasourceConfigs); - createRecordPlugin(allDatasourceConfigs); - } + // 为所有的 activeRecordPlugin 添加 jfinal 的表映射 + for (ActiveRecordPlugin activeRecordPlugin : activeRecordPlugins) { + DataSourceConfig dataSourceConfig = dsCache.get(System.identityHashCode(activeRecordPlugin)); - private void createRecordPlugin(Map mergeDatasourceConfigs) { - for (Map.Entry entry : mergeDatasourceConfigs.entrySet()) { - DataSourceConfig datasourceConfig = entry.getValue(); - if (datasourceConfig.isConfigOk()) { - ActiveRecordPlugin activeRecordPlugin = createRecordPlugin(datasourceConfig); - activeRecordPlugins.add(activeRecordPlugin); + List tableInfos = dataSourceConfig.getTableInfos(); + if (tableInfos != null && !tableInfos.isEmpty()) { + for (TableInfo table : tableInfos) { + String tableName = StrUtil.isNotBlank(dataSourceConfig.getTablePrefix()) ? dataSourceConfig.getTablePrefix() + table.getTableName() : table.getTableName(); + if (StrUtil.isNotBlank(table.getPrimaryKey())) { + activeRecordPlugin.addMapping(tableName, table.getPrimaryKey(), (Class>) table.getModelClass()); + } else { + activeRecordPlugin.addMapping(tableName, (Class>) table.getModelClass()); + } + } } } + + } + + private void initRecordPlugin(Map arpDatasourceConfigs, DataSourceConfig datasourceConfig) { + if (datasourceConfig != null && datasourceConfig.isConfigOk()) { + + // 执行 createRecordPlugin(...) 的时候,会同时完善 DataSourceConfig 里绑定的表数据 + // createRecordPlugin完毕后,就可以通过 dataSourceConfig.getTableInfos() 去获取该数据源有哪些表 + ActiveRecordPlugin activeRecordPlugin = createRecordPlugin(datasourceConfig); + + arpDatasourceConfigs.put(System.identityHashCode(activeRecordPlugin), datasourceConfig); + activeRecordPlugins.add(activeRecordPlugin); + } } @@ -99,10 +117,9 @@ public class ArpManager { ActiveRecordPlugin activeRecordPlugin = newRecordPlugin(config); - if (StrUtil.isNotBlank(config.getDbProFactory())) { IDbProFactory dbProFactory = Objects.requireNonNull(ClassUtil.newInstance(config.getDbProFactory()), - "can not create dbProfactory by class : " + config.getDbProFactory()); + "Can not create dbProfactory by class: " + config.getDbProFactory()); activeRecordPlugin.setDbProFactory(dbProFactory); } else { activeRecordPlugin.setDbProFactory(new JbootDbProFactory()); @@ -124,33 +141,17 @@ public class ArpManager { activeRecordPlugin.setCache(jbootCache); } - configSqlTemplate(config, activeRecordPlugin); + configSqlTemplate(activeRecordPlugin, config); configDialect(activeRecordPlugin, config); /** - * 不需要添加映射的直接返回 - * - * 在一个表有多个数据源的情况下,应该只需要添加一个映射就可以了, - * 添加映射:默认为该model的数据源, + * 在一个表有多个数据源的情况下,应该只需要添加一个映射就可以了 + * 添加映射:默认为该 model 的数据源 * 不添加映射:通过 model.use("xxx").save() 这种方式去调用该数据源 * 不添加映射使用从场景一般是:读写分离时,用于读取只读数据库的数据 */ - if (!config.isNeedAddMapping()) { - return activeRecordPlugin; - } - - //获得该数据源匹配的表 - List tableInfos = TableInfoManager.me().getMatchTablesInfos(config); - if (ArrayUtil.isNullOrEmpty(tableInfos)) { - return activeRecordPlugin; - } - - for (TableInfo table : tableInfos) { - if (StrUtil.isNotBlank(table.getPrimaryKey())) { - activeRecordPlugin.addMapping(table.getTableName(), table.getPrimaryKey(), (Class>) table.getModelClass()); - } else { - activeRecordPlugin.addMapping(table.getTableName(), (Class>) table.getModelClass()); - } + if (config.isNeedAddMapping()) { + TableInfoManager.me().initConfigMappingTables(config); } return activeRecordPlugin; @@ -189,7 +190,7 @@ public class ArpManager { * @param datasourceConfig * @param activeRecordPlugin */ - private void configSqlTemplate(DataSourceConfig datasourceConfig, ActiveRecordPlugin activeRecordPlugin) { + private void configSqlTemplate(ActiveRecordPlugin activeRecordPlugin, DataSourceConfig datasourceConfig) { String sqlTemplatePath = datasourceConfig.getSqlTemplatePath(); if (StrUtil.isNotBlank(sqlTemplatePath)) { activeRecordPlugin.setBaseSqlTemplatePath(sqlTemplatePath); @@ -218,7 +219,7 @@ public class ArpManager { if (datasourceConfig.getDialectClass() != null) { Dialect dialect = ClassUtil.newInstance(datasourceConfig.getDialectClass(), false); if (dialect == null) { - throw new NullPointerException("can not new instance by class:" + datasourceConfig.getDialectClass()); + throw new JbootIllegalConfigException("Can not new instance by class: " + datasourceConfig.getDialectClass()); } activeRecordPlugin.setDialect(dialect); return; @@ -246,8 +247,17 @@ public class ArpManager { case DataSourceConfig.TYPE_POSTGRESQL: activeRecordPlugin.setDialect(new JbootPostgreSqlDialect()); break; + case DataSourceConfig.TYPE_DM: + activeRecordPlugin.setDialect(new JbootDmDialect()); + break; + case DataSourceConfig.TYPE_CLICKHOUSE: + activeRecordPlugin.setDialect(new JbootClickHouseDialect()); + break; + case DataSourceConfig.TYPE_INFORMIX: + activeRecordPlugin.setDialect(new JbootInformixDialect()); + break; default: - throw new JbootIllegalConfigException("only support datasource type : mysql、orcale、sqlserver、sqlite、ansisql and postgresql, please check your jboot.properties. "); + throw new JbootIllegalConfigException("only support datasource type: mysql、orcale、sqlserver、sqlite、ansisql、postgresql and clickhouse, please check your jboot.properties. "); } } diff --git a/src/main/java/io/jboot/db/JbootDb.java b/src/main/java/io/jboot/db/JbootDb.java index 55d2529ce2e2996c617dda071c15041ef4a4b2eb..d0dc2185c488fe3a9cc7fd09e0d0f37e85c72f54 100644 --- a/src/main/java/io/jboot/db/JbootDb.java +++ b/src/main/java/io/jboot/db/JbootDb.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,19 +19,34 @@ import com.jfinal.plugin.activerecord.Db; import com.jfinal.plugin.activerecord.Record; import io.jboot.db.dbpro.JbootDbPro; import io.jboot.db.model.Columns; +import io.jboot.utils.StrUtil; import java.util.List; public class JbootDb extends Db { + private static ThreadLocal CONFIG_NAME_TL = new ThreadLocal<>(); + + public static String getCurrentConfigName() { + return CONFIG_NAME_TL.get(); + } + + public static void setCurrentConfigName(String configName) { + CONFIG_NAME_TL.set(configName); + } + + public static void clearCurrentConfigName() { + CONFIG_NAME_TL.remove(); + } public static JbootDbPro use(String configName) { return (JbootDbPro) Db.use(configName); } public static JbootDbPro use() { - return (JbootDbPro) Db.use(); + String currentConfigName = getCurrentConfigName(); + return StrUtil.isBlank(currentConfigName) ? (JbootDbPro) Db.use() : use(currentConfigName); } @@ -55,6 +70,17 @@ public class JbootDb extends Db { } + public static Record findFirst(String tableName, Columns columns) { + return findFirst(tableName, columns, null); + } + + + public static Record findFirst(String tableName, Columns columns, String orderBy) { + final List records = use().find(tableName, columns, orderBy, 1); + return records != null && !records.isEmpty() ? records.get(0) : null; + } + + public static int delete(String tableName, Columns columns) { return use().delete(tableName, columns); } diff --git a/src/main/java/io/jboot/db/SqlDebugger.java b/src/main/java/io/jboot/db/SqlDebugger.java index 60676e950a77d236057777e5f94617535b603bb2..cc5287ced9e11433deadd0c168ac82958959505d 100644 --- a/src/main/java/io/jboot/db/SqlDebugger.java +++ b/src/main/java/io/jboot/db/SqlDebugger.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2019, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -15,11 +15,13 @@ */ package io.jboot.db; -import com.jfinal.ext.kit.DateKit; +import com.jfinal.log.Log; import com.jfinal.plugin.activerecord.Config; import io.jboot.Jboot; +import io.jboot.utils.DateUtil; import io.jboot.utils.StrUtil; +import java.sql.SQLException; import java.util.Date; import java.util.regex.Matcher; @@ -29,20 +31,8 @@ import java.util.regex.Matcher; */ public class SqlDebugger { - private static SqlDebugPrinter defaultPrinter = new SqlDebugPrinter() { - @Override - public boolean isPrint(Config config, String sql, Object... paras) { - return Jboot.isDevMode(); - } - - @Override - public void print(String sql) { - System.out.println("\r\njboot exec sql >>> " + sql); - } - }; - - private static SqlDebugPrinter printer = defaultPrinter; + private static SqlDebugPrinter printer = SqlDebugPrinter.DEFAULT_PRINTER; public static SqlDebugPrinter getPrinter() { return printer; @@ -52,48 +42,113 @@ public class SqlDebugger { SqlDebugger.printer = printer; } - public static void debug(Config config, String sql, Object... paras) { - if (printer.isPrint(config, sql, paras)) { + public static T run(SqlRunner runner, Config config, String sql, Object... paras) throws SQLException { + if (!printer.isPrintEnable(config)) { + return runner.run(); + } else { + long timeMillis = System.currentTimeMillis(); + try { + return runner.run(); + } finally { + doDebug(System.currentTimeMillis() - timeMillis, sql, paras); + } + } + } - if (paras != null) { - for (Object value : paras) { - // null - if (value == null) { - sql = sql.replaceFirst("\\?", "null"); - } - // number - else if (value instanceof Number) { - sql = sql.replaceFirst("\\?", value.toString()); - } - // numeric - else if (value instanceof String && StrUtil.isNumeric((String) value)) { - sql = sql.replaceFirst("\\?", (String) value); - } - // other - else { - StringBuilder sb = new StringBuilder(); - sb.append("'"); - if (value instanceof Date) { - sb.append(DateKit.toStr((Date) value, DateKit.timeStampPattern)); - } else { - sb.append(value.toString()); - } - sb.append("'"); - sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(sb.toString())); + + private static void doDebug(Long tookTimeMillis, String sql, Object... paras) { + if (paras != null) { + for (Object value : paras) { + // null + if (value == null) { + sql = sql.replaceFirst("\\?", "null"); + } + // number + else if (value instanceof Number || value instanceof Boolean) { + sql = sql.replaceFirst("\\?", value.toString()); + } + // numeric + else if (value instanceof String && StrUtil.isNumeric((String) value)) { + sql = sql.replaceFirst("\\?", (String) value); + } + // other + else { + StringBuilder sb = new StringBuilder(); + sb.append("'"); + if (value instanceof Date) { + sb.append(DateUtil.toDateTimeString((Date) value)); + } else { + sb.append(value); } + sb.append("'"); + sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(sb.toString())); } } - - printer.print(sql); } + + printer.print(sql, tookTimeMillis); } - public static interface SqlDebugPrinter { + public interface SqlDebugPrinter { + + SqlDebugPrinter DEFAULT_PRINTER = new SqlDebugPrinter() { + + private boolean printSqlEnable = Jboot.isDevMode(); - public boolean isPrint(Config config, String sql, Object... paras); + @Override + public void setPrintEnable(boolean enable) { + this.printSqlEnable = enable; + } + + @Override + public boolean isPrintEnable(Config config) { + return printSqlEnable; + } + + @Override + public void print(String sql, Long tookTimeMillis) { + if (tookTimeMillis != null) { + System.out.println("Jboot exec sql took " + tookTimeMillis + " ms >>> " + sql); + } else { + System.out.println("Jboot exec sql >>> " + sql); + } + } + }; + + SqlDebugPrinter LOG_PRINTER = new SqlDebugPrinter() { + + private boolean printSqlEnable = Jboot.isDevMode(); + private Log log = Log.getLog("SqlDebugPrinter.LogPrinter"); + + @Override + public void setPrintEnable(boolean enable) { + this.printSqlEnable = enable; + } + + @Override + public boolean isPrintEnable(Config config) { + return printSqlEnable; + } - public void print(String sql); + @Override + public void print(String sql, Long tookTimeMillis) { + if (tookTimeMillis != null) { + log.debug("Jboot exec sql took " + tookTimeMillis + " ms >>> " + sql); + } else { + log.debug("Jboot exec sql >>> " + sql); + } + } + }; + + void setPrintEnable(boolean enable); + + boolean isPrintEnable(Config config); + + void print(String sql, Long tookTimeMillis); } + public interface SqlRunner { + V run() throws SQLException; + } } diff --git a/src/main/java/io/jboot/db/TableInfo.java b/src/main/java/io/jboot/db/TableInfo.java index 66eee8a7f2a5cc503dfece890f0e61d62009d3b8..eb8913b0f2698fafc4b44f54e5a9b76456585dee 100644 --- a/src/main/java/io/jboot/db/TableInfo.java +++ b/src/main/java/io/jboot/db/TableInfo.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,27 @@ package io.jboot.db; import com.jfinal.plugin.activerecord.Model; +import io.jboot.db.datasource.DataSourceConfig; +import io.jboot.utils.StrUtil; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.db */ public class TableInfo { private String tableName; private String primaryKey; private Class modelClass; - private String datasources; + private String datasource; + private Set datasourceNames; + + private List attachedDatasources; public String getTableName() { @@ -54,11 +63,70 @@ public class TableInfo { this.modelClass = modelClass; } - public String getDatasources() { - return datasources; + public String getDatasource() { + return datasource; + } + + public void setDatasource(String datasource) { + this.datasource = datasource; + } + + public Set getDatasourceNames() { + if (datasourceNames == null) { + datasourceNames = StrUtil.isNotBlank(datasource) + ? StrUtil.splitToSetByComma(datasource) + : new HashSet<>(); + } + return datasourceNames; + } + + + /** + * 添加数据源:让此表绑定数据源 + * + * @param dataSourceConfig + * @param fromDesignated 是否是通过 jboot.datasource.table 或者 @table(datasource="xxx") 来指定的 + */ + public boolean addAttachedDatasource(DataSourceConfig dataSourceConfig, boolean fromDesignated) { + if (this.attachedDatasources == null) { + this.attachedDatasources = new ArrayList<>(); + } + + // 只能添加到一个数据源,若 fromDesignated == false 的时候,直接返回 + if (!fromDesignated && !this.attachedDatasources.isEmpty()) { + return false; + } + + + // 若新添加的数据源,是配置了指定的表(亦或者表配置了指定的数据源), + // 那么需要移除那些未指定表的默认数据源 + if (fromDesignated && !this.attachedDatasources.isEmpty()) { + for (DataSourceConfigWrapper dataSourceConfigWrapper : attachedDatasources) { + if (!dataSourceConfigWrapper.fromDesignated) { + dataSourceConfigWrapper.dataSourceConfig.removeTableInfo(this); + } + } + attachedDatasources.removeIf(dataSourceConfigWrapper -> !dataSourceConfigWrapper.fromDesignated); + } + + + // 一张表只能绑定一个数据源,jfinal 的 TableMapping 决定的,默认情况下使用该数据源去进行增删改查 + if (attachedDatasources.isEmpty()) { + this.attachedDatasources.add(new DataSourceConfigWrapper(dataSourceConfig, fromDesignated)); + } + + return true; } - public void setDatasources(String datasources) { - this.datasources = datasources; + + public static class DataSourceConfigWrapper { + private final DataSourceConfig dataSourceConfig; + private final boolean fromDesignated; + + public DataSourceConfigWrapper(DataSourceConfig dataSourceConfig, boolean fromDesignated) { + this.dataSourceConfig = dataSourceConfig; + this.fromDesignated = fromDesignated; + } } + } diff --git a/src/main/java/io/jboot/db/TableInfoManager.java b/src/main/java/io/jboot/db/TableInfoManager.java index 65c4b11888fbc98ee5c64bfee3c75a1ba0c700a1..0686c79fd171cb9245d979bdb2b150e4d3f9f5e0 100644 --- a/src/main/java/io/jboot/db/TableInfoManager.java +++ b/src/main/java/io/jboot/db/TableInfoManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,14 +24,11 @@ import io.jboot.utils.ArrayUtil; import io.jboot.utils.ClassScanner; import io.jboot.utils.StrUtil; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; +import java.util.*; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.db */ public class TableInfoManager { @@ -46,46 +43,59 @@ public class TableInfoManager { /** - * 获取 某数据源 下匹配的表 + * 初始化该数据下的 tableInfos 对象,其用来存储该数据源下有哪些表 * * @param dataSourceConfig - * @return 该数据源下所有的表 */ - public List getMatchTablesInfos(DataSourceConfig dataSourceConfig) { + public void initConfigMappingTables(DataSourceConfig dataSourceConfig) { + // 该数据源下配置的所有表 Set configTables = StrUtil.isNotBlank(dataSourceConfig.getTable()) - ? StrUtil.splitToSet(dataSourceConfig.getTable(), ",") + ? StrUtil.splitToSetByComma(dataSourceConfig.getTable()) : null; + // 该数据源下排除的所有表 Set configExTables = StrUtil.isNotBlank(dataSourceConfig.getExTable()) - ? StrUtil.splitToSet(dataSourceConfig.getExTable(), ",") + ? StrUtil.splitToSetByComma(dataSourceConfig.getExTable()) : null; - List matchList = new ArrayList<>(); + // 所有的表信息 + List allTableInfos = getAllTableInfos(); - for (TableInfo tableInfo : getAllTableInfos()) { - //说明该表已经被指定到datasource了 - if (tableInfo.getDatasources() != null) { + for (TableInfo tableInfo : allTableInfos) { + + // 排除配置 jboot.datasource.extable 包含了这个表 + if (configExTables != null && configExTables.contains(tableInfo.getTableName())) { + continue; + } + + if (configTables != null && configTables.contains(tableInfo.getTableName())) { + dataSourceConfig.addTableInfo(tableInfo, true); + } + + if (tableInfo.getDatasourceNames().contains(dataSourceConfig.getName())) { + dataSourceConfig.addTableInfo(tableInfo, true); + } + + // 排除所有表,但允许当前数据源自己指定的表,指定的表不被排除 + if (configExTables != null && configExTables.contains("*")) { continue; } - // 如果 datasource.table 已经配置了, - // 就只用这个配置的,不是这个配置的都排除 - if (configTables != null && !configTables.contains(tableInfo.getTableName())) { + // 注解 @Table(datasource="xxxx") 指定了数据源,而且当前数据源未匹配 + if (tableInfo.getDatasourceNames().size() > 0) { continue; } - //被指定排除的表进行排除了 - if (configExTables != null && configExTables.contains(tableInfo.getTableName())) { + // 如果当前的数据源已经配置了绑定的表,且未当前表未命中,不让其他表添加到当前数据源 + if (configTables != null && configTables.size() > 0) { continue; } - tableInfo.setDatasources(dataSourceConfig.getName()); - matchList.add(tableInfo); + dataSourceConfig.addTableInfo(tableInfo, false); } - return matchList; } private List getAllTableInfos() { @@ -129,11 +139,12 @@ public class TableInfoManager { } - private void addTable(List tableInfoList, Class clazz, Table tb) { + private void addTable(List tableInfoList, Class modelClass, Table tb) { TableInfo tableInfo = new TableInfo(); - tableInfo.setModelClass(clazz); + tableInfo.setModelClass(modelClass); tableInfo.setPrimaryKey(AnnotationUtil.get(tb.primaryKey())); tableInfo.setTableName(AnnotationUtil.get(tb.tableName())); + tableInfo.setDatasource(AnnotationUtil.get(tb.datasource())); tableInfoList.add(tableInfo); } diff --git a/src/main/java/io/jboot/db/annotation/PriorDatasource.java b/src/main/java/io/jboot/db/annotation/PriorDatasource.java new file mode 100644 index 0000000000000000000000000000000000000000..d5c0cf3cefdf9e2a8c1c1a03c84fb921637577d8 --- /dev/null +++ b/src/main/java/io/jboot/db/annotation/PriorDatasource.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.annotation; + +import java.lang.annotation.*; + +/** + * @author Michael Yang 杨福海 (fuhai999@gmail.com) + *

+ * 用于配置,优先使用哪个数据源 + */ +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface PriorDatasource { + + String value(); + +} diff --git a/src/main/java/io/jboot/db/annotation/Table.java b/src/main/java/io/jboot/db/annotation/Table.java index fb4bf90522bc51af7d6962ad24e5585a66a07995..85932d66f20934343a88ef5497fb127b05eb5617 100644 --- a/src/main/java/io/jboot/db/annotation/Table.java +++ b/src/main/java/io/jboot/db/annotation/Table.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,4 +26,6 @@ public @interface Table { String primaryKey() default ""; + String datasource() default ""; + } \ No newline at end of file diff --git a/src/main/java/io/jboot/db/datasource/DataSourceBuilder.java b/src/main/java/io/jboot/db/datasource/DataSourceBuilder.java index 2f9d4a52b4679f22353d1c3bbd115f27265c9d4f..0dad07050c606d70331164061a201d37eb979fa9 100644 --- a/src/main/java/io/jboot/db/datasource/DataSourceBuilder.java +++ b/src/main/java/io/jboot/db/datasource/DataSourceBuilder.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import io.jboot.core.spi.JbootSpiLoader; import io.jboot.exception.JbootException; import io.jboot.support.seata.JbootSeataManager; import io.jboot.utils.StrUtil; -import org.apache.shardingsphere.shardingjdbc.api.yaml.YamlShardingDataSourceFactory; +import org.apache.shardingsphere.driver.api.yaml.YamlShardingSphereDataSourceFactory; import javax.sql.DataSource; import java.io.File; @@ -50,7 +50,8 @@ public class DataSourceBuilder { : new File(PathKit.getRootClassPath(), shardingConfigYaml); try { - return YamlShardingDataSourceFactory.createDataSource(yamlFile); +// return YamlShardingDataSourceFactory.createDataSource(yamlFile); + return YamlShardingSphereDataSourceFactory.createDataSource(yamlFile); } catch (Exception e) { throw new JbootException(e); } @@ -74,7 +75,7 @@ public class DataSourceBuilder { default: DataSourceFactory dataSourceFactory = JbootSpiLoader.load(DataSourceFactory.class, factory); if (dataSourceFactory == null) { - throw new NullPointerException("can not load DataSourceFactory spi for name : " + factory); + throw new NullPointerException("Can not load DataSourceFactory spi for name: " + factory); } return dataSourceFactory.createDataSource(dsc); } diff --git a/src/main/java/io/jboot/db/datasource/DataSourceConfig.java b/src/main/java/io/jboot/db/datasource/DataSourceConfig.java index 8c0b4e12e8da1568861a80e766f92b940ae3a75a..bff7ac1b790276ab7a723b5cbb3f55e6c1fdf66a 100644 --- a/src/main/java/io/jboot/db/datasource/DataSourceConfig.java +++ b/src/main/java/io/jboot/db/datasource/DataSourceConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,13 @@ package io.jboot.db.datasource; import com.jfinal.plugin.activerecord.DbKit; +import io.jboot.db.TableInfo; +import io.jboot.db.driver.DriverClassNames; import io.jboot.utils.StrUtil; +import java.util.ArrayList; +import java.util.List; + public class DataSourceConfig { public static final String NAME_DEFAULT = DbKit.MAIN_CONFIG_NAME; @@ -28,13 +33,17 @@ public class DataSourceConfig { public static final String TYPE_SQLITE = "sqlite"; public static final String TYPE_ANSISQL = "ansisql"; public static final String TYPE_POSTGRESQL = "postgresql"; + public static final String TYPE_DM = "dm"; + public static final String TYPE_CLICKHOUSE = "clickhouse"; + public static final String TYPE_INFORMIX = "informix"; + private String name; private String type = TYPE_MYSQL; private String url; private String user; private String password; - private String driverClassName = "com.mysql.jdbc.Driver"; + private String driverClassName; private String connectionInitSql; private String poolName; private boolean cachePrepStmts = true; @@ -46,6 +55,20 @@ public class DataSourceConfig { private Long idleTimeout; private Integer minimumIdle = 0; + // 配置获取连接等待超时的时间 + private long maxWait = -1; + + // 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + private long timeBetweenEvictionRunsMillis = 60 * 1000L; + // 配置连接在池中最小生存的时间 + private long minEvictableIdleTimeMillis = 1000L * 60L * 30L; + // 配置发生错误时多久重连 + private long timeBetweenConnectErrorMillis = 500; + private String validationQuery; + private boolean testWhileIdle = true; + private boolean testOnBorrow = false; + private boolean testOnReturn = false; + private String sqlTemplatePath; private String sqlTemplate; private String factory; //HikariDataSourceFactory.class.getName(); @@ -58,6 +81,7 @@ public class DataSourceConfig { private String table; //此数据源包含哪些表 private String exTable; //该数据源排除哪些表 + private String tablePrefix; //表前缀,假设 @Table(tableName="xxx"),那么实际表名为:tablePrefix + tableName private String dialectClass; private String activeRecordPluginClass; @@ -70,6 +94,7 @@ public class DataSourceConfig { */ private boolean needAddMapping = true; + public String getName() { return name; } @@ -111,7 +136,10 @@ public class DataSourceConfig { } public String getDriverClassName() { - return driverClassName; + if (StrUtil.isNotBlank(driverClassName)) { + return driverClassName; + } + return DriverClassNames.getDefaultDriverClass(getType()); } public void setDriverClassName(String driverClassName) { @@ -271,6 +299,14 @@ public class DataSourceConfig { this.exTable = exTable; } + public String getTablePrefix() { + return tablePrefix; + } + + public void setTablePrefix(String tablePrefix) { + this.tablePrefix = tablePrefix; + } + public Long getMaxLifetime() { return maxLifetime; } @@ -310,4 +346,114 @@ public class DataSourceConfig { public void setActiveRecordPluginClass(String activeRecordPluginClass) { this.activeRecordPluginClass = activeRecordPluginClass; } + + public long getMaxWait() { + return maxWait; + } + + public void setMaxWait(long maxWait) { + this.maxWait = maxWait; + } + + public long getTimeBetweenEvictionRunsMillis() { + return timeBetweenEvictionRunsMillis; + } + + public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) { + this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; + } + + public long getMinEvictableIdleTimeMillis() { + return minEvictableIdleTimeMillis; + } + + public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) { + this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis; + } + + public long getTimeBetweenConnectErrorMillis() { + return timeBetweenConnectErrorMillis; + } + + public void setTimeBetweenConnectErrorMillis(long timeBetweenConnectErrorMillis) { + this.timeBetweenConnectErrorMillis = timeBetweenConnectErrorMillis; + } + + public String getValidationQuery() { + if (validationQuery != null) { + return validationQuery; + } + if (this.url == null) { + return null; + } + String url = this.url.toLowerCase(); + if (url.startsWith("jdbc:oracle")) { + return "select 1 from dual"; + } else if (url.startsWith("jdbc:db2")) { + return "select 1 from sysibm.sysdummy1"; + } else if (url.startsWith("jdbc:hsqldb")) { + return "select 1 from INFORMATION_SCHEMA.SYSTEM_USERS"; + } else if (url.startsWith("jdbc:derby")) { + return "select 1 from INFORMATION_SCHEMA.SYSTEM_USERS"; + } + return "select 1"; + } + + public void setValidationQuery(String validationQuery) { + this.validationQuery = validationQuery; + } + + public boolean isTestWhileIdle() { + return testWhileIdle; + } + + public void setTestWhileIdle(boolean testWhileIdle) { + this.testWhileIdle = testWhileIdle; + } + + public boolean isTestOnBorrow() { + return testOnBorrow; + } + + public void setTestOnBorrow(boolean testOnBorrow) { + this.testOnBorrow = testOnBorrow; + } + + public boolean isTestOnReturn() { + return testOnReturn; + } + + public void setTestOnReturn(boolean testOnReturn) { + this.testOnReturn = testOnReturn; + } + + + private List tableInfos; + + /** + * 添加表信息 + * + * @param tableInfo 表信息 + * @param fromDesignated 是否是通过 jboot.datasource.table 或者 @table(datasource="xxx") 来指定的 + */ + public void addTableInfo(TableInfo tableInfo, boolean fromDesignated) { + if (tableInfos == null) { + tableInfos = new ArrayList<>(); + } + + if (!tableInfos.contains(tableInfo) && tableInfo.addAttachedDatasource(this, fromDesignated)) { + tableInfos.add(tableInfo); + } + } + + public void removeTableInfo(TableInfo tableInfo) { + if (tableInfos != null) { + tableInfos.remove(tableInfo); + } + } + + + public List getTableInfos() { + return tableInfos; + } } diff --git a/src/main/java/io/jboot/db/datasource/DataSourceConfigManager.java b/src/main/java/io/jboot/db/datasource/DataSourceConfigManager.java index bd147c6b14de35b2999a502dc03fa3e197f43f94..a7eec257de94dafcfa271fd3b30896274f36d1dc 100644 --- a/src/main/java/io/jboot/db/datasource/DataSourceConfigManager.java +++ b/src/main/java/io/jboot/db/datasource/DataSourceConfigManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,59 +15,35 @@ */ package io.jboot.db.datasource; -import com.google.common.collect.Maps; -import io.jboot.Jboot; -import io.jboot.app.config.JbootConfigManager; +import io.jboot.utils.ConfigUtil; import io.jboot.utils.StrUtil; -import java.util.*; +import java.util.HashMap; +import java.util.Map; public class DataSourceConfigManager { - private static final String DATASOURCE_PREFIX = "jboot.datasource."; - - private static DataSourceConfigManager manager = new DataSourceConfigManager(); public static DataSourceConfigManager me() { return manager; } - private Map datasourceConfigs = Maps.newHashMap(); + private Map datasourceConfigs = new HashMap<>(); private DataSourceConfigManager() { - - DataSourceConfig datasourceConfig = Jboot.config(DataSourceConfig.class, "jboot.datasource"); - - //若未配置数据源的名称,设置为默认 - if (StrUtil.isBlank(datasourceConfig.getName())) { - datasourceConfig.setName(DataSourceConfig.NAME_DEFAULT); - } - - addConfig(datasourceConfig); - - Properties prop = JbootConfigManager.me().getProperties(); - Set datasourceNames = new HashSet<>(); - for (Map.Entry entry : prop.entrySet()) { - if (entry.getKey() == null) { - continue; - } - String key = entry.getKey().toString().toLowerCase().replace('_','.'); - if (key.startsWith(DATASOURCE_PREFIX) && entry.getValue() != null) { - String[] keySplits = key.split("\\."); - if (keySplits.length == 4) { - datasourceNames.add(keySplits[2]); - } + Map configMap = ConfigUtil.getConfigModels(DataSourceConfig.class, "jboot.datasource"); + for (Map.Entry entry : configMap.entrySet()) { + DataSourceConfig config = entry.getValue(); + + //默认数据源 + if ("default".equals(entry.getKey()) && StrUtil.isBlank(config.getName())) { + config.setName(DataSourceConfig.NAME_DEFAULT); + } else if (StrUtil.isBlank(config.getName())) { + config.setName(entry.getKey()); } - } - - for (String name : datasourceNames) { - DataSourceConfig dsc = Jboot.config(DataSourceConfig.class, DATASOURCE_PREFIX + name); - if (StrUtil.isBlank(dsc.getName())) { - dsc.setName(name); - } - addConfig(dsc); + addConfig(config); } } @@ -81,7 +57,7 @@ public class DataSourceConfigManager { public Map getDatasourceConfigs() { - return datasourceConfigs; + return new HashMap<>(datasourceConfigs); } public DataSourceConfig getMainDatasourceConfig() { diff --git a/src/main/java/io/jboot/db/datasource/DataSourceFactory.java b/src/main/java/io/jboot/db/datasource/DataSourceFactory.java index 48148bc45510c1c02c071cb3a16597a837ddcf8e..0f3e99efb7005a36d279d6822c2225c1d72cb82e 100644 --- a/src/main/java/io/jboot/db/datasource/DataSourceFactory.java +++ b/src/main/java/io/jboot/db/datasource/DataSourceFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/db/datasource/DruidDataSourceFactory.java b/src/main/java/io/jboot/db/datasource/DruidDataSourceFactory.java index 871bd8a54e69ec83ef23b5e9423d620db737233a..eeca33247825e4027829b41a78703c3affc60c15 100644 --- a/src/main/java/io/jboot/db/datasource/DruidDataSourceFactory.java +++ b/src/main/java/io/jboot/db/datasource/DruidDataSourceFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import java.sql.SQLException; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.db.datasource */ public class DruidDataSourceFactory implements DataSourceFactory { @@ -40,7 +39,14 @@ public class DruidDataSourceFactory implements DataSourceFactory { druidDataSource.setPassword(config.getPassword()); druidDataSource.setDriverClassName(config.getDriverClassName()); druidDataSource.setMaxActive(config.getMaximumPoolSize()); - + druidDataSource.setMaxWait(config.getMaxWait()); + druidDataSource.setTimeBetweenEvictionRunsMillis(config.getTimeBetweenEvictionRunsMillis()); + druidDataSource.setMinEvictableIdleTimeMillis(config.getMinEvictableIdleTimeMillis()); + druidDataSource.setTimeBetweenConnectErrorMillis(config.getTimeBetweenConnectErrorMillis()); + druidDataSource.setValidationQuery(config.getValidationQuery()); + druidDataSource.setTestWhileIdle(config.isTestWhileIdle()); + druidDataSource.setTestOnBorrow(config.isTestOnBorrow()); + druidDataSource.setTestOnReturn(config.isTestOnReturn()); if (config.getMinimumIdle() != null) { druidDataSource.setMinIdle(config.getMinimumIdle()); } diff --git a/src/main/java/io/jboot/db/datasource/HikariDataSourceFactory.java b/src/main/java/io/jboot/db/datasource/HikariDataSourceFactory.java index f25a2ff699a099ba249e37131a98b8742d8406c5..92f3b26489f2e85ae5ee5c7874a74d772ead9c00 100644 --- a/src/main/java/io/jboot/db/datasource/HikariDataSourceFactory.java +++ b/src/main/java/io/jboot/db/datasource/HikariDataSourceFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import javax.sql.DataSource; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.db.datasource */ public class HikariDataSourceFactory implements DataSourceFactory { diff --git a/src/main/java/io/jboot/db/datasource/PriorDatasourceInterceptor.java b/src/main/java/io/jboot/db/datasource/PriorDatasourceInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..2874bcb556482b7453f1b2ff271549513c48dab2 --- /dev/null +++ b/src/main/java/io/jboot/db/datasource/PriorDatasourceInterceptor.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.datasource; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.db.JbootDb; +import io.jboot.db.annotation.PriorDatasource; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.StrUtil; + +import java.lang.reflect.Method; + +/** + * @author Michael Yang 杨福海 (fuhai999@gmail.com) + */ +@AutoLoad +public class PriorDatasourceInterceptor implements Interceptor, InterceptorBuilder { + + + @Override + public void intercept(Invocation inv) { + PriorDatasource datasource = getAnnotation(inv); + if (datasource != null) { + String configName = AnnotationUtil.get(datasource.value()); + if (StrUtil.isNotBlank(configName)) { + JbootDb.setCurrentConfigName(configName); + } + } + + inv.invoke(); + } + + + + private PriorDatasource getAnnotation(Invocation inv) { + PriorDatasource annotation = inv.getController().getClass().getAnnotation(PriorDatasource.class); + return annotation != null ? annotation : inv.getMethod().getAnnotation(PriorDatasource.class); + } + + + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.hasAnnotation(targetClass, PriorDatasource.class) || Util.hasAnnotation(method, PriorDatasource.class)) { + interceptors.add(this); + } + } +} diff --git a/src/main/java/io/jboot/db/dbpro/JbootDbPro.java b/src/main/java/io/jboot/db/dbpro/JbootDbPro.java index 251877a7df9ce4c35401015c8286bdeb345a84f2..21307bbee86398acf63b7547189ce76009c7d64d 100644 --- a/src/main/java/io/jboot/db/dbpro/JbootDbPro.java +++ b/src/main/java/io/jboot/db/dbpro/JbootDbPro.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package io.jboot.db.dbpro; +import com.jfinal.kit.StrKit; import com.jfinal.plugin.activerecord.*; import com.jfinal.plugin.activerecord.dialect.Dialect; import io.jboot.db.SqlDebugger; @@ -24,13 +25,14 @@ import io.jboot.db.model.Columns; import java.sql.*; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.db.dbpro */ public class JbootDbPro extends DbPro { + private static final String[] NO_PRIMARY_KEYS = new String[0]; public JbootDbPro() { } @@ -42,29 +44,26 @@ public class JbootDbPro extends DbPro { @Override - protected List find(Config config, Connection conn, String sql, Object... paras) throws SQLException { - SqlDebugger.debug(config, sql, paras); - return super.find(config, conn, sql, paras); + public List find(Config config, Connection conn, String sql, Object... paras) throws SQLException { + return SqlDebugger.run(() -> super.find(config, conn, sql, paras), config, sql, paras); } @Override - protected List query(Config config, Connection conn, String sql, Object... paras) throws SQLException { - SqlDebugger.debug(config, sql, paras); - return super.query(config, conn, sql, paras); + public List query(Config config, Connection conn, String sql, Object... paras) throws SQLException { + return SqlDebugger.run(() -> super.query(config, conn, sql, paras), config, sql, paras); } @Override public int update(Config config, Connection conn, String sql, Object... paras) throws SQLException { - SqlDebugger.debug(config, sql, paras); - return super.update(config, conn, sql, paras); + return SqlDebugger.run(() -> super.update(config, conn, sql, paras), config, sql, paras); } @Override protected boolean save(Config config, Connection conn, String tableName, String primaryKey, Record record) throws SQLException { - String[] pKeys = primaryKey.split(","); + String[] pKeys = StrKit.notBlank(primaryKey) ? primaryKey.split(",") : NO_PRIMARY_KEYS; List paras = new ArrayList(); StringBuilder sql = new StringBuilder(); @@ -72,27 +71,18 @@ public class JbootDbPro extends DbPro { dialect.forDbSave(tableName, pKeys, record, sql, paras); - PreparedStatement pst; - if (dialect.isOracle()) { - pst = conn.prepareStatement(sql.toString(), pKeys); - } else { - pst = conn.prepareStatement(sql.toString(), Statement.RETURN_GENERATED_KEYS); - } - dialect.fillStatement(pst, paras); - int result = pst.executeUpdate(); - dialect.getRecordGeneratedKey(pst, record, pKeys); - - if (pst != null) { - try { - pst.close(); - } catch (SQLException e) { - throw new ActiveRecordException(e); - } - } //add sql debug support - SqlDebugger.debug(config, sql.toString(), paras.toArray()); - - return result >= 1; + return SqlDebugger.run(() -> { + try (PreparedStatement pst = + dialect.isOracle() ? + conn.prepareStatement(sql.toString(), pKeys) : + conn.prepareStatement(sql.toString(), Statement.RETURN_GENERATED_KEYS)) { + dialect.fillStatement(pst, paras); + int result = pst.executeUpdate(); + dialect.getRecordGeneratedKey(pst, record, pKeys); + return result >= 1; + } + }, config, sql.toString(), paras.toArray()); } @@ -113,15 +103,52 @@ public class JbootDbPro extends DbPro { public List find(String tableName, Columns columns, String orderBy, Object limit) { JbootDialect dialect = (JbootDialect) getConfig().getDialect(); - String sql = dialect.forFindByColumns(null,tableName, "*", columns.getList(), orderBy, limit); + String sql = dialect.forFindByColumns(null, null, tableName, "*", columns.getList(), orderBy, limit); return columns.isEmpty() ? find(sql) : find(sql, columns.getValueArray()); } public int delete(String tableName, Columns columns) { JbootDialect dialect = (JbootDialect) getConfig().getDialect(); - String sql = dialect.forDeleteByColumns(tableName, columns.getList()); + String sql = dialect.forDeleteByColumns(null, null, tableName, columns.getList()); return columns.isEmpty() ? delete(sql) : delete(sql, columns.getValueArray()); } + @Override + public void each(Function func, String sql, Object... paras) { + //Connection conn = null; + // try { + // conn = config.getConnection(); + // + // try (PreparedStatement pst = conn.prepareStatement(sql)) { + // config.dialect.fillStatement(pst, paras); + // ResultSet rs = pst.executeQuery(); + // config.dialect.eachRecord(config, rs, func); + // DbKit.close(rs); + // } + // + // } catch (Exception e) { + // throw new ActiveRecordException(e); + // } finally { + // config.close(conn); + // } + + Dialect dialect = config.getDialect(); + try { + SqlDebugger.run(() -> { + try (Connection conn = config.getConnection(); + PreparedStatement pst = conn.prepareStatement(sql)) { + + dialect.fillStatement(pst, paras); + + try (ResultSet rs = pst.executeQuery();) { + dialect.eachRecord(config, rs, func); + } + } + return true; + }, config, sql, paras); + } catch (Exception e) { + throw new ActiveRecordException(e); + } + } } diff --git a/src/main/java/io/jboot/db/dbpro/JbootDbProFactory.java b/src/main/java/io/jboot/db/dbpro/JbootDbProFactory.java index 5a182e3fb4b4c3cb9443e1ea94ec3624be48f427..e1c60ef32047fc3543f7eae298208b82169cd68e 100644 --- a/src/main/java/io/jboot/db/dbpro/JbootDbProFactory.java +++ b/src/main/java/io/jboot/db/dbpro/JbootDbProFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import com.jfinal.plugin.activerecord.IDbProFactory; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.db */ public class JbootDbProFactory implements IDbProFactory { diff --git a/src/main/java/io/jboot/db/dialect/JbootAnsiSqlDialect.java b/src/main/java/io/jboot/db/dialect/JbootAnsiSqlDialect.java index 9c938b6217a8813ca92587ddce3af35fc62aa21b..ec6a19dc0a321604100692a4272069631341bf1b 100644 --- a/src/main/java/io/jboot/db/dialect/JbootAnsiSqlDialect.java +++ b/src/main/java/io/jboot/db/dialect/JbootAnsiSqlDialect.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ package io.jboot.db.dialect; import com.jfinal.plugin.activerecord.dialect.AnsiSqlDialect; import io.jboot.db.model.Column; -import io.jboot.db.model.SqlBuilder; import io.jboot.db.model.Join; +import io.jboot.db.model.SqlBuilder; import io.jboot.exception.JbootException; import java.util.List; @@ -28,8 +28,8 @@ public class JbootAnsiSqlDialect extends AnsiSqlDialect implements JbootDialect @Override - public String forFindByColumns(List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { - StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(joins, table, loadColumns, columns, orderBy, ' '); + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { + StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(alias, joins, table, loadColumns, columns, orderBy, ' '); if (limit != null) { throw new JbootException("limit param not finished JbootAnsiSqlDialect."); @@ -39,13 +39,13 @@ public class JbootAnsiSqlDialect extends AnsiSqlDialect implements JbootDialect } @Override - public String forFindCountByColumns(String table, List columns) { - return SqlBuilder.forFindCountByColumns(table, columns, ' '); + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns) { + return SqlBuilder.forFindCountByColumns(alias, joins, table, loadColumns, columns, ' '); } @Override - public String forDeleteByColumns(String table, List columns) { - return SqlBuilder.forDeleteByColumns(table, columns, ' '); + public String forDeleteByColumns(String alias, List joins, String table, List columns) { + return SqlBuilder.forDeleteByColumns(alias, joins, table, columns, ' '); } @@ -56,9 +56,14 @@ public class JbootAnsiSqlDialect extends AnsiSqlDialect implements JbootDialect @Override - public String forPaginateFrom(List joins, String table, List columns, String orderBy) { - return SqlBuilder.forPaginateFrom(joins, table, columns, orderBy, ' '); + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy) { + return SqlBuilder.forPaginateFrom(alias, joins, table, columns, orderBy, ' '); } + @Override + public String forPaginateTotalRow(String select, String sqlExceptSelect, Object ext) { + String distinctSql = SqlBuilder.forPaginateDistinctTotalRow(select, sqlExceptSelect, ext); + return distinctSql != null ? distinctSql : super.forPaginateTotalRow(select, sqlExceptSelect, ext); + } } diff --git a/src/main/java/io/jboot/db/dialect/JbootClickHouseDialect.java b/src/main/java/io/jboot/db/dialect/JbootClickHouseDialect.java new file mode 100644 index 0000000000000000000000000000000000000000..a720b79cb4ba3483b7f8cb70f8ab01e370fed486 --- /dev/null +++ b/src/main/java/io/jboot/db/dialect/JbootClickHouseDialect.java @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.dialect; + +import com.jfinal.plugin.activerecord.Model; +import com.jfinal.plugin.activerecord.Record; +import com.jfinal.plugin.activerecord.Table; +import com.jfinal.plugin.activerecord.dialect.AnsiSqlDialect; +import io.jboot.db.model.*; +import io.jboot.utils.StrUtil; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +public class JbootClickHouseDialect extends AnsiSqlDialect implements JbootDialect { + + @Override + public void getModelGeneratedKey(Model model, PreparedStatement pst, Table table) throws SQLException { + // doNothing() ; clickhouse 不支持生成主键 + } + + + @Override + public String forDbDeleteById(String tableName, String[] pKeys) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + StringBuilder sql = new StringBuilder("ALTER TABLE ").append(tableName).append(" DELETE WHERE "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" AND "); + } + sql.append(pKeys[i]).append(" = ?"); + } + return sql.toString(); + } + + + @Override + public String forModelDeleteById(Table table) { + String[] pKeys = table.getPrimaryKey(); + StringBuilder sql = new StringBuilder(45); + sql.append("ALTER TABLE "); + sql.append(table.getName()); + sql.append(" DELETE WHERE "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" AND "); + } + sql.append(pKeys[i]).append(" = ?"); + } + return sql.toString(); + } + + + @Override + public void forDbUpdate(String tableName, String[] pKeys, Object[] ids, Record record, StringBuilder sql, List paras) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + + sql.append("ALTER TABLE ").append(tableName).append(" UPDATE "); + for (Map.Entry e : record.getColumns().entrySet()) { + String colName = e.getKey(); + if (!isPrimaryKey(colName, pKeys)) { + if (paras.size() > 0) { + sql.append(", "); + } + sql.append(colName).append(" = ? "); + paras.add(e.getValue()); + } + } + sql.append(" WHERE "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" AND "); + } + sql.append(pKeys[i]).append(" = ?"); + paras.add(ids[i]); + } + } + + @Override + public void forModelUpdate(Table table, Map attrs, Set modifyFlag, StringBuilder sql, List paras) { + sql.append("ALTER TABLE ").append(table.getName()).append(" UPDATE "); + String[] pKeys = table.getPrimaryKey(); + for (Map.Entry e : attrs.entrySet()) { + String colName = e.getKey(); + if (modifyFlag.contains(colName) && !isPrimaryKey(colName, pKeys) && table.hasColumnLabel(colName)) { + if (paras.size() > 0) { + sql.append(", "); + } + sql.append(colName).append(" = ? "); + paras.add(e.getValue()); + } + } + sql.append(" WHERE "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" AND "); + } + sql.append(pKeys[i]).append(" = ?"); + paras.add(attrs.get(pKeys[i])); + } + } + + + @Override + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { + StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(alias, joins, table, loadColumns, columns, orderBy, ' '); + + if (limit != null) { + sqlBuilder.append(" LIMIT " + limit); + } + + return sqlBuilder.toString(); + } + + + @Override + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns) { + return SqlBuilder.forFindCountByColumns(alias, joins, table, loadColumns, columns, ' '); + } + + + @Override + public String forDeleteByColumns(String alias, List joins, String table, List columns) { + StringBuilder sqlBuilder = new StringBuilder(45); + sqlBuilder.append("ALTER TABLE ") + .append(table) + .append(" DELETE "); + +// SqlBuilder.buildAlias(sqlBuilder, alias); + SqlBuilder.buildJoinSql(sqlBuilder, joins, ' '); + SqlBuilder.buildWhereSql(sqlBuilder, columns, ' '); + + return sqlBuilder.toString(); + } + + + @Override + public String forPaginateSelect(String loadColumns) { + return "SELECT " + loadColumns; + } + + + @Override + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy) { + return SqlBuilder.forPaginateFrom(alias, joins, table, columns, orderBy, ' '); + } + + @Override + public String forPaginateTotalRow(String select, String sqlExceptSelect, Object ext) { + if (ext instanceof Model) { + if (io.jboot.db.model.CPI.hasAnyJoinEffective((JbootModel) ext)) { + String distinct = JbootModelExts.getDistinctColumn((JbootModel) ext); + if (StrUtil.isNotBlank(distinct)) { + return "SELECT count(DISTINCT " + distinct + ") " + replaceOrderBy(sqlExceptSelect); + } + } else { + String[] primaryKeys = com.jfinal.plugin.activerecord.CPI.getTable((Model) ext).getPrimaryKey(); + if (primaryKeys != null && primaryKeys.length == 1) { + return "select count(" + primaryKeys[0] + ") " + replaceOrderBy(sqlExceptSelect); + } + } + } + + + //return "select count(*) " + replaceOrderBy(sqlExceptSelect); + return super.forPaginateTotalRow(select, sqlExceptSelect, ext); + } +} diff --git a/src/main/java/io/jboot/db/dialect/JbootDialect.java b/src/main/java/io/jboot/db/dialect/JbootDialect.java index 6ba3f23c47b1a4a5f1e9631a8f2f56f5606f0759..b361fc636541d009c29228eb3de4270f87a17c21 100644 --- a/src/main/java/io/jboot/db/dialect/JbootDialect.java +++ b/src/main/java/io/jboot/db/dialect/JbootDialect.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,19 +24,19 @@ import java.util.List; public interface JbootDialect { - public String forFindByColumns(List joins, String table, String loadColumns, List columns, String orderBy, Object limit); + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit); - public String forFindCountByColumns(String table, List columns); + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns); - public String forDeleteByColumns(String table, List columns); + public String forDeleteByColumns(String alias, List joins, String table, List columns); public String forPaginateSelect(String loadColumns); - public String forPaginateFrom(List joins, String table, List columns, String orderBy); + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy); } diff --git a/src/main/java/io/jboot/db/dialect/JbootDmDialect.java b/src/main/java/io/jboot/db/dialect/JbootDmDialect.java new file mode 100644 index 0000000000000000000000000000000000000000..1395f52c26a9bdc434bff19756c6a250e98266f3 --- /dev/null +++ b/src/main/java/io/jboot/db/dialect/JbootDmDialect.java @@ -0,0 +1,290 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.dialect; + +import com.jfinal.plugin.activerecord.CPI; +import com.jfinal.plugin.activerecord.Record; +import com.jfinal.plugin.activerecord.Table; +import com.jfinal.plugin.activerecord.dialect.OracleDialect; +import io.jboot.db.model.Column; +import io.jboot.db.model.Join; +import io.jboot.db.model.SqlBuilder; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 达梦数据库的数据方言 + */ +public class JbootDmDialect extends OracleDialect implements JbootDialect { + + private static final char separator = '"'; + + public String wrap(String wrap) { + return "\"" + wrap.toUpperCase() + "\""; + } + + @Override + public String forTableBuilderDoBuild(String tableName) { + return toUpperCase("select * from " + wrap(tableName) + " where rownum < 1"); + } + + + @Override + // insert into table (id,name) values(seq.nextval, ?) + public void forModelSave(Table table, Map attrs, StringBuilder sql, List paras) { + sql.append("insert into ").append(wrap(table.getName())).append('('); + StringBuilder temp = new StringBuilder(") values("); + String[] pKeys = table.getPrimaryKey(); + int count = 0; + for (Map.Entry e : attrs.entrySet()) { + String colName = e.getKey(); + if (table.hasColumnLabel(colName)) { + Object value = e.getValue(); + if (isPrimaryKey(colName, pKeys) && value == null) { + continue; + } + + if (count++ > 0) { + sql.append(", "); + temp.append(", "); + } + + + if (isPrimaryKey(colName, pKeys) && value instanceof String && ((String) value).endsWith(".nextval")) { + sql.append(wrap(colName)); + temp.append(value); + } else { + sql.append(wrap(colName)); + temp.append('?'); + paras.add(value); + } + } + } + sql.append(temp).append(')'); + } + + @Override + public String forModelDeleteById(Table table) { + String[] pKeys = table.getPrimaryKey(); + StringBuilder sql = new StringBuilder(45); + sql.append("delete from "); + sql.append(wrap(table.getName())); + sql.append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(wrap(pKeys[i])).append(" = ?"); + } + return sql.toString(); + } + + @Override + public void forModelUpdate(Table table, Map attrs, Set modifyFlag, StringBuilder sql, List paras) { + sql.append("update ").append(wrap(table.getName())).append(" set "); + String[] pKeys = table.getPrimaryKey(); + for (Map.Entry e : attrs.entrySet()) { + String colName = e.getKey(); + if (modifyFlag.contains(colName) && !isPrimaryKey(colName, pKeys) && table.hasColumnLabel(colName)) { + if (paras.size() > 0) { + sql.append(", "); + } + sql.append(wrap(colName)).append(" = ? "); + paras.add(e.getValue()); + } + } + sql.append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(wrap(pKeys[i])).append(" = ?"); + paras.add(attrs.get(pKeys[i])); + } + } + + + @Override + public String forModelFindById(Table table, String columns) { + StringBuilder sql = new StringBuilder("select ").append(columns).append(" from "); + sql.append(wrap(table.getName())); + sql.append(" where "); + String[] pKeys = table.getPrimaryKey(); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(wrap(pKeys[i])).append(" = ?"); + } + return sql.toString(); + } + + + @Override + public String forDbFindById(String tableName, String[] pKeys) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + + StringBuilder sql = new StringBuilder("select * from ").append(wrap(tableName)).append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(wrap(pKeys[i])).append(" = ?"); + } + return sql.toString(); + } + + @Override + public String forDbDeleteById(String tableName, String[] pKeys) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + + StringBuilder sql = new StringBuilder("delete from ").append(wrap(tableName)).append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(wrap(pKeys[i])).append(" = ?"); + } + return sql.toString(); + } + + + @Override + public void forDbSave(String tableName, String[] pKeys, Record record, StringBuilder sql, List paras) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + + sql.append("insert into "); + sql.append(wrap(tableName)).append('('); + StringBuilder temp = new StringBuilder(); + temp.append(") values("); + + int count = 0; + for (Map.Entry e : record.getColumns().entrySet()) { + + String colName = e.getKey(); + Object value = e.getValue(); + + if (isPrimaryKey(colName, pKeys) && value == null) { + continue; + } + + if (count++ > 0) { + sql.append(", "); + temp.append(", "); + } + sql.append(wrap(colName)); + + if (value instanceof String && isPrimaryKey(colName, pKeys) && ((String) value).endsWith(".nextval")) { + temp.append(value); + } else { + temp.append('?'); + paras.add(value); + } + } + sql.append(temp).append(')'); + } + + + @Override + public void forDbUpdate(String tableName, String[] pKeys, Object[] ids, Record record, StringBuilder sql, List paras) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + + // Record 新增支持 modifyFlag + Set modifyFlag = CPI.getModifyFlag(record); + + sql.append("update ").append(wrap(tableName)).append(" set "); + for (Map.Entry e : record.getColumns().entrySet()) { + String colName = e.getKey(); + if (modifyFlag.contains(colName) && !isPrimaryKey(colName, pKeys)) { + if (paras.size() > 0) { + sql.append(", "); + } + sql.append(wrap(colName)).append(" = ? "); + paras.add(e.getValue()); + } + } + sql.append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(wrap(pKeys[i])).append(" = ?"); + paras.add(ids[i]); + } + } + + @Override + public String forPaginate(int pageNumber, int pageSize, StringBuilder findSql) { + int start = (pageNumber - 1) * pageSize; + int end = pageNumber * pageSize; + StringBuilder ret = new StringBuilder(); + ret.append("select * from ( select row_.*, rownum rownum_ from ( "); + ret.append(findSql); + ret.append(" ) row_ where rownum <= ").append(end).append(") table_alias"); + ret.append(" where table_alias.rownum_ > ").append(start); + return ret.toString(); + } + + + /////////////////jboot//////////// + + @Override + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { + StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(alias, joins, table, loadColumns, columns, orderBy, separator); + + if (limit != null) { + sqlBuilder.append(" LIMIT " + limit); + } + return toUpperCase(sqlBuilder.toString()); + } + + @Override + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns) { + return toUpperCase(SqlBuilder.forFindCountByColumns(alias, joins, table, loadColumns, columns, separator)); + } + + @Override + public String forDeleteByColumns(String alias, List joins, String table, List columns) { + return toUpperCase(SqlBuilder.forDeleteByColumns(alias, joins, table, columns, separator)); + } + + @Override + public String forPaginateSelect(String loadColumns) { + return toUpperCase("SELECT " + loadColumns); + } + + + @Override + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy) { + return toUpperCase(SqlBuilder.forPaginateFrom(alias, joins, table, columns, orderBy, separator)); + } + + @Override + public String forPaginateTotalRow(String select, String sqlExceptSelect, Object ext) { + String distinctSql = SqlBuilder.forPaginateDistinctTotalRow(select, sqlExceptSelect, ext); + return toUpperCase(distinctSql != null ? distinctSql : super.forPaginateTotalRow(select, sqlExceptSelect, ext)); + } + + public String toUpperCase(String sql) { + return sql.toUpperCase(); + } +} diff --git a/src/main/java/io/jboot/db/dialect/JbootInformixDialect.java b/src/main/java/io/jboot/db/dialect/JbootInformixDialect.java new file mode 100644 index 0000000000000000000000000000000000000000..69868c447bbdb00b77d2377a3d5fff66d71d8ac1 --- /dev/null +++ b/src/main/java/io/jboot/db/dialect/JbootInformixDialect.java @@ -0,0 +1,268 @@ +package io.jboot.db.dialect; + +import com.jfinal.plugin.activerecord.Record; +import com.jfinal.plugin.activerecord.Table; +import com.jfinal.plugin.activerecord.dialect.Dialect; +import io.jboot.db.model.Column; +import io.jboot.db.model.Join; +import io.jboot.db.model.SqlBuilder; +import io.jboot.exception.JbootException; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class JbootInformixDialect extends Dialect implements JbootDialect { + + + @Override + public String forTableBuilderDoBuild(String tableName) { + return "select * from " + tableName + " where 1 = 2"; + } + + @Override + public void forModelSave(Table table, Map attrs, StringBuilder sql, List paras) { + sql.append("insert into ").append(table.getName()).append('('); + StringBuilder temp = new StringBuilder(") values("); + for (Map.Entry e : attrs.entrySet()) { + String colName = e.getKey(); + if (table.hasColumnLabel(colName)) { + if (paras.size() > 0) { + sql.append(", "); + temp.append(", "); + } + sql.append(colName); + temp.append('?'); + paras.add(e.getValue()); + } + } + sql.append(temp).append(')'); + } + + @Override + public String forModelDeleteById(Table table) { + String[] pKeys = table.getPrimaryKey(); + StringBuilder sql = new StringBuilder(45); + sql.append("delete from "); + sql.append(table.getName()); + sql.append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(pKeys[i]).append(" = ?"); + } + return sql.toString(); + } + + @Override + public void forModelUpdate(Table table, Map attrs, Set modifyFlag, StringBuilder sql, List paras) { + sql.append("update ").append(table.getName()).append(" set "); + String[] pKeys = table.getPrimaryKey(); + for (Map.Entry e : attrs.entrySet()) { + String colName = e.getKey(); + if (modifyFlag.contains(colName) && !isPrimaryKey(colName, pKeys) && table.hasColumnLabel(colName)) { + if (paras.size() > 0) { + sql.append(", "); + } + sql.append(colName).append(" = ? "); + paras.add(e.getValue()); + } + } + sql.append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(pKeys[i]).append(" = ?"); + paras.add(attrs.get(pKeys[i])); + } + } + + @Override + public String forModelFindById(Table table, String columns) { + StringBuilder sql = new StringBuilder("select ").append(columns).append(" from "); + sql.append(table.getName()); + sql.append(" where "); + String[] pKeys = table.getPrimaryKey(); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(pKeys[i]).append(" = ?"); + } + return sql.toString(); + } + + @Override + public String forDbFindById(String tableName, String[] pKeys) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + + StringBuilder sql = new StringBuilder("select * from ").append(tableName).append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(pKeys[i]).append(" = ?"); + } + return sql.toString(); + } + + @Override + public String forDbDeleteById(String tableName, String[] pKeys) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + + StringBuilder sql = new StringBuilder("delete from ").append(tableName).append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(pKeys[i]).append(" = ?"); + } + return sql.toString(); + } + + @Override + public void forDbSave(String tableName, String[] pKeys, Record record, StringBuilder sql, List paras) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + + sql.append("insert into "); + sql.append(tableName).append('('); + StringBuilder temp = new StringBuilder(); + temp.append(") values("); + + for (Map.Entry e : record.getColumns().entrySet()) { + if (paras.size() > 0) { + sql.append(", "); + temp.append(", "); + } + sql.append(e.getKey()); + temp.append('?'); + paras.add(e.getValue()); + } + sql.append(temp).append(')'); + } + + @Override + public void forDbUpdate(String tableName, String[] pKeys, Object[] ids, Record record, StringBuilder sql, List paras) { + tableName = tableName.trim(); + trimPrimaryKeys(pKeys); + + sql.append("update ").append(tableName).append(" set "); + for (Map.Entry e : record.getColumns().entrySet()) { + String colName = e.getKey(); + if (!isPrimaryKey(colName, pKeys)) { + if (paras.size() > 0) { + sql.append(", "); + } + sql.append(colName).append(" = ? "); + paras.add(e.getValue()); + } + } + sql.append(" where "); + for (int i = 0; i < pKeys.length; i++) { + if (i > 0) { + sql.append(" and "); + } + sql.append(pKeys[i]).append(" = ?"); + paras.add(ids[i]); + } + } + + /** + * sql.replaceFirst("(?i)select", "") 正则中带有 "(?i)" 前缀,指定在匹配时不区分大小写 + */ + @Override + public String forPaginate(int pageNumber, int pageSize, StringBuilder findSql) { +// int end = pageNumber * pageSize; +// if (end <= 0) { +// end = pageSize; +// } + + int begin = (pageNumber - 1) * pageSize; + if (begin < 0) { + begin = 0; + } + +// StringBuilder ret = new StringBuilder(); +// ret.append(String.format("select skip %s first %s ", begin + "", pageSize + "")); +// ret.append(findSql.toString().replaceFirst("(?i)select", "")); + + StringBuilder ret = new StringBuilder("select skip "); + ret.append(begin).append(" first ").append(pageSize); + ret.append(findSql, 6, findSql.length()); + return ret.toString(); + } + + @Override + public void fillStatement(PreparedStatement pst, List paras) throws SQLException { + fillStatementHandleDateType(pst, paras); + } + + @Override + public void fillStatement(PreparedStatement pst, Object... paras) throws SQLException { + fillStatementHandleDateType(pst, paras); + } + + + //for jbootDialect ----------------- + @Override + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { + StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(alias, joins, table, loadColumns, columns, orderBy, ' '); + if (limit == null) { + return sqlBuilder.toString(); + } + + if (limit instanceof Number) { + StringBuilder ret = new StringBuilder("select first "); + ret.append(limit).append(" "); + ret.append(sqlBuilder, 6, sqlBuilder.length()); + return ret.toString(); + } else if (limit instanceof String && limit.toString().contains(",")) { + String[] startAndEnd = limit.toString().split(","); + String start = startAndEnd[0]; + String size = startAndEnd[1]; + + StringBuilder ret = new StringBuilder("select skip "); + ret.append(start).append(" first ").append(size); + ret.append(sqlBuilder, 6, sqlBuilder.length()); + return ret.toString(); + } else { + throw new JbootException("sql limit is error!,limit must is Number of String like \"0,10\""); + } + + } + + @Override + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns) { + return SqlBuilder.forFindCountByColumns(alias, joins, table, loadColumns, columns, ' '); + } + + @Override + public String forDeleteByColumns(String alias, List joins, String table, List columns) { + return SqlBuilder.forDeleteByColumns(alias, joins, table, columns, ' '); + } + + @Override + public String forPaginateSelect(String loadColumns) { + return "select " + loadColumns; + } + + + @Override + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy) { + return SqlBuilder.forPaginateFrom(alias, joins, table, columns, orderBy, ' '); + } + + @Override + public String forPaginateTotalRow(String select, String sqlExceptSelect, Object ext) { + String distinctSql = SqlBuilder.forPaginateDistinctTotalRow(select, sqlExceptSelect, ext); + return distinctSql != null ? distinctSql : super.forPaginateTotalRow(select, sqlExceptSelect, ext); + } + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/db/dialect/JbootMysqlDialect.java b/src/main/java/io/jboot/db/dialect/JbootMysqlDialect.java index f2c833331310cc4c387243e69fd78a888ab3d925..5b5def72de6ca82dfe05330b8c7b46779af782e6 100644 --- a/src/main/java/io/jboot/db/dialect/JbootMysqlDialect.java +++ b/src/main/java/io/jboot/db/dialect/JbootMysqlDialect.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,8 @@ import java.util.List; public class JbootMysqlDialect extends MysqlDialect implements JbootDialect { @Override - public String forFindByColumns(List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { - StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(joins, table, loadColumns, columns, orderBy, '`'); + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { + StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(alias, joins, table, loadColumns, columns, orderBy, '`'); if (limit != null) { sqlBuilder.append(" LIMIT " + limit); @@ -37,13 +37,13 @@ public class JbootMysqlDialect extends MysqlDialect implements JbootDialect { } @Override - public String forFindCountByColumns(String table, List columns) { - return SqlBuilder.forFindCountByColumns(table, columns, '`'); + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns) { + return SqlBuilder.forFindCountByColumns(alias, joins, table, loadColumns, columns, '`'); } @Override - public String forDeleteByColumns(String table, List columns) { - return SqlBuilder.forDeleteByColumns(table, columns, '`'); + public String forDeleteByColumns(String alias, List joins, String table, List columns) { + return SqlBuilder.forDeleteByColumns(alias, joins, table, columns, '`'); } @@ -54,8 +54,14 @@ public class JbootMysqlDialect extends MysqlDialect implements JbootDialect { @Override - public String forPaginateFrom(List joins, String table, List columns, String orderBy) { - return SqlBuilder.forPaginateFrom(joins, table, columns, orderBy, '`'); + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy) { + return SqlBuilder.forPaginateFrom(alias, joins, table, columns, orderBy, '`'); + } + + @Override + public String forPaginateTotalRow(String select, String sqlExceptSelect, Object ext) { + String distinctSql = SqlBuilder.forPaginateDistinctTotalRow(select, sqlExceptSelect, ext); + return distinctSql != null ? distinctSql : super.forPaginateTotalRow(select, sqlExceptSelect, ext); } } diff --git a/src/main/java/io/jboot/db/dialect/JbootOracleDialect.java b/src/main/java/io/jboot/db/dialect/JbootOracleDialect.java index b8e3228df3ba327c86563876015702da4fa7770c..940b21fff045ca3b6c63a767961e460e6de398e1 100644 --- a/src/main/java/io/jboot/db/dialect/JbootOracleDialect.java +++ b/src/main/java/io/jboot/db/dialect/JbootOracleDialect.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,8 +28,8 @@ public class JbootOracleDialect extends OracleDialect implements JbootDialect { @Override - public String forFindByColumns(List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { - StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(joins, table, loadColumns, columns, orderBy, ' '); + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { + StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(alias, joins, table, loadColumns, columns, orderBy, ' '); if (limit == null) { return sqlBuilder.toString(); @@ -59,13 +59,13 @@ public class JbootOracleDialect extends OracleDialect implements JbootDialect { } @Override - public String forFindCountByColumns(String table, List columns) { - return SqlBuilder.forFindCountByColumns(table, columns, ' '); + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns) { + return SqlBuilder.forFindCountByColumns(alias, joins, table, loadColumns, columns, ' '); } @Override - public String forDeleteByColumns(String table, List columns) { - return SqlBuilder.forDeleteByColumns(table,columns,' '); + public String forDeleteByColumns(String alias, List joins, String table, List columns) { + return SqlBuilder.forDeleteByColumns(alias, joins, table, columns, ' '); } @@ -76,8 +76,14 @@ public class JbootOracleDialect extends OracleDialect implements JbootDialect { @Override - public String forPaginateFrom(List joins,String table, List columns, String orderBy) { - return SqlBuilder.forPaginateFrom(joins, table, columns, orderBy, ' '); + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy) { + return SqlBuilder.forPaginateFrom(alias, joins, table, columns, orderBy, ' '); + } + + @Override + public String forPaginateTotalRow(String select, String sqlExceptSelect, Object ext) { + String distinctSql = SqlBuilder.forPaginateDistinctTotalRow(select, sqlExceptSelect, ext); + return distinctSql != null ? distinctSql : super.forPaginateTotalRow(select, sqlExceptSelect, ext); } diff --git a/src/main/java/io/jboot/db/dialect/JbootPostgreSqlDialect.java b/src/main/java/io/jboot/db/dialect/JbootPostgreSqlDialect.java index e4dfda37a055e58697e738ac434b0631065bac3b..dbec0a732a80a234b419115a87c5d806da24a1cf 100644 --- a/src/main/java/io/jboot/db/dialect/JbootPostgreSqlDialect.java +++ b/src/main/java/io/jboot/db/dialect/JbootPostgreSqlDialect.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,8 +28,8 @@ public class JbootPostgreSqlDialect extends PostgreSqlDialect implements JbootDi @Override - public String forFindByColumns(List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { - StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(joins, table, loadColumns, columns, orderBy, '"'); + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { + StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(alias, joins, table, loadColumns, columns, orderBy, '"'); if (limit == null) { return sqlBuilder.toString(); @@ -51,13 +51,13 @@ public class JbootPostgreSqlDialect extends PostgreSqlDialect implements JbootDi } @Override - public String forFindCountByColumns(String table, List columns) { - return SqlBuilder.forFindCountByColumns(table, columns, '"'); + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns) { + return SqlBuilder.forFindCountByColumns(alias, joins, table, loadColumns, columns, '"'); } @Override - public String forDeleteByColumns(String table, List columns) { - return SqlBuilder.forDeleteByColumns(table,columns,'"'); + public String forDeleteByColumns(String alias, List joins, String table, List columns) { + return SqlBuilder.forDeleteByColumns(alias, joins, table, columns, '"'); } @@ -68,9 +68,14 @@ public class JbootPostgreSqlDialect extends PostgreSqlDialect implements JbootDi @Override - public String forPaginateFrom(List joins,String table, List columns, String orderBy) { - return SqlBuilder.forPaginateFrom(joins, table, columns, orderBy, '"'); + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy) { + return SqlBuilder.forPaginateFrom(alias, joins, table, columns, orderBy, '"'); } + @Override + public String forPaginateTotalRow(String select, String sqlExceptSelect, Object ext) { + String distinctSql = SqlBuilder.forPaginateDistinctTotalRow(select, sqlExceptSelect, ext); + return distinctSql != null ? distinctSql : super.forPaginateTotalRow(select, sqlExceptSelect, ext); + } } diff --git a/src/main/java/io/jboot/db/dialect/JbootSqlServerDialect.java b/src/main/java/io/jboot/db/dialect/JbootSqlServerDialect.java index c29cd464c3c7a968268925de8e57369c57ae2980..cec4d976a3eecdd6947895c13bb0b533497f9bc5 100644 --- a/src/main/java/io/jboot/db/dialect/JbootSqlServerDialect.java +++ b/src/main/java/io/jboot/db/dialect/JbootSqlServerDialect.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,9 +28,9 @@ public class JbootSqlServerDialect extends SqlServerDialect implements JbootDial @Override - public String forFindByColumns(List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { - StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(joins, table, loadColumns, columns, orderBy, ' '); + StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(alias, joins, table, loadColumns, columns, orderBy, ' '); if (limit == null) { return sqlBuilder.toString(); @@ -63,13 +63,13 @@ public class JbootSqlServerDialect extends SqlServerDialect implements JbootDial } @Override - public String forFindCountByColumns(String table, List columns) { - return SqlBuilder.forFindCountByColumns(table, columns, ' '); + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns) { + return SqlBuilder.forFindCountByColumns(alias, joins, table, loadColumns, columns, ' '); } @Override - public String forDeleteByColumns(String table, List columns) { - return SqlBuilder.forDeleteByColumns(table, columns, ' '); + public String forDeleteByColumns(String alias, List joins, String table, List columns) { + return SqlBuilder.forDeleteByColumns(alias, joins, table, columns, ' '); } @@ -80,9 +80,14 @@ public class JbootSqlServerDialect extends SqlServerDialect implements JbootDial @Override - public String forPaginateFrom(List joins, String table, List columns, String orderBy) { - return SqlBuilder.forPaginateFrom(joins, table, columns, orderBy, ' '); + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy) { + return SqlBuilder.forPaginateFrom(alias, joins, table, columns, orderBy, ' '); } + @Override + public String forPaginateTotalRow(String select, String sqlExceptSelect, Object ext) { + String distinctSql = SqlBuilder.forPaginateDistinctTotalRow(select, sqlExceptSelect, ext); + return distinctSql != null ? distinctSql : super.forPaginateTotalRow(select, sqlExceptSelect, ext); + } } diff --git a/src/main/java/io/jboot/db/dialect/JbootSqlite3Dialect.java b/src/main/java/io/jboot/db/dialect/JbootSqlite3Dialect.java index 317c706a8369cb86e9e9be6c13e091ddff83727f..b4f792b8bf787d39e2eb4104f57bf9e39c4caf7c 100644 --- a/src/main/java/io/jboot/db/dialect/JbootSqlite3Dialect.java +++ b/src/main/java/io/jboot/db/dialect/JbootSqlite3Dialect.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,8 @@ public class JbootSqlite3Dialect extends Sqlite3Dialect implements JbootDialect @Override - public String forFindByColumns(List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { - StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(joins, table, loadColumns, columns, orderBy, ' '); + public String forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, Object limit) { + StringBuilder sqlBuilder = SqlBuilder.forFindByColumns(alias, joins, table, loadColumns, columns, orderBy, ' '); if (limit != null) { sqlBuilder.append(" LIMIT " + limit); @@ -38,13 +38,13 @@ public class JbootSqlite3Dialect extends Sqlite3Dialect implements JbootDialect } @Override - public String forFindCountByColumns(String table, List columns) { - return SqlBuilder.forFindCountByColumns(table, columns, ' '); + public String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns) { + return SqlBuilder.forFindCountByColumns(alias, joins, table, loadColumns, columns, ' '); } @Override - public String forDeleteByColumns(String table, List columns) { - return SqlBuilder.forDeleteByColumns(table, columns, ' '); + public String forDeleteByColumns(String alias, List joins, String table, List columns) { + return SqlBuilder.forDeleteByColumns(alias, joins, table, columns, ' '); } @@ -55,9 +55,14 @@ public class JbootSqlite3Dialect extends Sqlite3Dialect implements JbootDialect @Override - public String forPaginateFrom(List joins, String table, List columns, String orderBy) { - return SqlBuilder.forPaginateFrom(joins, table, columns, orderBy, ' '); + public String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy) { + return SqlBuilder.forPaginateFrom(alias, joins, table, columns, orderBy, ' '); } + @Override + public String forPaginateTotalRow(String select, String sqlExceptSelect, Object ext) { + String distinctSql = SqlBuilder.forPaginateDistinctTotalRow(select, sqlExceptSelect, ext); + return distinctSql != null ? distinctSql : super.forPaginateTotalRow(select, sqlExceptSelect, ext); + } } diff --git a/src/main/java/io/jboot/db/driver/DriverClassNames.java b/src/main/java/io/jboot/db/driver/DriverClassNames.java new file mode 100644 index 0000000000000000000000000000000000000000..f903886a1a8e44b359dd32a3d3b884dc9d3434f3 --- /dev/null +++ b/src/main/java/io/jboot/db/driver/DriverClassNames.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.driver; + +import io.jboot.db.datasource.DataSourceConfig; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.StrUtil; + +import java.util.HashMap; +import java.util.Map; + +public class DriverClassNames { + + private static final Map driverClassNames = new HashMap<>(); + + static { + driverClassNames.put(DataSourceConfig.TYPE_MYSQL, new String[]{"com.mysql.cj.jdbc.Driver", "com.mysql.jdbc.Driver"}); + driverClassNames.put(DataSourceConfig.TYPE_ORACLE, new String[]{"oracle.jdbc.driver.OracleDriver", "oracle.jdbc.OracleDriver"}); + driverClassNames.put(DataSourceConfig.TYPE_SQLSERVER, new String[]{"com.microsoft.sqlserver.jdbc.SQLServerDriver"}); + driverClassNames.put(DataSourceConfig.TYPE_SQLITE, new String[]{"org.sqlite.JDBC"}); + driverClassNames.put(DataSourceConfig.TYPE_POSTGRESQL, new String[]{"org.postgresql.Driver"}); + driverClassNames.put(DataSourceConfig.TYPE_DM, new String[]{"dm.jdbc.driver.DmDriver"}); + driverClassNames.put(DataSourceConfig.TYPE_CLICKHOUSE, new String[]{"com.github.housepower.jdbc.ClickHouseDriver", "ru.yandex.clickhouse.ClickHouseDriver"}); + driverClassNames.put(DataSourceConfig.TYPE_INFORMIX, new String[]{"com.informix.jdbc.IfxDriver"}); + } + + + /** + * Jboot 自己实现的驱动,比如 ClickHouse 为了适配 JFinal 做了一些驱动改动 + */ + private static final Map jbootDriverMapping = new HashMap<>(); + + static { + jbootDriverMapping.put("com.github.housepower.jdbc.ClickHouseDriver", "io.jboot.db.driver.NativeClickHouseDriver"); + jbootDriverMapping.put("ru.yandex.clickhouse.ClickHouseDriver", "io.jboot.db.driver.OfficialClickHouseDriver"); + } + + + /** + * 获取 默认的 jdbc 驱动类 + * + * @param type + * @return + */ + public static String getDefaultDriverClass(String type) { + String[] drivers = driverClassNames.get(type.toLowerCase()); + if (drivers == null || drivers.length == 0) { + return null; + } + + for (String driver : drivers) { + if (ClassUtil.hasClass(driver)) { + String jbootDriver = jbootDriverMapping.get(driver); + return StrUtil.isNotBlank(jbootDriver) ? jbootDriver : driver; + } + } + return null; + } + + +} diff --git a/src/main/java/io/jboot/db/driver/NativeClickHouseConnection.java b/src/main/java/io/jboot/db/driver/NativeClickHouseConnection.java new file mode 100644 index 0000000000000000000000000000000000000000..27735b76d4365bfd58352e4285f0ab7b9cb098b9 --- /dev/null +++ b/src/main/java/io/jboot/db/driver/NativeClickHouseConnection.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.driver; + +import com.github.housepower.jdbc.ClickHouseConnection; +import com.github.housepower.jdbc.connect.NativeClient; +import com.github.housepower.jdbc.connect.NativeContext; +import com.github.housepower.jdbc.misc.Validate; +import com.github.housepower.jdbc.protocol.HelloResponse; +import com.github.housepower.jdbc.protocol.QueryRequest; +import com.github.housepower.jdbc.settings.ClickHouseConfig; +import com.github.housepower.jdbc.settings.ClickHouseDefines; + +import java.net.InetSocketAddress; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.time.ZoneId; +import java.util.Locale; + +public class NativeClickHouseConnection extends ClickHouseConnection { + + protected NativeClickHouseConnection(ClickHouseConfig cfg, NativeContext nativeCtx) { + super(cfg, nativeCtx); + } + + + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { +// clickhouse 不支持 autoGeneratedKeys,但是 jfinal 调用了此方法会出错 + return prepareStatement(sql); + } + + + public static ClickHouseConnection createClickHouseConnection(ClickHouseConfig configure) throws SQLException { + return new NativeClickHouseConnection(configure, createNativeContext(configure)); + } + + private static NativeContext createNativeContext(ClickHouseConfig configure) throws SQLException { + NativeClient nativeClient = NativeClient.connect(configure); + return new NativeContext(clientContext(nativeClient, configure), serverContext(nativeClient, configure), nativeClient); + } + + private static QueryRequest.ClientContext clientContext(NativeClient nativeClient, ClickHouseConfig configure) throws SQLException { + Validate.isTrue(nativeClient.address() instanceof InetSocketAddress); + InetSocketAddress address = (InetSocketAddress) nativeClient.address(); + String clientName = String.format(Locale.ROOT, "%s %s", ClickHouseDefines.NAME, "client"); + String initialAddress = "[::ffff:127.0.0.1]:0"; + return new QueryRequest.ClientContext(initialAddress, address.getHostName(), clientName); + } + + private static NativeContext.ServerContext serverContext(NativeClient nativeClient, ClickHouseConfig configure) throws SQLException { + try { + long revision = ClickHouseDefines.CLIENT_REVISION; + nativeClient.sendHello("client", revision, configure.database(), configure.user(), configure.password()); + + HelloResponse response = nativeClient.receiveHello(configure.queryTimeout(), null); + ZoneId timeZone = ZoneId.of(response.serverTimeZone()); + return new NativeContext.ServerContext( + response.majorVersion(), response.minorVersion(), response.reversion(), + configure, timeZone, response.serverDisplayName()); + } catch (SQLException rethrows) { + nativeClient.silentDisconnect(); + throw rethrows; + } + } + +} diff --git a/src/main/java/io/jboot/app/config/Prop.java b/src/main/java/io/jboot/db/driver/NativeClickHouseDriver.java similarity index 35% rename from src/main/java/io/jboot/app/config/Prop.java rename to src/main/java/io/jboot/db/driver/NativeClickHouseDriver.java index bf850b53464566f2263e3f1a145c943aa29c2269..470e1d60d33d2b6e2905186009708eb9b366d520 100644 --- a/src/main/java/io/jboot/app/config/Prop.java +++ b/src/main/java/io/jboot/db/driver/NativeClickHouseDriver.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,44 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.app.config; +package io.jboot.db.driver; -import java.io.*; +import com.github.housepower.jdbc.ClickHouseConnection; +import com.github.housepower.jdbc.ClickHouseDriver; +import com.github.housepower.jdbc.settings.ClickHouseConfig; + +import java.sql.DriverManager; +import java.sql.SQLException; import java.util.Properties; +public class NativeClickHouseDriver extends ClickHouseDriver { + + static { + try { + DriverManager.registerDriver(new NativeClickHouseDriver()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } -class Prop { - protected Properties properties = null; - private static final String DEFAULT_ENCODING = "UTF-8"; + @Override + public ClickHouseConnection connect(String url, Properties properties) throws SQLException { + if (!this.acceptsURL(url)) { + return null; + } - public Prop(String fileName) { - this(fileName, DEFAULT_ENCODING); + ClickHouseConfig cfg = ClickHouseConfig.Builder.builder() + .withJdbcUrl(url) + .withProperties(properties) + .build(); + return connect(url, cfg); } - public Prop(String fileName, String encoding) { - InputStream inputStream = null; - try { - inputStream = Utils.getClassLoader().getResourceAsStream(fileName); - if (inputStream == null) { - throw new IllegalArgumentException("properties file not found in classpath, fileName : " + fileName); - } - properties = new Properties(); - properties.load(new InputStreamReader(inputStream, encoding)); - } catch (IOException e) { - throw new RuntimeException("error loading properties file.", e); - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } + ClickHouseConnection connect(String url, ClickHouseConfig cfg) throws SQLException { + if (!this.acceptsURL(url)) { + return null; } + return NativeClickHouseConnection.createClickHouseConnection(cfg.withJdbcUrl(url)); } - public Properties getProperties() { - return properties; - } } diff --git a/src/main/java/io/jboot/db/driver/OfficialClickHouseConnection.java b/src/main/java/io/jboot/db/driver/OfficialClickHouseConnection.java new file mode 100644 index 0000000000000000000000000000000000000000..9a002fdf42a8c6d4f76fdd478586a0e4ff3f3184 --- /dev/null +++ b/src/main/java/io/jboot/db/driver/OfficialClickHouseConnection.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.driver; + +import org.apache.http.impl.client.CloseableHttpClient; +import ru.yandex.clickhouse.ClickHouseConnectionImpl; +import ru.yandex.clickhouse.ClickhouseJdbcUrlParser; +import ru.yandex.clickhouse.settings.ClickHouseProperties; + +import java.lang.reflect.Field; +import java.net.URISyntaxException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +public class OfficialClickHouseConnection extends ClickHouseConnectionImpl { + + + private ClickHouseProperties properties; + + + public OfficialClickHouseConnection(String url) throws SQLException { + super(url); + try { + this.properties = ClickhouseJdbcUrlParser.parse(url,new Properties()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public OfficialClickHouseConnection(String url, ClickHouseProperties properties) throws SQLException { + super(url, properties); + try { + this.properties = ClickhouseJdbcUrlParser.parse(url, properties.asProperties()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { +// clickhouse 不支持 autoGeneratedKeys,但是 jfinal 调用了此方法会出错 + return prepareStatement(sql); + } + + + public void cleanConnections() { + try { + Field httpClientField = ClickHouseConnectionImpl.class.getDeclaredField("httpclient"); + httpClientField.setAccessible(true); + CloseableHttpClient httpclient = (CloseableHttpClient) httpClientField.get(this); + if (httpclient != null){ + httpclient.getConnectionManager().closeExpiredConnections(); + httpclient.getConnectionManager().closeIdleConnections(2 * properties.getSocketTimeout(), TimeUnit.MILLISECONDS); + } + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + +} diff --git a/src/main/java/io/jboot/db/driver/OfficialClickHouseDriver.java b/src/main/java/io/jboot/db/driver/OfficialClickHouseDriver.java new file mode 100644 index 0000000000000000000000000000000000000000..c6eb6005dc92840bcb88aa3eca45a0581ec60b3d --- /dev/null +++ b/src/main/java/io/jboot/db/driver/OfficialClickHouseDriver.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.driver; + +import com.google.common.collect.MapMaker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.yandex.clickhouse.ClickHouseConnection; +import ru.yandex.clickhouse.ClickHouseDriver; +import ru.yandex.clickhouse.settings.ClickHouseProperties; +import ru.yandex.clickhouse.util.LogProxy; + +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.concurrent.*; + +public class OfficialClickHouseDriver extends ClickHouseDriver { + + private static final Logger logger = LoggerFactory.getLogger(ClickHouseDriver.class); + private static final ConcurrentMap connections = new MapMaker().weakKeys().makeMap(); + + static { + try { + DriverManager.registerDriver(new OfficialClickHouseDriver()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public OfficialClickHouseDriver() { + // 10 seconds + scheduleConnectionsCleaning(10, TimeUnit.SECONDS); + } + + + @Override + public ClickHouseConnection connect(String url, ClickHouseProperties properties) throws SQLException { + if (!acceptsURL(url)) { + return null; + } + OfficialClickHouseConnection connection = new OfficialClickHouseConnection(url, properties); + registerConnection(connection); + return LogProxy.wrap(ClickHouseConnection.class, connection); + } + + private void registerConnection(OfficialClickHouseConnection connection) { + connections.put(connection, Boolean.TRUE); + } + + + /** + * Schedules connections cleaning at a rate. Turned off by default. + * See https://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/connmgmt.html#d5e418 + * + * @param rate period when checking would be performed + * @param timeUnit time unit of rate + */ + @Override + public void scheduleConnectionsCleaning(int rate, TimeUnit timeUnit) { + ScheduledConnectionCleaner.INSTANCE.scheduleAtFixedRate(() -> { + try { + for (OfficialClickHouseConnection connection : connections.keySet()) { + connection.cleanConnections(); + } + } catch (Exception e) { + logger.error("error evicting connections: " + e); + } + }, 0, rate, timeUnit); + } + + + static class ScheduledConnectionCleaner { + static final ScheduledExecutorService INSTANCE = Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory()); + + static class DaemonThreadFactory implements ThreadFactory { + @Override + public Thread newThread(Runnable r) { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setDaemon(true); + return thread; + } + } + } + + +} diff --git a/src/main/java/io/jboot/db/model/CPI.java b/src/main/java/io/jboot/db/model/CPI.java new file mode 100644 index 0000000000000000000000000000000000000000..d97b96c7754a1dfe50d3db2b7bfaf7541c2fd960 --- /dev/null +++ b/src/main/java/io/jboot/db/model/CPI.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.model; + +import com.jfinal.plugin.activerecord.Config; +import io.jboot.db.dialect.JbootDialect; + +public class CPI { + + public static boolean hasAnyJoinEffective(JbootModel dao) { + return dao.hasAnyJoinEffective(); + } + + + public static boolean hasColumn(JbootModel dao, String columnLabel) { + return dao._hasColumn(columnLabel); + } + + + public static JbootDialect getJbootDialect(JbootModel dao) { + return dao._getDialect(); + } + + + public static Config getModelConfig(JbootModel dao) { + return dao._getConfig(); + } + + + public static M loadByCache(JbootModel dao, Object... idValues) { + return (M) dao.loadByCache(idValues); + } + + + public static void safeDeleteCache(JbootModel dao, Object... idValues) { + dao.safeDeleteCache(idValues); + } + + + public static Class safeDeleteCache(JbootModel dao) { + return dao._getPrimaryType(); + } + + + public static String buildIdCacheName(JbootModel dao, String name) { + return dao.buildIdCacheName(name); + } + + + public static String buildIdCacheKey(JbootModel dao, Object... idValues) { + return dao.buildIdCacheKey(idValues); + } + + +} diff --git a/src/main/java/io/jboot/db/model/Column.java b/src/main/java/io/jboot/db/model/Column.java index 9703527cf5f1ec89fff6c2777ebde78b99022922..a0a242166a153093db4451d814179a465fedde9a 100644 --- a/src/main/java/io/jboot/db/model/Column.java +++ b/src/main/java/io/jboot/db/model/Column.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package io.jboot.db.model; +import io.jboot.utils.StrUtil; + import java.io.Serializable; public class Column implements Serializable { @@ -96,4 +98,11 @@ public class Column implements Serializable { && !LOGIC_IS_NOT_NULL.equals(getLogic()); } + public boolean checkAvailable() { + if (StrUtil.isBlank(getName())) { + return false; + } + return !hasPara() || getValue() != null; + } + } diff --git a/src/main/java/io/jboot/db/model/Columns.java b/src/main/java/io/jboot/db/model/Columns.java index 1013090e99f1bab7cfeeef4baaf9f85855e30bef..6c233deb2cea2ccfed0cd96e4add4606fc35e053 100644 --- a/src/main/java/io/jboot/db/model/Columns.java +++ b/src/main/java/io/jboot/db/model/Columns.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,7 @@ import io.jboot.db.dialect.JbootSqlServerDialect; import io.jboot.utils.StrUtil; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; +import java.util.*; /** * Column 的工具类,用于方便组装sql @@ -34,11 +31,35 @@ public class Columns implements Serializable { private List cols; + /** + * 在很多场景下,只会根据字段来查询,如果字段值为 null 的情况,Columns 会直接忽略 null 值,此时会造成结果不准确的情况 + *

+ * 比如 : + * ``` + * public ShopInfo findFirstByAccountId(BigInteger accountId) { + * return findFirstByColumns(Columns.create("account_id", accountId)); + * } + * ``` + * 根据账户 id 来查询该账户对应的 ShopInfo,此时 如果传入 null 值,则返回了 第一个 ShopInfo,这个 ShopInfo 可能并不是该账户的。 + *

+ * 在这种场景下,我们就不应该允许用户传入 null 值进行查询,当传入 null 的时候直接抛出异常即可 。 + *

+ * 此时,我们可以使用如下代码进行查询。 + *

+ * ``` + * public ShopInfo findFirstByAccountId(BigInteger accountId) { + * return findFirstByColumns(Columns.safeMode().eq("account_id", accountId)); + * } + * ``` + */ + private boolean useSafeMode = false; + public static Columns create() { return new Columns(); } + public static Columns create(Column column) { Columns that = new Columns(); that.add(column); @@ -58,16 +79,26 @@ public class Columns implements Serializable { } + public static Columns safeMode() { + return new Columns().useSafeMode(); + } + + + public static Columns safeCreate(String name, Object value) { + return safeMode().eq(name, value); + } + + /** * add new column in Columns * * @param column */ - public void add(Column column) { + public Columns add(Column column) { //do not add null value column if (column.hasPara() && column.getValue() == null) { - return; + return this; } if (this.cols == null) { @@ -75,11 +106,55 @@ public class Columns implements Serializable { } this.cols.add(column); + return this; + } + + + /** + * add new column in Columns + * + * @param column + */ + public Columns addToFirst(Column column) { + + //do not add null value column + if (column.hasPara() && column.getValue() == null) { + return this; + } + + if (this.cols == null) { + this.cols = new LinkedList<>(); + } + + this.cols.add(0, column); + return this; } - public Columns add(String name, Object value) { - return eq(name, value); + /** + * add Columns + * + * @param columns + * @return + */ + public Columns add(Columns columns) { + return append(columns); + } + + + /** + * add Columns To First + * + * @param columns + * @return + */ + public Columns addToFirst(Columns columns) { + if (columns != null && !columns.isEmpty()) { + for (Column column : columns.getList()) { + addToFirst(column); + } + } + return this; } @@ -91,8 +166,8 @@ public class Columns implements Serializable { * @return */ public Columns eq(String name, Object value) { - this.add(Column.create(name, value)); - return this; + Util.checkNullParas(this, name, value); + return add(Column.create(name, value)); } /** @@ -103,8 +178,8 @@ public class Columns implements Serializable { * @return */ public Columns ne(String name, Object value) { - this.add(Column.create(name, value, Column.LOGIC_NOT_EQUALS)); - return this; + Util.checkNullParas(this, name, value); + return add(Column.create(name, value, Column.LOGIC_NOT_EQUALS)); } @@ -116,8 +191,8 @@ public class Columns implements Serializable { * @return */ public Columns like(String name, Object value) { - this.add(Column.create(name, value, Column.LOGIC_LIKE)); - return this; + Util.checkNullParas(this, name, value); + return add(Column.create(name, value, Column.LOGIC_LIKE)); } /** @@ -128,12 +203,11 @@ public class Columns implements Serializable { * @return */ public Columns likeAppendPercent(String name, Object value) { - if (value == null || StrUtil.isBlank(value.toString())) { - //do nothing + Util.checkNullParas(this, name, value); + if (value == null || (value instanceof String && StrUtil.isBlank((String) value))) { return this; } - this.add(Column.create(name, "%" + value + "%", Column.LOGIC_LIKE)); - return this; + return add(Column.create(name, "%" + value + "%", Column.LOGIC_LIKE)); } /** @@ -144,8 +218,8 @@ public class Columns implements Serializable { * @return */ public Columns gt(String name, Object value) { - this.add(Column.create(name, value, Column.LOGIC_GT)); - return this; + Util.checkNullParas(this, name, value); + return add(Column.create(name, value, Column.LOGIC_GT)); } /** @@ -156,8 +230,8 @@ public class Columns implements Serializable { * @return */ public Columns ge(String name, Object value) { - this.add(Column.create(name, value, Column.LOGIC_GE)); - return this; + Util.checkNullParas(this, name, value); + return add(Column.create(name, value, Column.LOGIC_GE)); } /** @@ -168,8 +242,8 @@ public class Columns implements Serializable { * @return */ public Columns lt(String name, Object value) { - this.add(Column.create(name, value, Column.LOGIC_LT)); - return this; + Util.checkNullParas(this, name, value); + return add(Column.create(name, value, Column.LOGIC_LT)); } /** @@ -180,8 +254,8 @@ public class Columns implements Serializable { * @return */ public Columns le(String name, Object value) { - this.add(Column.create(name, value, Column.LOGIC_LE)); - return this; + Util.checkNullParas(this, name, value); + return add(Column.create(name, value, Column.LOGIC_LE)); } @@ -192,7 +266,19 @@ public class Columns implements Serializable { * @return */ public Columns isNull(String name) { - this.add(Column.create(name, null, Column.LOGIC_IS_NULL)); + return add(Column.create(name, null, Column.LOGIC_IS_NULL)); + } + + + /** + * @param name + * @param condition + * @return + */ + public Columns isNullIf(String name, Boolean condition) { + if (condition != null && condition) { + add(Column.create(name, null, Column.LOGIC_IS_NULL)); + } return this; } @@ -204,65 +290,328 @@ public class Columns implements Serializable { * @return */ public Columns isNotNull(String name) { - this.add(Column.create(name, null, Column.LOGIC_IS_NOT_NULL)); + return add(Column.create(name, null, Column.LOGIC_IS_NOT_NULL)); + } + + + /** + * IS NOT NULL + * + * @param name + * @param condition + * @return + */ + public Columns isNotNullIf(String name, Boolean condition) { + if (condition != null && condition) { + add(Column.create(name, null, Column.LOGIC_IS_NOT_NULL)); + } return this; } + /** + * in arrays + * + * @param name + * @param arrays + * @return + */ public Columns in(String name, Object... arrays) { - this.add(Column.create(name, arrays, Column.LOGIC_IN)); + Util.checkNullParas(this, name, arrays); + + //忽略 columns.in("name", null) 的情况 + if (arrays != null && arrays.length == 1 && arrays[0] == null) { + return this; + } + return add(Column.create(name, arrays, Column.LOGIC_IN)); + } + + + /** + * in Collection + * + * @param name + * @param collection + * @return + */ + public Columns in(String name, Collection collection) { + Util.checkNullParas(this, collection); + if (collection != null && !collection.isEmpty()) { + in(name, collection.toArray()); + } return this; } + /** + * not int arrays + * + * @param name + * @param arrays + * @return + */ public Columns notIn(String name, Object... arrays) { - this.add(Column.create(name, arrays, Column.LOGIC_NOT_IN)); + Util.checkNullParas(this, name, arrays); + + //忽略 columns.notIn("name", null) 的情况 + if (arrays != null && arrays.length == 1 && arrays[0] == null) { + return this; + } + return add(Column.create(name, arrays, Column.LOGIC_NOT_IN)); + } + + + /** + * not in Collection + * + * @param name + * @param collection + * @return + */ + public Columns notIn(String name, Collection collection) { + Util.checkNullParas(this, collection); + if (collection != null && !collection.isEmpty()) { + notIn(name, collection.toArray()); + } return this; } + /** + * between + * + * @param name + * @param start + * @param end + * @return + */ public Columns between(String name, Object start, Object end) { - this.add(Column.create(name, new Object[]{start, end}, Column.LOGIC_BETWEEN)); - return this; + Util.checkNullParas(this, name, start, end); + return add(Column.create(name, new Object[]{start, end}, Column.LOGIC_BETWEEN)); } + /** + * not between + * + * @param name + * @param start + * @param end + * @return + */ public Columns notBetween(String name, Object start, Object end) { - this.add(Column.create(name, new Object[]{start, end}, Column.LOGIC_NOT_BETWEEN)); - return this; + Util.checkNullParas(this, name, start, end); + return add(Column.create(name, new Object[]{start, end}, Column.LOGIC_NOT_BETWEEN)); } + /** + * group + * + * @param columns + * @return + */ public Columns group(Columns columns) { + if (columns == this) { + throw new IllegalArgumentException("Columns.group(...) need a new Columns"); + } if (!columns.isEmpty()) { - this.add(new Group(columns)); + add(new Group(columns)); } return this; } - public Columns string(String string) { - if (StrUtil.isNotBlank(string)) { - this.add(new Str(string)); + + /** + * @param columns + * @param condition + * @return + */ + public Columns groupIf(Columns columns, Boolean condition) { + if (columns == this) { + throw new IllegalArgumentException("Columns.group(...) need a new Columns"); + } + if (condition != null && condition && !columns.isEmpty()) { + add(new Group(columns)); + } + return this; + } + + /** + * @param name + * @return + */ + public Columns groupBy(String name) { + add(new GroupBy(name)); + return this; + } + + /** + * @param name + * @return + */ + public Columns having(String name) { + add(new Having(name)); + return this; + } + + + /** + * @param sql + * @return + */ + public Columns having(String sql, Object... paras) { + add(new Having(sql, paras)); + return this; + } + + + /** + * @param columns + * @return + */ + public Columns having(Columns columns) { + add(new Having(columns)); + return this; + } + + + /** + * customize string sql + * + * @param sql + * @return + */ + public Columns sqlPart(String sql) { + if (StrUtil.isNotBlank(sql)) { + add(new SqlPart(sql)); + } + return this; + } + + /** + * customize string sql + * + * @param sql + * @param paras + * @return + */ + public Columns sqlPart(String sql, Object... paras) { + Util.checkNullParas(this, paras); + if (StrUtil.isNotBlank(sql)) { + add(new SqlPart(sql, paras)); + } + return this; + } + + /** + * customize string sql + * + * @param sql + * @param condition + * @return + */ + public Columns sqlPartIf(String sql, Boolean condition) { + if (condition != null && condition && StrUtil.isNotBlank(sql)) { + add(new SqlPart(sql)); + } + return this; + } + + /** + * customize string sql + * + * @param sql + * @param condition + * @param paras + * @return + */ + public Columns sqlPartIf(String sql, Boolean condition, Object... paras) { + Util.checkNullParas(this, paras); + if (condition != null && condition && StrUtil.isNotBlank(sql)) { + add(new SqlPart(sql, paras)); + } + return this; + } + + /** + * customize string sql + * + * @param sql + * @return + */ + public Columns sqlPartWithoutLink(String sql) { + if (StrUtil.isNotBlank(sql)) { + add(new SqlPart(sql, true)); + } + return this; + } + + /** + * customize string sql + * + * @param sql + * @param paras + * @return + */ + public Columns sqlPartWithoutLink(String sql, Object... paras) { + Util.checkNullParas(this, paras); + if (StrUtil.isNotBlank(sql)) { + add(new SqlPart(sql, paras, true)); + } + return this; + } + + /** + * customize string sql + * + * @param sql + * @param condition + * @return + */ + public Columns sqlPartWithoutLinkIf(String sql, Boolean condition) { + if (condition != null && condition && StrUtil.isNotBlank(sql)) { + add(new SqlPart(sql, true)); + } + return this; + } + + /** + * customize string sql + * + * @param sql + * @param condition + * @param paras + * @return + */ + public Columns sqlPartWithoutLinkIf(String sql, Boolean condition, Object... paras) { + Util.checkNullParas(this, paras); + if (condition != null && condition && StrUtil.isNotBlank(sql)) { + add(new SqlPart(sql, paras, true)); } return this; } public Columns or() { - this.add(new Or()); + add(new Or()); return this; } public Columns ors(String name, String logic, Object... values) { + Util.checkNullParas(this, name, values); + + Columns columns = new Columns(); for (int i = 0; i < values.length; i++) { Object value = values[i]; if (value != null) { - this.add(Column.create(name, value, logic)); + columns.add(Column.create(name, value, logic)); if (i != values.length - 1) { - this.add(new Or()); + columns.add(new Or()); } } } - return this; + + return group(columns); } @@ -271,6 +620,49 @@ public class Columns implements Serializable { } + /** + * 追加 新的 columns + * + * @param columns + * @return + */ + public Columns append(Columns columns) { + if (columns != null && !columns.isEmpty()) { + for (Column column : columns.getList()) { + add(column); + } + } + return this; + } + + + /** + * 追加 新的 columns + * + * @param columns + * @return + */ + public Columns appendIf(Columns columns, Boolean condition) { + if (condition != null && condition) { + append(columns); + } + return this; + } + + public boolean isUseSafeMode() { + return useSafeMode; + } + + public Columns useSafeMode() { + this.useSafeMode = true; + return this; + } + + public Columns unUseSafeMode() { + this.useSafeMode = false; + return this; + } + public boolean isEmpty() { return cols == null || cols.isEmpty(); } @@ -285,6 +677,19 @@ public class Columns implements Serializable { return cols; } + public boolean containsName(String name) { + if (isEmpty()) { + return false; + } + + for (Column col : cols) { + if (col.getName() != null && col.getName().equals(name)) { + return true; + } + } + return false; + } + public String getCacheKey() { if (isEmpty()) { @@ -298,77 +703,61 @@ public class Columns implements Serializable { return s.toString(); } + private static final char SQL_CACHE_SEPARATOR = '-'; + private void buildCacheKey(StringBuilder s, List columns) { - for (Column column : columns) { + for (int i = 0; i < columns.size(); i++) { + + Column column = columns.get(i); + if (column instanceof Or) { - s.append("or").append("-"); + Column before = i > 0 ? columns.get(i - 1) : null; + if (before != null && !(before instanceof Or)) { + s.append("or").append(SQL_CACHE_SEPARATOR); + } } else if (column instanceof Group) { - s.append("("); + s.append('('); buildCacheKey(s, ((Group) column).getColumns().getList()); - s.append(")-"); - } else if (column instanceof Str) { - s.append(deleteWhitespace(((Str) column).getString())).append("-"); + s.append(')').append(SQL_CACHE_SEPARATOR); + } else if (column instanceof SqlPart) { + String sqlpart = ((SqlPart) column).getSql(); + Object value = column.getValue(); + if (value != null) { + if (value.getClass().isArray()) { + Object[] values = (Object[]) value; + for (Object v : values) { + sqlpart = Util.replaceSqlPara(sqlpart, v); + } + } else { + sqlpart = Util.replaceSqlPara(sqlpart, value); + } + } + s.append(Util.deleteWhitespace(sqlpart)).append(SQL_CACHE_SEPARATOR); } else { s.append(column.getName()) - .append("-") - .append(getLogicStr(column.getLogic())) - .append("-"); + .append(SQL_CACHE_SEPARATOR) + .append(getLogicString(column.getLogic())) + .append(SQL_CACHE_SEPARATOR); Object value = column.getValue(); if (value != null) { if (value.getClass().isArray()) { - s.append(array2String((Object[]) column.getValue())); + s.append(Util.array2String((Object[]) value)); } else { s.append(column.getValue()); } - s.append("-"); + s.append(SQL_CACHE_SEPARATOR); } } } s.deleteCharAt(s.length() - 1); } - private static String deleteWhitespace(String str) { - final int strLen = str.length(); - final char[] chs = new char[strLen]; - int count = 0; - for (int i = 0; i < strLen; i++) { - if (!Character.isWhitespace(str.charAt(i))) { - chs[count++] = str.charAt(i); - } - } - if (count == strLen) { - return str; - } - return new String(chs, 0, count); - } - - private static String array2String(Object[] a) { - if (a == null) { - return "null"; - } - - int iMax = a.length - 1; - if (iMax == -1) { - return "[]"; - } - - StringBuilder b = new StringBuilder(); - b.append('['); - for (int i = 0; ; i++) { - b.append(a[i]); - if (i == iMax) { - return b.append(']').toString(); - } - b.append("-"); - } - } - /** * @param logic * @return */ - private String getLogicStr(String logic) { + private static String getLogicString(String logic) { switch (logic) { case Column.LOGIC_LIKE: return "lk"; @@ -401,58 +790,116 @@ public class Columns implements Serializable { } } + /** - * 这个只是用于调试 + * 输出 where 后面的 sql 部分,风格是 mysql 的风格 SQL * * @return */ - public String toMysqlSql() { - JbootMysqlDialect dialect = new JbootMysqlDialect(); - return dialect.forFindByColumns(null, "table", "*", getList(), null, null); + public String toWherePartSql() { + return toWherePartSql('`', false); } - public String toSqlServerSql() { - JbootSqlServerDialect dialect = new JbootSqlServerDialect(); - return dialect.forFindByColumns(null, "table", "*", getList(), null, null); + + /** + * 输出 where 后面的 sql 部分,风格是 mysql 的风格 SQL + * + * @param withWhereKeyword 是否带上 where 关键字 + * @return + */ + public String toWherePartSql(boolean withWhereKeyword) { + return toWherePartSql('`', withWhereKeyword); } + + /** + * 输出 where 部分的 sql + * + * @param separator 字段分隔符 + * @param withWhereKeyword 是否带上 "where 关键字" + * @return + */ + public String toWherePartSql(char separator, boolean withWhereKeyword) { + StringBuilder sb = new StringBuilder(); + SqlBuilder.buildWhereSql(sb, getList(), separator, withWhereKeyword); + return sb.toString(); + } + + @Override public String toString() { - return getCacheKey(); + String cacheKey = getCacheKey(); + return StrUtil.isNotBlank(cacheKey) ? cacheKey : "{}"; } + public static void main(String[] args) { - Columns columns = Columns.create(); - System.out.println(columns.getCacheKey()); - columns.add("name", "zhangsan"); - System.out.println(columns.getCacheKey()); + Columns columns = Columns.create().useSafeMode().or().or().or().eq("aa", "bb").or().or().or().notIn("aaa", 123, 456, 789).like("titile", "a"); + columns.group(Columns.create().or().or().sqlPart("aa=bb")); + columns.group(Columns.create("aa", "bb").eq("cc", "dd") + .group(Columns.create("aa", "bb").eq("cc", "dd")) + .group(Columns.create("aa", "bb").eq("cc", "dd").group(Columns.create("aa", "bb").eq("cc", "dd")))); columns.ge("age", 10); - columns.string("user.id != 1"); - System.out.println(columns.getCacheKey()); - - columns.group(Columns.create().likeAppendPercent("name", "lisi").eq("age", 20)); - System.out.println(columns.getCacheKey()); + columns.or(); + columns.or(); + columns.or(); + columns.or(); + columns.sqlPart("user.id != ? and xxx= ?", 1, "abc2"); + columns.sqlPart("user.id != ? and xxx= ?", 1, "abc2"); columns.or(); + columns.or(); + columns.or(); + columns.group(Columns.create().likeAppendPercent("name", "null").or().or().or() + .eq("age", "18").eq("ddd", null)); - columns.group(Columns.create().isNotNull("price").isNull("nickname").group(Columns.create().in("name", "123", "123", "111").notIn("nickname", "aaa", "bbb"))); + columns.or(); + columns.or(); - System.out.println(columns.getCacheKey()); + columns.group(Columns.create().or().or().sqlPart("name = ?", "zhangsan")); + columns.or(); + columns.or(); columns.or(); columns.between("name", "123", "1233"); - System.out.println(columns.getCacheKey()); + columns.between("name", "123", "1233"); + columns.or(); +// columns.sqlPartWithoutLink("group by xxx"); + columns.groupBy("aaa").having(Columns.create("aaa", "bbb").ge("ccc", 111)); +// columns.or(); +// columns.or(); +// columns.or(); + + System.out.println(columns.getCacheKey()); System.out.println(Arrays.toString(columns.getValueArray())); System.out.println(columns.toMysqlSql()); - System.out.println(columns.toSqlServerSql()); + System.out.println("-----------"); + System.out.println(columns.toWherePartSql('"', true)); + } + + /** + * 这个只是用于调试 + * + * @return + */ + private String toMysqlSql() { JbootMysqlDialect dialect = new JbootMysqlDialect(); - System.out.println(dialect.forDeleteByColumns("table", columns.getList())); - System.out.println(dialect.forFindCountByColumns("table", columns.getList())); + return dialect.forFindByColumns(null, null, "table", "*", getList(), null, null); + } + + /** + * 这个只是用于调试 + * + * @return + */ + private String toSqlServerSql() { + JbootSqlServerDialect dialect = new JbootSqlServerDialect(); + return dialect.forFindByColumns(null, null, "table", "*", getList(), null, null); } } diff --git a/src/main/java/io/jboot/db/model/Group.java b/src/main/java/io/jboot/db/model/Group.java index 1060cc6e37bf86a21361f1bce30781e300a2114e..191f3350999599ebfab8076bd0f1b4005308a4bd 100644 --- a/src/main/java/io/jboot/db/model/Group.java +++ b/src/main/java/io/jboot/db/model/Group.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,15 @@ public class Group extends Column { @Override public boolean hasPara() { - return false; + return true; + } + + @Override + public Object getValue() { + if (columns == null || columns.isEmpty()) { + return null; + } else { + return Util.getValueArray(columns.getList()); + } } } diff --git a/src/main/java/io/jboot/db/model/Str.java b/src/main/java/io/jboot/db/model/GroupBy.java similarity index 60% rename from src/main/java/io/jboot/db/model/Str.java rename to src/main/java/io/jboot/db/model/GroupBy.java index e31b4ba0dfd248614b1c038e24a3bbc6d25c3f6f..fe38309db8138698eaaf1578081d1d7f5831281b 100644 --- a/src/main/java/io/jboot/db/model/Str.java +++ b/src/main/java/io/jboot/db/model/GroupBy.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,25 +15,24 @@ */ package io.jboot.db.model; +public class GroupBy extends SqlPart{ + private static final String KEYWORD = "GROUP BY "; -class Str extends Column { - - private String string; - - public Str(String string) { - this.string = string; + public GroupBy(String sql) { + super(sql); } - public String getString() { - return string; + public GroupBy(String sql, Object para) { + super(sql, para); } - public void setString(String string) { - this.string = string; + @Override + public String getSql() { + return KEYWORD + super.getSql(); } @Override - public boolean hasPara() { - return false; + public boolean isWithoutLink() { + return true; } } diff --git a/src/main/java/io/jboot/wechat/JbootAccessTokenCache.java b/src/main/java/io/jboot/db/model/Having.java similarity index 43% rename from src/main/java/io/jboot/wechat/JbootAccessTokenCache.java rename to src/main/java/io/jboot/db/model/Having.java index 2d2b033631db4c942cbf46ea3ac90580a70117ae..f1ce3f0dc4464b4ad272f473d9f0467c34d7c4c1 100644 --- a/src/main/java/io/jboot/wechat/JbootAccessTokenCache.java +++ b/src/main/java/io/jboot/db/model/Having.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -13,28 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.wechat; +package io.jboot.db.model; -import com.jfinal.weixin.sdk.cache.IAccessTokenCache; -import io.jboot.Jboot; +public class Having extends SqlPart{ + private static final String KEYWORD = "HAVING "; + private Columns columns; + public Having(Columns columns) { + super("",Util.getValueArray(columns.getList())); + this.columns = columns; + } -public class JbootAccessTokenCache implements IAccessTokenCache { + public Having(String sql) { + super(sql); + } - static final String cache_name = "__jboot_wechat_access_tokens"; + public Having(String sql, Object para) { + super(sql, para); + } @Override - public String get(String key) { - return Jboot.getCache().get(cache_name, key); + public String getSql() { + return KEYWORD + super.getSql(); } @Override - public void set(String key, String value) { - Jboot.getCache().put(cache_name, key, value); + public void build(char separator) { + if (this.columns != null){ + setSql(columns.toWherePartSql(separator,false)); + } } @Override - public void remove(String key) { - Jboot.getCache().remove(cache_name, key); + public boolean isWithoutLink() { + return true; } } diff --git a/src/main/java/io/jboot/db/model/JbootModel.java b/src/main/java/io/jboot/db/model/JbootModel.java index 010c735ad703482294824450ed509d9b2edfc38c..5b5645ebf2f8bdc0375efb1a70ae3eb8b5cc9e55 100644 --- a/src/main/java/io/jboot/db/model/JbootModel.java +++ b/src/main/java/io/jboot/db/model/JbootModel.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,21 @@ */ package io.jboot.db.model; +import com.jfinal.kit.LogKit; import com.jfinal.log.Log; import com.jfinal.plugin.activerecord.*; import com.jfinal.plugin.activerecord.dialect.Dialect; +import io.jboot.db.JbootDb; import io.jboot.db.SqlDebugger; import io.jboot.db.dialect.JbootDialect; import io.jboot.exception.JbootException; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.ClassUtil; import io.jboot.utils.StrUtil; -import java.lang.reflect.Array; import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.SQLException; import java.sql.Statement; import java.util.*; @@ -33,7 +37,6 @@ import java.util.*; /** * @author michael yang */ -@SuppressWarnings("serial") public class JbootModel> extends Model { private static final Log LOG = Log.getLog(JbootModel.class); @@ -45,6 +48,11 @@ public class JbootModel> extends Model { private static boolean idCacheEnable = config.isIdCacheEnable(); protected List joins = null; + String datasourceName = null; + String alias = null; + String loadColumns = null; + boolean isCopyModel = false; + public Joiner leftJoin(String table) { return joining(Join.TYPE_LEFT, table, true); @@ -78,9 +86,24 @@ public class JbootModel> extends Model { return joining(Join.TYPE_FULL, table, condition); } + /** + * set table alias in sql + * + * @param alias + * @return + */ + public M alias(String alias) { + if (StrUtil.isBlank(alias)) { + throw new IllegalArgumentException("alias must not be null or empty."); + } + M model = getOrCopyDao(); + model.alias = alias; + return model; + } + protected Joiner joining(String type, String table, boolean condition) { - M model = joins == null ? copy() : (M) this; + M model = getOrCopyDao(); if (model.joins == null) { model.joins = new LinkedList<>(); } @@ -91,7 +114,55 @@ public class JbootModel> extends Model { /** - * copy new model with all attrs + * set load columns in sql + * + * @param loadColumns + * @return + */ + public M loadColumns(String loadColumns) { + if (StrUtil.isBlank(loadColumns)) { + throw new IllegalArgumentException("loadColumns must not be null or empty."); + } + M model = getOrCopyDao(); + model.loadColumns = loadColumns; + return model; + } + + + public M distinct(String columnName) { + if (StrUtil.isBlank(columnName)) { + throw new IllegalArgumentException("columnName must not be null or empty."); + } + M dao = getOrCopyDao(); + JbootModelExts.setDistinctColumn(dao, columnName); + return dao; + } + + + private M getOrCopyDao() { + if (isCopyModel) { + return (M) this; + } else { + M dao = copy()._setConfigName(datasourceName); + dao.isCopyModel = true; + return dao; + } + } + + @Override + public M dao() { + put("__is_dao", true); + return (M) this; + } + + + private boolean isDaoModel() { + Boolean flag = getBoolean("__is_dao"); + return flag != null && flag; + } + + /** + * copy model with attrs or false * * @return */ @@ -100,8 +171,12 @@ public class JbootModel> extends Model { try { m = (M) _getUsefulClass().newInstance(); m.put(_getAttrs()); - } catch (Throwable e) { - e.printStackTrace(); + + for (String attr : _getModifyFlag()) { + m._getModifyFlag().add(attr); + } + } catch (Exception e) { + LOG.error(e.toString(), e); } return m; } @@ -123,37 +198,102 @@ public class JbootModel> extends Model { m.set(attrKey, o); } } - } catch (Throwable e) { - e.printStackTrace(); + } catch (Exception e) { + LOG.error(e.toString(), e); } return m; } /** - * 修复 jfinal use 可能造成的线程安全问题 + * 修复 JFinal use 造成的线程安全问题 * * @param configName * @return */ @Override public M use(String configName) { - M m = this.get(DATASOURCE_CACHE_PREFIX + configName); - if (m == null) { - synchronized (configName.intern()) { - m = this.get(DATASOURCE_CACHE_PREFIX + configName); - if (m == null) { - m = this.copy().superUse(configName); - this.put(DATASOURCE_CACHE_PREFIX + configName, m); - } + return use(configName, true); + } + + + /** + * 优先使用哪个数据源进行查询 + * + * @param configNames + * @return + */ + public M useFirst(String... configNames) { + if (configNames == null || configNames.length == 0) { + throw new IllegalArgumentException("configNames must not be null or empty."); + } + + for (String name : configNames) { + M newDao = use(name, false); + if (newDao != null) { + return newDao; } } - return m; + return (M) this; + } + + + private M use(String configName, boolean validateDatasourceExist) { + + //非 service 的 dao,例如 new User().user('ds').save()/upate() + if (!isDaoModel()) { + _setConfigName(configName); + return validDatasourceExist((M) this, validateDatasourceExist, configName); + } + + //定义在 service 中的 DAO + M newDao = JbootModelExts.getDatasourceDAO(this, DATASOURCE_CACHE_PREFIX + configName); + if (newDao == null) { + newDao = this.copy()._setConfigName(configName); + newDao = validDatasourceExist(newDao, validateDatasourceExist, configName); + if (newDao != null) { + JbootModelExts.setDatasourceDAO(this, DATASOURCE_CACHE_PREFIX + configName, newDao); + } + } + return newDao; + } + + + private M validDatasourceExist(M model, boolean valid, String configName) { + if (model._getConfig() == null) { + if (valid) { + throw new JbootIllegalConfigException("The datasource \"" + configName + "\" not config well, please config it in jboot.properties."); + } else { + return null; + } + } + return model; + } + + + M _setConfigName(String configName) { + this.datasourceName = configName; + return (M) this; } - M superUse(String configName) { - return super.use(configName); + @Override + protected Config _getConfig() { + if (datasourceName != null) { + return DbKit.getConfig(datasourceName); + } + + String currentConfigName = JbootDb.getCurrentConfigName(); + if (StrUtil.isNotBlank(currentConfigName)) { + Config config = DbKit.getConfig(currentConfigName); + if (config == null) { + LogKit.error("Can not use the datasource: {}, user default to replace.", currentConfigName); + } else { + return config; + } + } + + return DbKit.getConfig(_getUsefulClass()); } @@ -171,10 +311,13 @@ public class JbootModel> extends Model { set(column_created, new Date()); } - boolean needInitPrimaryKey = (String.class == _getPrimaryType() && null == get(_getPrimaryKey())); - - if (needInitPrimaryKey) { - set(_getPrimaryKey(), generatePrimaryValue()); + // 生成主键,只对单一主键的表生成,如果是多主键,不生成。 + String[] pkeys = _getPrimaryKeys(); + if (pkeys != null && pkeys.length == 1 && get(pkeys[0]) == null) { + Object value = config.getPrimarykeyValueGenerator().genValue(this, _getPrimaryType()); + if (value != null) { + set(pkeys[0], value); + } } @@ -184,41 +327,41 @@ public class JbootModel> extends Model { Table table = _getTable(); StringBuilder sql = new StringBuilder(); - List paras = new ArrayList(); + List paras = new ArrayList<>(); Dialect dialect = _getConfig().getDialect(); - dialect.forModelSave(table, _getAttrs(), sql, paras); - // if (paras.size() == 0) return false; // The sql "insert into tableName() values()" works fine, so delete this line - // -------- - Connection conn = null; - PreparedStatement pst = null; - int result = 0; try { - conn = config.getConnection(); - if (dialect.isOracle()) { - pst = conn.prepareStatement(sql.toString(), table.getPrimaryKey()); - } else { - pst = conn.prepareStatement(sql.toString(), Statement.RETURN_GENERATED_KEYS); - } - dialect.fillStatement(pst, paras); - result = pst.executeUpdate(); - dialect.getModelGeneratedKey(this, pst, table); - _getModifyFlag().clear(); - return result >= 1; - } catch (Exception e) { + return SqlDebugger.run(() -> { + Connection conn = null; + PreparedStatement pst = null; + int result = 0; + try { + conn = config.getConnection(); + if (dialect.isOracle()) { + pst = conn.prepareStatement(sql.toString(), table.getPrimaryKey()); + } else { + pst = conn.prepareStatement(sql.toString(), Statement.RETURN_GENERATED_KEYS); + } + dialect.fillStatement(pst, paras); + result = pst.executeUpdate(); + dialect.getModelGeneratedKey(this, pst, table); + _getModifyFlag().clear(); + return result >= 1; + } finally { + config.close(pst, conn); + } + }, config, sql.toString(), paras.toArray()); + } catch (SQLException e) { throw new ActiveRecordException(e); - } finally { - //add sqlDebugger print sql - SqlDebugger.debug(config, sql.toString(), paras.toArray()); - config.close(pst, conn); } } - protected String generatePrimaryValue() { - return StrUtil.uuid(); + @Override + protected void filter(int filterBy) { + config.getFilter().filter(this, filterBy); } @Override @@ -229,6 +372,20 @@ public class JbootModel> extends Model { return idCacheEnable ? loadByCache(idValue) : super.findById(idValue); } + /** + * 直接查询数据库,不走缓存 + * + * @param idValue + * @return + */ + public M findByIdWithoutCache(Object idValue) { + if (idValue == null) { + return null; + } + return super.findById(idValue); + } + + @Override public M findByIds(Object... idValues) { if (idValues == null) { @@ -240,25 +397,45 @@ public class JbootModel> extends Model { return idCacheEnable ? loadByCache(idValues) : super.findByIds(idValues); } + /** + * 直接查询数据库,不走缓存 + * + * @param idValues + * @return + */ + public M findByIdsWithoutCache(Object... idValues) { + if (idValues == null) { + return null; + } + if (idValues.length != _getPrimaryKeys().length) { + throw new IllegalArgumentException("idValues.length != _getPrimaryKeys().length"); + } + return super.findByIds(idValues); + } + + protected M loadByCache(Object... idValues) { try { - return config.getCache().get(_getTableName() - , buildCacheKey(idValues) + M m = config.getIdCache().get(buildIdCacheName(_getTableName()) + , buildIdCacheKey(idValues) , () -> JbootModel.super.findByIds(idValues) , config.getIdCacheTime()); + return m != null && config.isIdCacheByCopyEnable() ? m.copy() : m; } catch (Exception ex) { - LOG.error(ex.toString(), ex); + LOG.error("Jboot load model [" + ClassUtil.getUsefulClass(getClass()) + "] by cache is error, safe deleted it in cache.", ex); safeDeleteCache(idValues); } + return JbootModel.super.findByIds(idValues); } + protected void safeDeleteCache(Object... idValues) { try { - config.getCache().remove(_getTableName() - , buildCacheKey(idValues)); + config.getIdCache().remove(buildIdCacheName(_getTableName()) + , buildIdCacheKey(idValues)); } catch (Exception ex) { - LOG.error(ex.toString(), ex); + LOG.error("Remove cache is error by name [" + buildIdCacheName(_getTableName()) + "] and key [" + buildIdCacheKey(idValues) + "]", ex); } } @@ -292,18 +469,32 @@ public class JbootModel> extends Model { public boolean deleteByColumn(Column column) { - return deleteByColumns(Arrays.asList(column)); + if (column == null || !column.checkAvailable()) { + throw new IllegalArgumentException("Column or value must not be null."); + } + return deleteByColumns(Columns.create(column)); } public boolean deleteByColumns(Columns columns) { - return deleteByColumns(columns.getList()); + processColumns(columns, "delete"); + + if (columns.isEmpty()) { + throw new IllegalArgumentException("Columns must not be null or empty."); + } + String sql = _getDialect().forDeleteByColumns(alias, joins, _getTableName(), columns.getList()); + return Db.use(_getConfig().getName()).update(sql, Util.getValueArray(columns.getList())) >= 1; } - public boolean deleteByColumns(List columns) { - String sql = _getDialect().forDeleteByColumns(_getTableName(), columns); - return Db.use(_getConfig().getName()).update(sql, Util.getValueArray(columns)) >= 1; + public boolean deleteAll() { + Columns columns = Columns.create(); + + //通过 processColumns 可以重构 deleteAll 的行为 + processColumns(columns, "deleteAll"); + + String sql = _getDialect().forDeleteByColumns(alias, joins, _getTableName(), columns.getList()); + return Db.use(_getConfig().getName()).update(sql, Util.getValueArray(columns.getList())) >= 1; } @@ -355,7 +546,11 @@ public class JbootModel> extends Model { } - private static String buildCacheKey(Object... idValues) { + protected String buildIdCacheName(String orginal) { + return orginal; + } + + protected String buildIdCacheKey(Object... idValues) { if (idValues == null || idValues.length == 0) { return null; } @@ -377,12 +572,23 @@ public class JbootModel> extends Model { protected JbootDialect _getDialect() { Config config = _getConfig(); if (config == null) { + return throwCannotMappingException(); + } + return (JbootDialect) config.getDialect(); + } + + + private JbootDialect throwCannotMappingException() { + io.jboot.db.annotation.Table annotation = this.getClass().getAnnotation(io.jboot.db.annotation.Table.class); + if (annotation != null && StrUtil.isNotBlank(annotation.datasource())) { throw new JbootException( - String.format("class %s can not mapping to database table, maybe cannot connect to database. " + String.format("Model \"%s\" can not mapping to datasource: " + annotation.datasource() + , _getUsefulClass().getName())); + } else { + throw new JbootException( + String.format("Model \"%s\" can not mapping to database table, maybe application cannot connect to database. " , _getUsefulClass().getName())); - } - return (JbootDialect) config.getDialect(); } @@ -396,11 +602,19 @@ public class JbootModel> extends Model { } public M findFirstByColumn(Column column) { + if (column == null || !column.checkAvailable()) { +// throw new IllegalArgumentException("Column or value must not be null."); + return null; + } return findFirstByColumns(Columns.create(column)); } public M findFirstByColumn(Column column, String orderBy) { + if (column == null || !column.checkAvailable()) { +// throw new IllegalArgumentException("Column or value must not be null."); + return null; + } return findFirstByColumns(Columns.create(column), orderBy); } @@ -411,17 +625,71 @@ public class JbootModel> extends Model { public M findFirstByColumns(Columns columns, String orderby) { - return findFirstByColumns(columns, orderby, "*"); + return findFirstByColumns(columns, orderby, null); } public M findFirstByColumns(Columns columns, String orderby, String loadColumns) { - String sql = _getDialect().forFindByColumns(joins, _getTableName(), loadColumns, columns.getList(), orderby, 1); + processColumns(columns, "findFirst"); + if (StrUtil.isBlank(loadColumns) && this.loadColumns != null) { + loadColumns = this.loadColumns; + } + if (StrUtil.isBlank(loadColumns)) { + loadColumns = "*"; + } + String sql = _getDialect().forFindByColumns(alias, joins, _getTableName(), loadColumns, columns.getList(), orderby, 1); return columns.isEmpty() ? findFirst(sql) : findFirst(sql, columns.getValueArray()); } public List findListByIds(Object... ids) { - return findListByColumns(Columns.create().in(_getPrimaryKey(), ids)); + if (ids == null || ids.length == 0) { + return null; + } + + List list = new ArrayList<>(); + for (Object id : ids) { + if (id.getClass() == int[].class) { + findListByIds(list, (int[]) id); + } else if (id.getClass() == long[].class) { + findListByIds(list, (long[]) id); + } else if (id.getClass() == short[].class) { + findListByIds(list, (short[]) id); + } else { + M model = findById(id); + if (model != null) { + list.add(model); + } + } + } + return list; + } + + private void findListByIds(List list, int[] ids) { + for (int id : ids) { + M model = findById(id); + if (model != null) { + list.add(model); + } + } + } + + private void findListByIds(List list, long[] ids) { + for (long id : ids) { + M model = findById(id); + if (model != null) { + list.add(model); + } + } + } + + + private void findListByIds(List list, short[] ids) { + for (short id : ids) { + M model = findById(id); + if (model != null) { + list.add(model); + } + } } @@ -457,6 +725,9 @@ public class JbootModel> extends Model { } public List findListByColumn(Column column, String orderBy, Integer count) { + if (column == null || !column.checkAvailable()) { + return null; + } return findListByColumns(Columns.create(column), orderBy, count); } @@ -490,15 +761,78 @@ public class JbootModel> extends Model { } public List findListByColumns(Columns columns, String orderBy, Integer count) { - return findListByColumns(columns, orderBy, count, "*"); + return findListByColumns(columns, orderBy, count, null); } public List findListByColumns(Columns columns, String orderBy, Integer count, String loadColumns) { - String sql = _getDialect().forFindByColumns(joins, _getTableName(), loadColumns, columns.getList(), orderBy, count); + processColumns(columns, "findList"); + loadColumns = getLoadColumns(loadColumns); + String sql = _getDialect().forFindByColumns(alias, joins, _getTableName(), loadColumns, columns.getList(), orderBy, count); return columns.isEmpty() ? find(sql) : find(sql, columns.getValueArray()); } + //方便在某些场景下,对 columns 进行二次加工 + protected void processColumns(Columns columns, String action) { + } + + @Override + protected Class _getUsefulClass() { + Class c = getClass(); + // guice : Model$$EnhancerByGuice$$40471411 + // cglib : com.demo.blog.Blog$$EnhancerByCGLIB$$69a17158 + // return c.getName().indexOf("EnhancerByCGLIB") == -1 ? c : c.getSuperclass(); + // return c.getName().indexOf("$$EnhancerBy") == -1 ? c : c.getSuperclass(); + + //不支持匿名类,匿名无法被创建 + return c.getName().indexOf("$") == -1 ? c : c.getSuperclass(); + } + + private String getLoadColumns(String loadColumns) { + if (StrUtil.isBlank(loadColumns) && StrUtil.isNotBlank(this.loadColumns)) { + loadColumns = this.loadColumns; + } + + //使用 join 的情况下,需要判断 distinct + if (hasAnyJoinEffective()) { + String distinctColumn = JbootModelExts.getDistinctColumn(this); + + //用户配置了 distinct + if (StrUtil.isNotBlank(distinctColumn)) { + if (StrUtil.isBlank(loadColumns)) { + loadColumns = (StrUtil.isNotBlank(alias) ? alias : _getTableName()) + ".*"; + } + + //用户配置的 loadColumns 未包含 distinct 关键字 + if (!loadColumns.toLowerCase().contains("distinct ")) { + loadColumns = "DISTINCT " + distinctColumn + ", " + loadColumns; + } + } + } + + if (StrUtil.isBlank(loadColumns)) { + loadColumns = "*"; + } + + return loadColumns; + } + + + boolean hasAnyJoinEffective() { + if (joins == null || joins.size() == 0) { + return false; + } + + for (Join join : joins) { + if (join.isEffective()) { + return true; + } + } + + return false; + } + + public Page paginate(int pageNumber, int pageSize) { return paginateByColumns(pageNumber, pageSize, Columns.create(), null); } @@ -535,16 +869,39 @@ public class JbootModel> extends Model { public Page paginateByColumns(int pageNumber, int pageSize, Columns columns, String orderBy) { - return paginateByColumns(pageNumber, pageSize, columns, orderBy, "*"); + return paginateByColumns(pageNumber, pageSize, columns, orderBy, null); } public Page paginateByColumns(int pageNumber, int pageSize, Columns columns, String orderBy, String loadColumns) { + processColumns(columns, "paginate"); + + loadColumns = getLoadColumns(loadColumns); + + String selectPartSql = _getDialect().forPaginateSelect(loadColumns); - String fromPartSql = _getDialect().forPaginateFrom(joins, _getTableName(), columns.getList(), orderBy); + String fromPartSql = _getDialect().forPaginateFrom(alias, joins, _getTableName(), columns.getList(), orderBy); - return columns.isEmpty() - ? paginate(pageNumber, pageSize, selectPartSql, fromPartSql) - : paginate(pageNumber, pageSize, selectPartSql, fromPartSql, columns.getValueArray()); +// return columns.isEmpty() +// ? paginate(pageNumber, pageSize, selectPartSql, fromPartSql) +// : paginate(pageNumber, pageSize, selectPartSql, fromPartSql, columns.getValueArray()); + + Config config = _getConfig(); + Connection conn = null; + try { + conn = config.getConnection(); +// String totalRowSql = config.dialect.forPaginateTotalRow(select, sqlExceptSelect, this); + String totalRowSqlExceptSelect = _getDialect().forPaginateFrom(alias, joins, _getTableName(), columns.getList(), null); + String totalRowSql = config.getDialect().forPaginateTotalRow(selectPartSql, totalRowSqlExceptSelect, this); + + StringBuilder findSql = new StringBuilder(); + findSql.append(selectPartSql).append(' ').append(fromPartSql); + + return doPaginateByFullSql(config, conn, pageNumber, pageSize, null, totalRowSql, findSql, columns.getValueArray()); + } catch (Exception e) { + throw new ActiveRecordException(e); + } finally { + config.close(conn); + } } @@ -554,12 +911,21 @@ public class JbootModel> extends Model { public long findCountByColumns(Columns columns) { - return findCountByColumns(columns.getList()); - } + processColumns(columns, "findCount"); + + String loadColumns = "*"; - public long findCountByColumns(List columns) { - String sql = _getDialect().forFindCountByColumns(_getTableName(), columns); - Long value = Db.use(_getConfig().getName()).queryLong(sql, Util.getValueArray(columns)); + //使用 distinct + if (hasAnyJoinEffective()) { + String distinctColumn = JbootModelExts.getDistinctColumn(this); + if (StrUtil.isNotBlank(distinctColumn)) { + loadColumns = "DISTINCT " + distinctColumn; + } + } + + + String sql = _getDialect().forFindCountByColumns(alias, joins, _getTableName(), loadColumns, columns.getList()); + Long value = Db.use(_getConfig().getName()).queryLong(sql, Util.getValueArray(columns.getList())); return value == null ? 0 : value; } @@ -568,9 +934,10 @@ public class JbootModel> extends Model { return get(_getPrimaryKey()); } - public T[] _getIdValues(Class clazz) { + + public Object[] _getIdValues() { String[] pkeys = _getPrimaryKeys(); - T[] values = (T[]) Array.newInstance(clazz, pkeys.length); + Object[] values = new Object[pkeys.length]; int i = 0; for (String key : pkeys) { @@ -591,13 +958,11 @@ public class JbootModel> extends Model { private transient Table table; - public Table _getTable(boolean validateNull) { + public Table _getTable(boolean validateMapping) { if (table == null) { table = super._getTable(); - if (table == null && validateNull) { - throw new JbootException( - String.format("class %s can not mapping to database table,maybe application cannot connect to database. " - , _getUsefulClass().getName())); + if (table == null && validateMapping) { + throwCannotMappingException(); } } return table; @@ -638,17 +1003,9 @@ public class JbootModel> extends Model { } - @Override - protected List find(Config config, String sql, Object... paras) { - SqlDebugger.debug(config, sql, paras); - return super.find(config, sql, paras); - } - - @Override public boolean equals(Object o) { - - if (o == null || !(o instanceof JbootModel)) { + if (!(o instanceof JbootModel)) { return false; } @@ -657,16 +1014,27 @@ public class JbootModel> extends Model { return this == o; } - Object id = ((JbootModel) o)._getIdValue(); - return id != null && id.equals(_getIdValue()); + Object id = ((JbootModel) o)._getIdValue(); + return id != null && id.equals(this._getIdValue()); } + @Override + public int hashCode() { + //可能model在rpc的Controller层,没有映射到数据库 + if (_getTable(false) == null) { + return Objects.hash(_getAttrValues()); + } + + final Object[] idValues = _getIdValues(); + return idValues.length > 0 ? Objects.hash(idValues) : Objects.hash(_getAttrValues()); + } + public M preventXssAttack() { String[] attrNames = _getAttrNames(); for (String attrName : attrNames) { Object value = get(attrName); - if (value == null || !(value instanceof String)) { + if (!(value instanceof String)) { continue; } @@ -680,7 +1048,7 @@ public class JbootModel> extends Model { String[] attrNames = _getAttrNames(); for (String attrName : attrNames) { Object value = get(attrName); - if (value == null || !(value instanceof String)) { + if (!(value instanceof String)) { continue; } @@ -692,9 +1060,7 @@ public class JbootModel> extends Model { } } - if (isIgnoreAttr) { - continue; - } else { + if (!isIgnoreAttr) { set(attrName, StrUtil.escapeHtml((String) value)); } } @@ -702,4 +1068,30 @@ public class JbootModel> extends Model { return (M) this; } + + /** + * Override for print sql + * + * @param config + * @param conn + * @param sql + * @param paras + * @return + * @throws Exception + */ + @Override + protected List find(Config config, Connection conn, String sql, Object... paras) throws Exception { + return SqlDebugger.run(() -> { + try { + return super.find(config, conn, sql, paras); + } catch (Exception e) { + if (e instanceof SQLException) { + throw (SQLException) e; + } else { + throw new SQLException(e); + } + } + }, config, sql, paras); + } + } diff --git a/src/main/java/io/jboot/db/model/JbootModelConfig.java b/src/main/java/io/jboot/db/model/JbootModelConfig.java index 8d82b1f0c04dbb271fa0c84236d77c3b262536bc..c470c04cbc4a2e841a86e6c857b2790cb46c887c 100644 --- a/src/main/java/io/jboot/db/model/JbootModelConfig.java +++ b/src/main/java/io/jboot/db/model/JbootModelConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,13 @@ package io.jboot.db.model; import io.jboot.Jboot; import io.jboot.app.config.annotation.ConfigModel; import io.jboot.components.cache.JbootCache; -import io.jboot.components.cache.JbootCacheConfig; import io.jboot.components.cache.JbootCacheManager; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.StrUtil; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.db.model */ @ConfigModel(prefix = "jboot.model") public class JbootModelConfig { @@ -34,10 +34,41 @@ public class JbootModelConfig { private String columnCreated = "created"; private String columnModified = "modified"; - private int idCacheTime = 60 * 60 * 1; // id 缓存默认缓存1个小时 - private boolean idCacheEnable = true; // 是否启用ID自动缓存 - private String idCacheType = Jboot.config(JbootCacheConfig.class).getType(); + /** + * id 缓存的时间,默认为 1 个小时,单位:秒 + */ + private int idCacheTime = 60 * 60; + + /** + * Model 过滤器,可以通过这个配置来防止 xss 等问题 + * filter 会在 save 和 update 的时候被执行 + */ + private String filterClass; + + /** + * 主键的值的生成器,可以通过配置这个来自定义主键的生成策略 + */ + private String primarykeyValueGeneratorClass; + + + /** + * 是否启用 id 缓存,如果启用,当根据 id 查询的时候,会自动存入缓存 + * 下次再通过 id 查询的时候,直接从缓存中获取 Model + */ + private boolean idCacheEnable = true; + + /** + * 从缓存获取数据的时候,是复制一个返回,这样保证前端在修改的时候不修改到缓存数据 + */ + private boolean idCacheByCopyEnable = true; + + + private String idCacheName = "default"; + + + public JbootModelConfig() { + } public String getScanPackage() { return scanPackage; @@ -79,6 +110,22 @@ public class JbootModelConfig { this.idCacheTime = idCacheTime; } + public String getFilterClass() { + return filterClass; + } + + public void setFilterClass(String filterClass) { + this.filterClass = filterClass; + } + + public String getPrimarykeyValueGeneratorClass() { + return primarykeyValueGeneratorClass; + } + + public void setPrimarykeyValueGeneratorClass(String primarykeyValueGeneratorClass) { + this.primarykeyValueGeneratorClass = primarykeyValueGeneratorClass; + } + public boolean isIdCacheEnable() { return idCacheEnable; } @@ -87,14 +134,56 @@ public class JbootModelConfig { this.idCacheEnable = idCacheEnable; } - public String getIdCacheType() { - return idCacheType; + public boolean isIdCacheByCopyEnable() { + return idCacheByCopyEnable; + } + + public void setIdCacheByCopyEnable(boolean idCacheByCopyEnable) { + this.idCacheByCopyEnable = idCacheByCopyEnable; } - public void setIdCacheType(String idCacheType) { - this.idCacheType = idCacheType; + + public JbootModelConfig(String idCacheName) { + this.idCacheName = idCacheName; + } + + private JbootModelFilter filter; + + public JbootModelFilter getFilter() { + if (filter == null) { + if (StrUtil.isNotBlank(filterClass)) { + filter = ClassUtil.newInstance(filterClass); + } else { + filter = JbootModelFilter.DEFAULT; + } + } + return filter; + } + + public void setFilter(JbootModelFilter filter) { + this.filter = filter; + } + + + private PrimarykeyValueGenerator primarykeyValueGenerator; + + public PrimarykeyValueGenerator getPrimarykeyValueGenerator() { + if (primarykeyValueGenerator == null) { + if (StrUtil.isNotBlank(primarykeyValueGeneratorClass)) { + primarykeyValueGenerator = ClassUtil.newInstance(primarykeyValueGeneratorClass); + } else { + primarykeyValueGenerator = PrimarykeyValueGenerator.DEFAULT; + } + } + return primarykeyValueGenerator; + } + + + public void setPrimarykeyValueGenerator(PrimarykeyValueGenerator primarykeyValueGenerator) { + this.primarykeyValueGenerator = primarykeyValueGenerator; } + private static JbootModelConfig config; public static JbootModelConfig getConfig() { @@ -104,12 +193,18 @@ public class JbootModelConfig { return config; } - private JbootCache jbootCache; + private JbootCache idCache; - public JbootCache getCache() { - if (jbootCache == null) { - jbootCache = JbootCacheManager.me().getCache(idCacheType); + public JbootCache getIdCache() { + if (idCache == null) { + idCache = JbootCacheManager.me().getCache(idCacheName); } - return jbootCache; + return idCache; + } + + public void setIdCache(JbootCache idCache) { + this.idCache = idCache; } + + } diff --git a/src/main/java/io/jboot/db/model/JbootModelExts.java b/src/main/java/io/jboot/db/model/JbootModelExts.java new file mode 100644 index 0000000000000000000000000000000000000000..62c291cfc6684f303a6d0c091a1cf1714eb21585 --- /dev/null +++ b/src/main/java/io/jboot/db/model/JbootModelExts.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.model; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class JbootModelExts { + + private static Map> extAttrs = new ConcurrentHashMap<>(); + + private static final String DISTINCT = "distinct"; + + + public static Object getExtAttr(Object holder, String attr) { + Map map = extAttrs.get(System.identityHashCode(holder)); + return map != null ? map.get(attr) : null; + } + + + public static void setExtAttr(Object holder, String attr, Object value) { + Map map = extAttrs.get(System.identityHashCode(holder)); + if (map == null) { + synchronized (holder) { + map = extAttrs.get(System.identityHashCode(holder)); + if (map == null) { + map = new ConcurrentHashMap<>(); + extAttrs.put(System.identityHashCode(holder), map); + } + } + } + + map.put(attr, value); + } + + + public static void setDistinctColumn(JbootModel holder, String column) { + setExtAttr(holder, DISTINCT, column); + } + + public static String getDistinctColumn(JbootModel holder) { + return (String) getExtAttr(holder, DISTINCT); + } + + + public static void setDatasourceDAO(JbootModel holder, String key, JbootModel datasourceDAO) { + setExtAttr(holder, key, datasourceDAO); + } + + public static T getDatasourceDAO(JbootModel holder, String key) { + return (T) getExtAttr(holder, key); + } + + +} diff --git a/src/main/java/io/jboot/db/model/JbootModelFilter.java b/src/main/java/io/jboot/db/model/JbootModelFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..c9bb5b820f8db1b56b77708134ae217b1c00523b --- /dev/null +++ b/src/main/java/io/jboot/db/model/JbootModelFilter.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.model; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/4 + */ +public interface JbootModelFilter { + + JbootModelFilter DEFAULT = (model, filterBy) -> { + }; + + void filter(JbootModel model, int filterBy); +} diff --git a/src/main/java/io/jboot/db/model/Join.java b/src/main/java/io/jboot/db/model/Join.java index 0bf39d24ca83ac088a5675747c673a61cbeb79fc..d49f9ad729ecb757258b1a8bc80088bad852b9ed 100644 --- a/src/main/java/io/jboot/db/model/Join.java +++ b/src/main/java/io/jboot/db/model/Join.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/db/model/Joiner.java b/src/main/java/io/jboot/db/model/Joiner.java index d9ca095d10829029780376b42840f2fde98d1e40..db3422ecca0f489ee0da64e4713d7549051ec8a8 100644 --- a/src/main/java/io/jboot/db/model/Joiner.java +++ b/src/main/java/io/jboot/db/model/Joiner.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/db/model/Or.java b/src/main/java/io/jboot/db/model/Or.java index 19f31ed9199e2f92a7a896a76b4e216f06d1b6b5..2dbbf3ac310d0b1ab29524da539e1c5e2d94ebfc 100644 --- a/src/main/java/io/jboot/db/model/Or.java +++ b/src/main/java/io/jboot/db/model/Or.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/db/model/PrimarykeyValueGenerator.java b/src/main/java/io/jboot/db/model/PrimarykeyValueGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..b7012825b1b809035f3e5561bddba38700c8a84e --- /dev/null +++ b/src/main/java/io/jboot/db/model/PrimarykeyValueGenerator.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.model; + +import io.jboot.utils.StrUtil; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/4 + */ +public interface PrimarykeyValueGenerator { + + static PrimarykeyValueGenerator DEFAULT = (model, primarykeyValueClass) -> primarykeyValueClass == String.class ? StrUtil.uuid() : null; + + public Object genValue(JbootModel model,Class primarykeyValueClass); +} diff --git a/src/main/java/io/jboot/db/model/SqlBuilder.java b/src/main/java/io/jboot/db/model/SqlBuilder.java index 3cb2450622d730713bd6e2529731f6631160473b..7a3904a1e2237895ca0b30a5d21c7c994a904c8f 100644 --- a/src/main/java/io/jboot/db/model/SqlBuilder.java +++ b/src/main/java/io/jboot/db/model/SqlBuilder.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,116 +15,154 @@ */ package io.jboot.db.model; +import com.jfinal.kit.LogKit; import io.jboot.utils.ArrayUtil; import io.jboot.utils.StrUtil; import java.util.List; +import java.util.regex.Pattern; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.db.dialect */ public class SqlBuilder { + private static final String OR = " OR "; + private static final String AND = " AND "; + public static void buildMysqlWhereSql(StringBuilder sqlBuilder, List columns) { buildWhereSql(sqlBuilder, columns, '`'); } - public static String forDeleteByColumns(String table, List columns, char separator) { - StringBuilder sql = new StringBuilder(45); - sql.append("DELETE FROM ").append(separator).append(table).append(separator); - SqlBuilder.buildWhereSql(sql, columns, ' '); - return sql.toString(); + public static String forDeleteByColumns(String alias, List joins, String table, List columns, char separator) { + StringBuilder sqlBuilder = new StringBuilder(45); + sqlBuilder.append("DELETE FROM "); + + appendTextWithSeparator(sqlBuilder, table, separator); + + buildAlias(sqlBuilder, alias); + buildJoinSql(sqlBuilder, joins, separator); + buildWhereSql(sqlBuilder, columns, separator); + + return sqlBuilder.toString(); } public static void buildWhereSql(StringBuilder sqlBuilder, List columns, char separator) { + buildWhereSql(sqlBuilder, columns, separator, true); + } + + public static void buildWhereSql(StringBuilder sqlBuilder, List columns, char separator, boolean appendWhereKeyword) { if (ArrayUtil.isNullOrEmpty(columns)) { return; } - sqlBuilder.append(" WHERE "); - buildByColumns(sqlBuilder, columns, separator); + StringBuilder whereSqlBuilder = new StringBuilder(); + buildByColumns(whereSqlBuilder, columns, separator); + if (whereSqlBuilder.length() > 0) { + if (appendWhereKeyword && !isAllGroupByColumns(columns)) { + sqlBuilder.append(" WHERE "); + } + sqlBuilder.append(whereSqlBuilder); + } + } + //fixed: https://gitee.com/JbootProjects/jboot/issues/I3TP7J + private static boolean isAllGroupByColumns(List columns) { + for (Column column : columns) { + if (!(column instanceof GroupBy)) { + return false; + } + } + return true; } + private static void buildByColumns(StringBuilder sqlBuilder, List columns, char separator) { for (int i = 0; i < columns.size(); i++) { - Column curent = columns.get(i); - Column next = i >= columns.size() - 1 ? null : columns.get(i + 1); + Column before = i > 0 ? columns.get(i - 1) : null; + Column current = columns.get(i); - // or - if (curent instanceof Or) { + if (current instanceof Or) { continue; } - // string - else if (curent instanceof Str) { - sqlBuilder.append(((Str) curent).getString()); + // sqlPart + else if (current instanceof SqlPart) { + appendSqlPartLogic(sqlBuilder, before, (SqlPart) current, separator); } // group - else if (curent instanceof Group) { - appendGroupLogic(sqlBuilder, ((Group) curent).getColumns().getList(), separator); + else if (current instanceof Group) { + appendGroupLogic(sqlBuilder, before, (Group) current, separator); } // in logic - else if (Column.LOGIC_IN.equals(curent.getLogic()) || Column.LOGIC_NOT_IN.equals(curent.getLogic())) { - appendInLogic(sqlBuilder, curent, separator); + else if (Column.LOGIC_IN.equals(current.getLogic()) || Column.LOGIC_NOT_IN.equals(current.getLogic())) { + appendLinkString(sqlBuilder, before); + appendInLogic(sqlBuilder, current, separator); } // between logic - else if (Column.LOGIC_BETWEEN.equals(curent.getLogic()) || Column.LOGIC_NOT_BETWEEN.equals(curent.getLogic())) { - appendBetweenLogic(sqlBuilder, curent, separator); + else if (Column.LOGIC_BETWEEN.equals(current.getLogic()) || Column.LOGIC_NOT_BETWEEN.equals(current.getLogic())) { + appendLinkString(sqlBuilder, before); + appendBetweenLogic(sqlBuilder, current, separator); } // others else { + appendLinkString(sqlBuilder, before); + appendColumnName(sqlBuilder, current, separator); - appendColumnName(sqlBuilder, curent, separator); - - if (curent.hasPara()) { - sqlBuilder.append("?"); + if (current.hasPara()) { + sqlBuilder.append('?'); } } + } + } + - appendLinkString(sqlBuilder, next); + private static void appendSqlPartLogic(StringBuilder sqlBuilder, Column before, SqlPart sqlPart, char separator) { + if (!sqlPart.isWithoutLink()) { + appendLinkString(sqlBuilder, before); } + sqlPart.build(separator); + sqlBuilder.append(' ').append(sqlPart.getSql()).append(' '); } + private static void appendColumnName(StringBuilder sqlBuilder, Column column, char separator) { - if (column.getName().contains(".")) { - sqlBuilder.append(column.getName()) - .append(" ") - .append(column.getLogic()) - .append(" "); - } else { - sqlBuilder.append(separator) - .append(column.getName()) - .append(separator) - .append(" ") - .append(column.getLogic()) - .append(" "); - } + appendTextWithSeparator(sqlBuilder, column.getName(), separator); + sqlBuilder.append(' ') + .append(column.getLogic()) + .append(' '); } - private static void appendLinkString(StringBuilder sqlBuilder, Column next) { - if (next == null) { + private static void appendLinkString(StringBuilder sqlBuilder, Column before) { + if (sqlBuilder.length() == 0 || before == null) { return; } else { - sqlBuilder.append(next instanceof Or ? " OR " : " AND "); + sqlBuilder.append(before instanceof Or ? OR : AND); } } - public static void appendGroupLogic(StringBuilder sqlBuilder, List columns, char separator) { + public static void appendGroupLogic(StringBuilder sqlBuilder, Column before, Group group, char separator) { + List columns = group.getColumns().getList(); if (ArrayUtil.isNullOrEmpty(columns)) { return; } - sqlBuilder.append("("); - buildByColumns(sqlBuilder, columns, separator); - sqlBuilder.append(")"); + StringBuilder groupSqlBuilder = new StringBuilder(); + buildByColumns(groupSqlBuilder, columns, separator); + + String groupSql = groupSqlBuilder.toString(); + if (StrUtil.isNotBlank(groupSql)) { + appendLinkString(sqlBuilder, before); + sqlBuilder.append('('); + sqlBuilder.append(groupSql); + sqlBuilder.append(')'); + } } @@ -132,40 +170,61 @@ public class SqlBuilder { appendColumnName(sqlBuilder, column, separator); - sqlBuilder.append("("); + sqlBuilder.append('('); + Object[] values = (Object[]) column.getValue(); - for (int i = 0; i < values.length; i++) { - sqlBuilder.append("?"); - if (i != values.length - 1) { - sqlBuilder.append(","); + + //in 里的参数数量 + int paraCount = 0; + for (Object v : values) { + if (v.getClass() == int[].class) { + paraCount += ((int[]) v).length; + } else if (v.getClass() == long[].class) { + paraCount += ((long[]) v).length; + } else if (v.getClass() == short[].class) { + paraCount += ((short[]) v).length; + } else { + paraCount++; } } - sqlBuilder.append(")"); + + for (int i = 0; i < paraCount; i++) { + sqlBuilder.append('?'); + if (i != paraCount - 1) { + sqlBuilder.append(','); + } + } + sqlBuilder.append(')'); } public static void appendBetweenLogic(StringBuilder sqlBuilder, Column column, char separator) { - sqlBuilder.append(separator) - .append(column.getName()) - .append(separator) - .append(" ") - .append(column.getLogic()); - + appendTextWithSeparator(sqlBuilder, column.getName(), separator); + sqlBuilder.append(' ').append(column.getLogic()); sqlBuilder.append(" ? AND ?"); } - public static StringBuilder forFindByColumns(List joins, String table, String loadColumns, List columns, String orderBy, char separator) { + + public static void appendTextWithSeparator(StringBuilder sqlBuilder, String text, char separator) { + if (text.indexOf(".") > 0) { + sqlBuilder.append(text); + } else { + sqlBuilder.append(separator).append(text).append(separator); + } + } + + + public static StringBuilder forFindByColumns(String alias, List joins, String table, String loadColumns, List columns, String orderBy, char separator) { StringBuilder sqlBuilder = new StringBuilder("SELECT "); sqlBuilder.append(loadColumns) - .append(" FROM ") - .append(separator) - .append(table) - .append(separator); + .append(" FROM "); + appendTextWithSeparator(sqlBuilder, table, separator); + buildAlias(sqlBuilder, alias); buildJoinSql(sqlBuilder, joins, separator); - buildWhereSql(sqlBuilder, columns, separator); + orderBy = escapeOrderBySql(orderBy); if (StrUtil.isNotBlank(orderBy)) { sqlBuilder.append(" ORDER BY ").append(orderBy); } @@ -173,16 +232,36 @@ public class SqlBuilder { return sqlBuilder; } - public static String forPaginateFrom(List joins, String table, List columns, String orderBy, char separator) { - StringBuilder sqlBuilder = new StringBuilder(" FROM ") - .append(separator) - .append(table) - .append(separator); + //来源于 @link Dialect.java + private static final Pattern ORDER_BY_PATTERN = Pattern.compile( + "order\\s+by\\s+[^,\\s]+(\\s+asc|\\s+desc)?(\\s*,\\s*[^,\\s]+(\\s+asc|\\s+desc)?)*", + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); + private static String replaceOrderBy(String sql) { + return ORDER_BY_PATTERN.matcher(sql).replaceAll(""); + } + public static String forPaginateDistinctTotalRow(String select, String sqlExceptSelect, Object ext) { + if (ext instanceof JbootModel && CPI.hasAnyJoinEffective((JbootModel) ext)) { + String distinct = JbootModelExts.getDistinctColumn((JbootModel) ext); + if (StrUtil.isNotBlank(distinct)) { + return "SELECT count(DISTINCT " + distinct + ") " + replaceOrderBy(sqlExceptSelect); + } + } + return null; + } + + + public static String forPaginateFrom(String alias, List joins, String table, List columns, String orderBy, char separator) { + StringBuilder sqlBuilder = new StringBuilder(" FROM "); + appendTextWithSeparator(sqlBuilder, table, separator); + + buildAlias(sqlBuilder, alias); buildJoinSql(sqlBuilder, joins, separator); buildWhereSql(sqlBuilder, columns, separator); + orderBy = escapeOrderBySql(orderBy); + if (StrUtil.isNotBlank(orderBy)) { sqlBuilder.append(" ORDER BY ").append(orderBy); } @@ -190,7 +269,8 @@ public class SqlBuilder { return sqlBuilder.toString(); } - private static void buildJoinSql(StringBuilder sqlBuilder, List joins, char separator) { + + public static void buildJoinSql(StringBuilder sqlBuilder, List joins, char separator) { if (joins == null || joins.isEmpty()) { return; } @@ -199,14 +279,10 @@ public class SqlBuilder { continue; } - sqlBuilder.append(join.getType()) - .append(separator) - .append(join.getTable()) - .append(separator); + sqlBuilder.append(join.getType()); + appendTextWithSeparator(sqlBuilder, join.getTable(), separator); - if (StrUtil.isNotBlank(join.getAs())) { - sqlBuilder.append(" AS ").append(join.getAs()); - } + buildAlias(sqlBuilder, join.getAs()); sqlBuilder.append(" ON ") .append(join.getOn()); @@ -214,14 +290,40 @@ public class SqlBuilder { } - public static String forFindCountByColumns(String table, List columns, char separator) { - StringBuilder sqlBuilder = new StringBuilder("SELECT count(*) FROM ") - .append(separator) - .append(table) - .append(separator); + public static void buildAlias(StringBuilder sqlBuilder, String alias) { + if (StrUtil.isNotBlank(alias)) { + sqlBuilder.append(" AS ").append(alias); + } + } + + + public static String forFindCountByColumns(String alias, List joins, String table, String loadColumns, List columns, char separator) { + StringBuilder sqlBuilder = new StringBuilder("SELECT count(" + loadColumns + ") FROM "); + appendTextWithSeparator(sqlBuilder, table, separator); + buildAlias(sqlBuilder, alias); + buildJoinSql(sqlBuilder, joins, separator); buildWhereSql(sqlBuilder, columns, separator); return sqlBuilder.toString(); } + + + public static String escapeOrderBySql(String orignalOrderBy) { + if (StrUtil.isNotBlank(orignalOrderBy) && !isValidOrderBySql(orignalOrderBy)) { + LogKit.warn("Sql Warn: order_by value has inject chars and be filtered, order_by value: " + orignalOrderBy); + return ""; + } + return orignalOrderBy; + } + + + /** + * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) + */ + private static String SQL_ORDER_BY_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; + + private static boolean isValidOrderBySql(String value) { + return value.matches(SQL_ORDER_BY_PATTERN); + } } diff --git a/src/main/java/io/jboot/db/model/SqlPart.java b/src/main/java/io/jboot/db/model/SqlPart.java new file mode 100644 index 0000000000000000000000000000000000000000..67567d8620927d3762c06841d9b7cf8d055e3368 --- /dev/null +++ b/src/main/java/io/jboot/db/model/SqlPart.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.model; + + +class SqlPart extends Column { + + private String sql; + private Object para; + private boolean withoutLink = false; + + public SqlPart(String sql) { + this.sql = sql; + } + + public SqlPart(String sql, Object para) { + this.sql = sql; + this.para = para; + } + + public SqlPart(String sql, boolean withoutLink) { + this.sql = sql; + this.withoutLink = withoutLink; + } + + public SqlPart(String sql, Object para, boolean withoutLink) { + this.sql = sql; + this.para = para; + this.withoutLink = withoutLink; + } + + public String getSql() { + return sql; + } + + public void setSql(String sql) { + this.sql = sql; + } + + public Object getPara() { + return para; + } + + public void setPara(Object para) { + this.para = para; + } + + public boolean isWithoutLink() { + return withoutLink; + } + + public void setWithoutLink(boolean withoutLink) { + this.withoutLink = withoutLink; + } + + @Override + public boolean hasPara() { + return para != null; + } + + @Override + public Object getValue() { + return para; + } + + //构建 sql + public void build(char separator) { + } +} diff --git a/src/main/java/io/jboot/db/model/Util.java b/src/main/java/io/jboot/db/model/Util.java index d8e52c0b033170999c7a5e330e2919deb8641a5c..1cd0b5971897ffaaf5eaf03c1ade528752bb20c7 100644 --- a/src/main/java/io/jboot/db/model/Util.java +++ b/src/main/java/io/jboot/db/model/Util.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,16 @@ package io.jboot.db.model; -import java.util.Collections; +import com.jfinal.ext.kit.DateKit; +import io.jboot.utils.CollectionUtil; +import io.jboot.utils.StrUtil; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Date; import java.util.LinkedList; import java.util.List; +import java.util.regex.Matcher; class Util { @@ -30,7 +37,7 @@ class Util { return NULL_PARA_ARRAY; } - List values = new LinkedList<>(); + List paras = new LinkedList<>(); for (Column column : columns) { if (!column.hasPara()) { @@ -39,14 +46,122 @@ class Util { Object value = column.getValue(); if (value != null) { if (value.getClass().isArray()) { - Object[] vs = (Object[]) value; - Collections.addAll(values, vs); + Object[] values = (Object[]) value; + for (Object v : values) { + if (v != null && ( + v.getClass() == int[].class + || v.getClass() == long[].class + || v.getClass() == short[].class + || v.getClass() == float[].class + || v.getClass() == double[].class + )) { + for (int i = 0; i < Array.getLength(v); i++) { + paras.add(Array.get(v, i)); + } + } else { + paras.add(v); + } + } } else { - values.add(value); + paras.add(value); + } + } + } + + return paras.isEmpty() ? NULL_PARA_ARRAY : paras.toArray(); + } + + + + static String replaceSqlPara(String sql, Object value) { + // null + if (value == null) { + return sql.replaceFirst("\\?", "null"); + } + // number + else if (value instanceof Number) { + return sql.replaceFirst("\\?", value.toString()); + } + // numeric + else if (value instanceof String && StrUtil.isNumeric((String) value)) { + return sql.replaceFirst("\\?", (String) value); + } + // other + else { + StringBuilder sb = new StringBuilder(); + if (value instanceof Date) { + sb.append(DateKit.toStr((Date) value, DateKit.timeStampPattern)); + } else { + sb.append(value); + } + return sql.replaceFirst("\\?", Matcher.quoteReplacement(sb.toString())); + } + } + + + static String deleteWhitespace(String str) { + final int strLen = str.length(); + final char[] chs = new char[strLen]; + int count = 0; + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + chs[count++] = str.charAt(i); + } + } + if (count == strLen) { + return str; + } + return new String(chs, 0, count); + } + + + static String array2String(Object[] a) { + if (a == null) { + return "null"; + } + + int iMax = a.length - 1; + if (iMax == -1) { + return "[]"; + } + + StringBuilder b = new StringBuilder(); + b.append('['); + for (int i = 0; ; i++) { + b.append(a[i]); + if (i == iMax) { + return b.append(']').toString(); + } + b.append("-"); + } + } + + + static void checkNullParas(Columns columns, String name, Object... paras) { + if (columns.isUseSafeMode()) { + for (Object obj : paras) { + if (obj == null) { + throw new NullPointerException("Column \"" + name + "\" para is null, Columns must has not null para value in safeMode."); + } + } + } + } + + static void checkNullParas(Columns columns, Object... paras) { + if (columns.isUseSafeMode()) { + for (Object obj : paras) { + if (obj == null) { + throw new NullPointerException("Columns must has not null para value in safeMode."); } } } + } - return values.isEmpty() ? NULL_PARA_ARRAY : values.toArray(); + static void checkNullParas(Columns columns, Collection collection) { + if (columns.isUseSafeMode()) { + if (CollectionUtil.isEmpty(collection)) { + throw new NullPointerException("Columns must has not empty collection in safeMode."); + } + } } } diff --git a/src/main/java/io/jboot/db/transactional/TransactionalInterceptor.java b/src/main/java/io/jboot/db/transactional/TransactionalInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..e02e969103ba4fe28d8446295487d8296f14eb5e --- /dev/null +++ b/src/main/java/io/jboot/db/transactional/TransactionalInterceptor.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.transactional; + + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.kit.LogKit; +import com.jfinal.kit.Ret; +import com.jfinal.plugin.activerecord.*; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.aop.annotation.Transactional; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.StrUtil; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +/** + * 缓存操作的拦截器 + * + * @author michael yang + */ +@AutoLoad +public class TransactionalInterceptor implements Interceptor, InterceptorBuilder { + + + @Override + public void intercept(Invocation inv) { + + Transactional transactional = inv.getMethod().getAnnotation(Transactional.class); + String configName = AnnotationUtil.get(transactional.config()); + + DbPro dbPro = StrUtil.isBlank(configName) ? Db.use() : Db.use(configName); + Config config = StrUtil.isBlank(configName) ? DbKit.getConfig() : DbKit.getConfig(configName); + + int transactionLevel = transactional.transactionLevel(); + if (transactionLevel == -1) { + transactionLevel = config.getTransactionLevel(); + } + + + IAtom runnable = () -> { + try { + inv.invoke(); + } catch (Throwable ex) { + for (Class forClass : transactional.noRollbackFor()) { + if (ex.getClass().isAssignableFrom(forClass)) { + LogKit.error(ex.toString(), ex); + + //允许事务提交 + return true; + } + } + throw ex; + } + + //没有返回值的方法,只要没有异常就是提交事务 + if (inv.getMethod().getReturnType() == void.class) { + return true; + } + + Object result = inv.getReturnValue(); + + if (result == null && transactional.rollbackForNull()) { + return false; + } + + if (result instanceof Boolean && !(Boolean) result && transactional.rollbackForFalse()) { + return false; + } + + if (result instanceof Ret && ((Ret) result).isFail() && transactional.rollbackForRetFail()) { + return false; + } + + return true; + }; + + + if (!inv.isActionInvocation() && transactional.inNewThread()) { + try { + Future future = txInNewThread(inv, transactional.threadPoolName(), dbPro, transactionLevel, runnable); + + //有返回值的场景下,需要等待返回值 + //或者没有返回值,但是配置了 @Transacional(threadWithBlocked=ture) 的时候 + if (inv.getMethod().getReturnType() != void.class + || transactional.threadWithBlocked()) { + Boolean success = future.get(); + } + } catch (Exception e) { + LogKit.error(e.toString(), e); + } + } else { + dbPro.tx(transactionLevel, runnable); + } + + } + + + public Future txInNewThread(Invocation inv, String name, DbPro dbPro, int transactionLevel, IAtom atom) { + Callable callable = () -> dbPro.tx(transactionLevel, atom); + return TransactionalManager.me().execute(name, callable, inv); + } + + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.hasAnnotation(method, Transactional.class)) { + interceptors.add(this); + } + } +} diff --git a/src/main/java/io/jboot/db/transactional/TransactionalManager.java b/src/main/java/io/jboot/db/transactional/TransactionalManager.java new file mode 100644 index 0000000000000000000000000000000000000000..2647e95002d3d91e8f80bec606d8203065357a6d --- /dev/null +++ b/src/main/java/io/jboot/db/transactional/TransactionalManager.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.db.transactional; + +import com.jfinal.aop.Invocation; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.NamedThreadFactory; +import io.jboot.utils.StrUtil; + +import java.util.Map; +import java.util.concurrent.*; + +public class TransactionalManager { + + private static TransactionalManager instance = new TransactionalManager(); + + public static TransactionalManager me() { + return instance; + } + + + private Map executorServiceMap = new ConcurrentHashMap<>(); + private ThreadFactory threadFactory = new NamedThreadFactory("Transactional", true); + + //当未配置线程池名称的时候,是否使用 threadFactory 来执行 + private boolean runDefaultWithoutConfigName = false; + + + public void addExecutorService(String name, ExecutorService service) { + executorServiceMap.put(name, service); + } + + public ExecutorService getExecutorService(String name) { + return executorServiceMap.get(name); + } + + public void removeExecutorService(String name) { + executorServiceMap.remove(name); + } + + public Map getExecutorServiceMap() { + return executorServiceMap; + } + + public ThreadFactory getThreadFactory() { + return threadFactory; + } + + public void setThreadFactory(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + } + + public boolean isRunDefaultWithoutConfigName() { + return runDefaultWithoutConfigName; + } + + public void setRunDefaultWithoutConfigName(boolean runDefaultWithoutConfigName) { + this.runDefaultWithoutConfigName = runDefaultWithoutConfigName; + } + + public Future execute(String byName, Callable callable, Invocation inv) { + if (StrUtil.isBlank(byName)) { + FutureTask task = new FutureTask<>(callable); + threadFactory.newThread(task).start(); + return task; + } + + + ExecutorService executorService = executorServiceMap.get(byName); + if (executorService != null) { + return executorService.submit(callable); + } + + if (!runDefaultWithoutConfigName) { + throw new IllegalStateException("Can not find threadPoolName: \"" + byName + "\" for @Transactional() in method: " + + ClassUtil.buildMethodString(inv.getMethod()) + + ".\n Please invoke TransactionalManager.me().addExecutorService() to configure transactional threadPool on application started."); + } + + + FutureTask task = new FutureTask<>(callable); + threadFactory.newThread(task).start(); + return task; + } + + +} diff --git a/src/main/java/io/jboot/exception/JbootException.java b/src/main/java/io/jboot/exception/JbootException.java index 28b79a3ee411ffe98d64c39ebc20c7afbebddec8..484a34efc6dd03ab7a1d998e1a75f3989c6497d5 100644 --- a/src/main/java/io/jboot/exception/JbootException.java +++ b/src/main/java/io/jboot/exception/JbootException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/exception/JbootExceptionHolder.java b/src/main/java/io/jboot/exception/JbootExceptionHolder.java index 716c34f1c451dcdea68de7021fdb7c00b84f06bb..76390b7d875235e4f19b5c3e8a248cf46aa380ce 100644 --- a/src/main/java/io/jboot/exception/JbootExceptionHolder.java +++ b/src/main/java/io/jboot/exception/JbootExceptionHolder.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,8 +39,12 @@ public class JbootExceptionHolder { throwables.get().add(ex); } public static void hold(String message,Throwable ex) { - messages.get().add(message); - throwables.get().add(ex); + if (message != null) { + messages.get().add(message); + } + if (ex != null) { + throwables.get().add(ex); + } } public static List getThrowables() { diff --git a/src/main/java/io/jboot/exception/JbootIllegalConfigException.java b/src/main/java/io/jboot/exception/JbootIllegalConfigException.java index baead5f5e7c8d6ea6ec419cebe0c5bbe2e96472e..36572440b96c0c2f11dac62c16f9e2c88b836873 100644 --- a/src/main/java/io/jboot/exception/JbootIllegalConfigException.java +++ b/src/main/java/io/jboot/exception/JbootIllegalConfigException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ package io.jboot.exception; * @version V1.0 * @Title: 配置错误 * @Description: 在某些情况,必须要指定的某个配置,但是用户没有配置时,抛出该错误 - * @Package io.jboot.exception */ public class JbootIllegalConfigException extends JbootException { diff --git a/src/main/java/io/jboot/exception/JbootRpcException.java b/src/main/java/io/jboot/exception/JbootRpcException.java index 5a4c5c150ac384df0c82569201b8e039a0cd50ab..14bb76e6f47030ea9090b8d11375d87ad4fd5a99 100644 --- a/src/main/java/io/jboot/exception/JbootRpcException.java +++ b/src/main/java/io/jboot/exception/JbootRpcException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/ext/MixedByteArrayOutputStream.java b/src/main/java/io/jboot/ext/MixedByteArrayOutputStream.java new file mode 100644 index 0000000000000000000000000000000000000000..ff8983f6c1c7ea7f5197585214a1382d8271d391 --- /dev/null +++ b/src/main/java/io/jboot/ext/MixedByteArrayOutputStream.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.ext; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +public class MixedByteArrayOutputStream extends ByteArrayOutputStream { + + public MixedByteArrayOutputStream() { + } + + public MixedByteArrayOutputStream(int size) { + super(size); + } + + + public ByteArrayInputStream getInputStream(){ + return new ByteArrayInputStream(buf,0, count); + } +} diff --git a/src/main/java/io/jboot/objects/counter/JbootCounter.java b/src/main/java/io/jboot/objects/counter/JbootCounter.java new file mode 100644 index 0000000000000000000000000000000000000000..f93da079bd2e6444030ddeb022032ec913c0a05f --- /dev/null +++ b/src/main/java/io/jboot/objects/counter/JbootCounter.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.objects.counter; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/2/28 + */ +public interface JbootCounter { + + Long increment(); + + Long decrement(); + + Long get(); + + void set(long newValue); + +} diff --git a/src/main/java/io/jboot/objects/counter/JbootCounterConfig.java b/src/main/java/io/jboot/objects/counter/JbootCounterConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..541df41b71a508d12fd33eafc660bea9f289af06 --- /dev/null +++ b/src/main/java/io/jboot/objects/counter/JbootCounterConfig.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.objects.counter; + +import io.jboot.app.config.annotation.ConfigModel; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/8 + */ +@ConfigModel(prefix = "jboot.object.counter") +public class JbootCounterConfig { + + public static final String TYPE_LOCAL = "local"; + public static final String TYPE_REDIS = "redis"; + + private String type = TYPE_LOCAL; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/src/main/java/io/jboot/objects/counter/JbootCounterManager.java b/src/main/java/io/jboot/objects/counter/JbootCounterManager.java new file mode 100644 index 0000000000000000000000000000000000000000..af02c19c064998f4d7cb95dee27c8c309d3d0769 --- /dev/null +++ b/src/main/java/io/jboot/objects/counter/JbootCounterManager.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.objects.counter; + +import io.jboot.Jboot; +import io.jboot.core.spi.JbootSpiLoader; +import io.jboot.objects.counter.impl.JbootLocalCounter; +import io.jboot.objects.counter.impl.JbootRedisCounter; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/2/28 + */ +public class JbootCounterManager { + + private static JbootCounterManager instance = new JbootCounterManager(); + public static JbootCounterManager me() { + return instance; + } + + + private JbootCounterConfig config = Jboot.config(JbootCounterConfig.class); + + public JbootCounter create(String name){ + switch (config.getType()){ + case JbootCounterConfig.TYPE_LOCAL: + return new JbootLocalCounter(name); + case JbootCounterConfig.TYPE_REDIS: + return new JbootRedisCounter(name); + default: + return JbootSpiLoader.load(JbootCounter.class,config.getType()); + } + + + } + +} diff --git a/src/main/java/io/jboot/objects/counter/impl/JbootLocalCounter.java b/src/main/java/io/jboot/objects/counter/impl/JbootLocalCounter.java new file mode 100644 index 0000000000000000000000000000000000000000..9fa501cc9514af157bcdc243ccfc75644c2a2af2 --- /dev/null +++ b/src/main/java/io/jboot/objects/counter/impl/JbootLocalCounter.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.objects.counter.impl; + +import io.jboot.objects.counter.JbootCounter; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/7 + */ +public class JbootLocalCounter implements JbootCounter { + + private static Map ATOMICLONGS = new HashMap<>(); + + private AtomicLong atomicLong; + + public JbootLocalCounter(String name) { + atomicLong = ATOMICLONGS.get(name); + if (atomicLong == null) { + synchronized (JbootLocalCounter.class) { + atomicLong = ATOMICLONGS.get(name); + if (atomicLong == null) { + atomicLong = new AtomicLong(); + ATOMICLONGS.put(name, atomicLong); + } + } + } + } + + @Override + public Long increment() { + return atomicLong.incrementAndGet(); + } + + @Override + public Long decrement() { + return atomicLong.decrementAndGet(); + } + + @Override + public Long get() { + return atomicLong.get(); + } + + @Override + public void set(long newValue) { + atomicLong.set(newValue); + } +} diff --git a/src/main/java/io/jboot/objects/counter/impl/JbootRedisCounter.java b/src/main/java/io/jboot/objects/counter/impl/JbootRedisCounter.java new file mode 100644 index 0000000000000000000000000000000000000000..d59eabba582d441ed25706c276a00527fc27892f --- /dev/null +++ b/src/main/java/io/jboot/objects/counter/impl/JbootRedisCounter.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.objects.counter.impl; + +import io.jboot.Jboot; +import io.jboot.objects.counter.JbootCounter; +import io.jboot.support.redis.JbootRedis; +import io.jboot.utils.StrUtil; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/7 + */ +public class JbootRedisCounter implements JbootCounter { + + private JbootRedis redis = Jboot.getRedis(); + private String name; + + public JbootRedisCounter(String name) { + this.name = name; + } + + @Override + public Long increment() { + return redis.incr(name); + } + + @Override + public Long decrement() { + return redis.decr(name); + } + + @Override + public Long get() { + String value = redis.getWithoutSerialize(name); + return StrUtil.isNotBlank(value) ? null : Long.valueOf(value); + } + + @Override + public void set(long newValue) { + redis.setWithoutSerialize(name, newValue); + } +} diff --git a/src/main/java/io/jboot/objects/lock/JbootLock.java b/src/main/java/io/jboot/objects/lock/JbootLock.java new file mode 100644 index 0000000000000000000000000000000000000000..249cb28f0294b4d65b35e4ab272666e490a12a9a --- /dev/null +++ b/src/main/java/io/jboot/objects/lock/JbootLock.java @@ -0,0 +1,23 @@ +package io.jboot.objects.lock; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/2/28 + */ +public interface JbootLock extends Lock { + + @Override + void lock(); + + @Override + void unlock(); + + @Override + boolean tryLock(); + + @Override + boolean tryLock(long time, TimeUnit unit) throws InterruptedException; +} diff --git a/src/main/java/io/jboot/objects/lock/JbootLockConfig.java b/src/main/java/io/jboot/objects/lock/JbootLockConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..cb914d82d762c76a75d799efec3ca98e6975fff5 --- /dev/null +++ b/src/main/java/io/jboot/objects/lock/JbootLockConfig.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.objects.lock; + +import io.jboot.app.config.annotation.ConfigModel; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/8 + */ +@ConfigModel(prefix = "jboot.object.lock") +public class JbootLockConfig { + + public static final String TYPE_LOCAL = "local"; + public static final String TYPE_REDIS = "redis"; + + private String type = TYPE_LOCAL; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/src/main/java/io/jboot/objects/lock/JbootLockManager.java b/src/main/java/io/jboot/objects/lock/JbootLockManager.java new file mode 100644 index 0000000000000000000000000000000000000000..9fca2b39e1090d074cde38cfebe76fab106797dd --- /dev/null +++ b/src/main/java/io/jboot/objects/lock/JbootLockManager.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.objects.lock; + +import io.jboot.Jboot; +import io.jboot.core.spi.JbootSpiLoader; +import io.jboot.objects.counter.JbootCounterConfig; +import io.jboot.objects.lock.impl.JbootLocalLock; +import io.jboot.objects.lock.impl.JbootRedisLock; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/2/28 + */ +public class JbootLockManager { + + private static JbootLockManager instance = new JbootLockManager(); + public static JbootLockManager me() { + return instance; + } + + + private JbootLockConfig config = Jboot.config(JbootLockConfig.class); + + public JbootLock create(String name){ + switch (config.getType()){ + case JbootCounterConfig.TYPE_LOCAL: + return new JbootLocalLock(name); + case JbootCounterConfig.TYPE_REDIS: + return new JbootRedisLock(name); + default: + return JbootSpiLoader.load(JbootLock.class,config.getType()); + } + + + } + +} diff --git a/src/main/java/io/jboot/objects/lock/impl/JbootLocalLock.java b/src/main/java/io/jboot/objects/lock/impl/JbootLocalLock.java new file mode 100644 index 0000000000000000000000000000000000000000..5c8e1ed09e32535f5beaeb5cdc89181040bf7952 --- /dev/null +++ b/src/main/java/io/jboot/objects/lock/impl/JbootLocalLock.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.objects.lock.impl; + +import io.jboot.objects.lock.JbootLock; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/7 + */ +public class JbootLocalLock implements JbootLock { + + private static Map LOCKS = new HashMap<>(); + + private ReentrantLock lock; + + public JbootLocalLock(String name) { + lock = LOCKS.get(name); + if (lock == null) { + synchronized (JbootLocalLock.class) { + lock = LOCKS.get(name); + if (lock == null) { + lock = new ReentrantLock(); + LOCKS.put(name, lock); + } + } + } + } + + @Override + public void lock() { + lock.lock(); + } + + @Override + public void lockInterruptibly() throws InterruptedException { + lock.lockInterruptibly(); + } + + @Override + public boolean tryLock() { + return lock.tryLock(); + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + return lock.tryLock(time, unit); + } + + @Override + public void unlock() { + lock.unlock(); + } + + @Override + public Condition newCondition() { + return lock.newCondition(); + } +} diff --git a/src/main/java/io/jboot/objects/lock/impl/JbootRedisLock.java b/src/main/java/io/jboot/objects/lock/impl/JbootRedisLock.java new file mode 100644 index 0000000000000000000000000000000000000000..76c52bb0c83818181883dc2fc28acb1b238e723d --- /dev/null +++ b/src/main/java/io/jboot/objects/lock/impl/JbootRedisLock.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.objects.lock.impl; + +import io.jboot.objects.lock.JbootLock; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/7 + */ +public class JbootRedisLock implements JbootLock { + + private io.jboot.support.redis.JbootRedisLock lock ; + private String name; + + public JbootRedisLock(String name) { + this.name = name; + this.lock = new io.jboot.support.redis.JbootRedisLock(name); + } + + @Override + public void lock() { + lock.setTimeoutMsecs(60 * 1000); // 1 分钟 + if (!lock.acquire()){ + throw new IllegalStateException("can not get lock in JbootRedisLock"); + } + } + + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock() { + return lock.acquire(); + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + lock.setTimeoutMsecs(unit.toMillis(time)); + return lock.acquire(); + } + + @Override + public void unlock() { + lock.release(); + } + + @Override + public Condition newCondition() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/io/jboot/service/JbootServiceBase.java b/src/main/java/io/jboot/service/JbootServiceBase.java index 38ccdc114b63e7f70ee90a3453f8fe8ba66f3bdd..864dd76e16d25ec83c54f5e90e76a9869321502d 100644 --- a/src/main/java/io/jboot/service/JbootServiceBase.java +++ b/src/main/java/io/jboot/service/JbootServiceBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,18 @@ */ package io.jboot.service; -import com.jfinal.plugin.activerecord.Model; -import com.jfinal.plugin.activerecord.Page; +import com.jfinal.kit.LogKit; +import com.jfinal.plugin.activerecord.*; import io.jboot.db.model.Columns; import io.jboot.db.model.JbootModel; -import io.jboot.exception.JbootException; import io.jboot.utils.ClassUtil; +import io.jboot.utils.ObjectFunc; +import io.jboot.utils.ObjectUtil; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -36,7 +41,7 @@ public class JbootServiceBase> protected static final int ACTION_DEL = 2; protected static final int ACTION_UPDATE = 3; - protected M DAO = null; + protected JbootModel DAO = null; public JbootServiceBase() { DAO = initDao(); @@ -49,17 +54,33 @@ public class JbootServiceBase> * @return */ protected M initDao() { - Class modelClass = ClassUtil.getGenericClass(getClass()); - if (modelClass == null) { - throw new JbootException("can not get model class name in JbootServiceBase"); + Class usefulClass = ClassUtil.getUsefulClass(getClass()); + return createDao(usefulClass); + } + + + private M createDao(Class usefulClass) { + Type type = usefulClass.getGenericSuperclass(); + if (type instanceof ParameterizedType) { + Class modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + return ClassUtil.newInstance(modelClass, false).dao(); + } + //from child class + else if (type instanceof Class) { + Class typeClass = (Class) type; + if (typeClass != JbootServiceBase.class + && typeClass != Object.class + ) { + return createDao(typeClass); + } } - //默认不通过AOP构建DAO,提升性能,若特殊需要重写initDao()方法即可 - return ClassUtil.newInstance(modelClass, false); + LogKit.warn("Not define Model class in service: " +usefulClass); + return null; } - public M getDao() { + public JbootModel getDao() { return DAO; } @@ -154,6 +175,17 @@ public class JbootServiceBase> } + /** + * 根据多个 id 查找多个对象 + * + * @param ids + * @return + */ + public List findListByIds(Object... ids) { + return DAO.findListByIds(ids); + } + + /** * 根据提交查询数据量 * @@ -296,17 +328,60 @@ public class JbootServiceBase> } + /** + * 同步 model 数据到数据库 + * + * @param columns + * @param syncModels + * @param compareAttrGetters + */ + public void syncModels(Columns columns, Collection syncModels, ObjectFunc... compareAttrGetters) { + if (columns == null) { + throw new NullPointerException("columns must not be null"); + } + + if (syncModels == null || syncModels.isEmpty()) { + DAO.deleteByColumns(columns); + return; + } + + List existModels = findListByColumns(columns); + if (existModels == null || existModels.isEmpty()) { + Db.batchSave(new ArrayList<>(syncModels), syncModels.size()); + return; + } + + + for (M existModel : existModels) { + if (!ObjectUtil.isContainsObject(syncModels, existModel, compareAttrGetters)) { + existModel.delete(); + } + } + + + for (M syncModel : syncModels) { + M existModel = ObjectUtil.getContainsObject(existModels, syncModel, compareAttrGetters); + if (existModel == null) { + syncModel.save(); + } else { + existModel._setAttrs(syncModel).update(); + } + } + } + + /** * 复写 JbootServiceJoinerImpl 的方法 * - * @param id + * @param columnValue * @return */ @Override - protected JbootModel joinById(Object id) { - return findById(id); + protected JbootModel joinByValue(Object columnValue, JbootModel sourceModel) { + return findById(columnValue); } + /** * 用于给子类复写,用于刷新缓存 * @@ -316,4 +391,10 @@ public class JbootServiceBase> */ public void shouldUpdateCache(int action, Model model, Object id) { } + + + @Override + protected List joinManyByValue(String columnName, Object value, M sourceModel) { + return (List) findListByColumns(Columns.create(columnName, value)); + } } diff --git a/src/main/java/io/jboot/service/JbootServiceJoiner.java b/src/main/java/io/jboot/service/JbootServiceJoiner.java index 125434b711fc79c4703b426dba0c91753af82c97..b083a743444119f614c41e9117ed4e9f479153db 100644 --- a/src/main/java/io/jboot/service/JbootServiceJoiner.java +++ b/src/main/java/io/jboot/service/JbootServiceJoiner.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,9 @@ */ package io.jboot.service; -import com.jfinal.plugin.activerecord.Model; import com.jfinal.plugin.activerecord.Page; +import io.jboot.db.model.JbootModel; +import io.jboot.utils.ObjectFunc; import java.util.List; @@ -26,31 +27,139 @@ import java.util.List; public interface JbootServiceJoiner { - public Page join(Page page, String joinOnField); + public Page join(Page page, String columnName); - public Page join(Page page, String joinOnField, String[] attrs); + public Page join(Page page, String columnName, String[] attrs); - public Page join(Page page, String joinOnField, String joinName); + public Page join(Page page, String columnName, String joinName); - public Page join(Page page, String joinOnField, String joinName, String[] attrs); + public Page join(Page page, String columnName, String joinName, String[] attrs); - public List join(List models, String joinOnField); + public List join(List models, String columnName); - public List join(List models, String joinOnField, String[] attrs); + public List join(List models, String columnName, String[] attrs); - public List join(List models, String joinOnField, String joinName); + public List join(List models, String columnName, String joinName); - public List join(List models, String joinOnField, String joinName, String[] attrs); + public List join(List models, String columnName, String joinName, String[] attrs); - public M join(M model, String joinOnField); + public M join(M model, String columnName); - public M join(M model, String joinOnField, String[] attrs); + public M join(M model, String columnName, String[] attrs); - public M join(M model, String joinOnField, String joinName); + public M join(M model, String columnName, String joinName); - public M join(M model, String joinOnField, String joinName, String[] attrs); + public M join(M model, String columnName, String joinName, String[] attrs); + + + public Page joinMany(Page page, String targetColumnName); + + public Page joinMany(Page page, String targetColumnName, String[] attrs); + + public Page joinMany(Page page, String targetColumnName, String joinName); + + public Page joinMany(Page page, String targetColumnName, String joinName, String[] attrs); + + + public List joinMany(List models, String targetColumnName); + + public List joinMany(List models, String targetColumnName, String[] attrs); + + public List joinMany(List models, String targetColumnName, String joinName); + + public List joinMany(List models, String targetColumnName, String joinName, String[] attrs); + + + public M joinMany(M model, String targetColumnName); + + public M joinMany(M model, String targetColumnName, String[] attrs); + + public M joinMany(M model, String targetColumnName, String joinName); + + public M joinMany(M model, String targetColumnName, String joinName, String[] attrs); + + + public Page joinMany(Page page, ObjectFunc modelValueGetter, String targetColumnName); + + public Page joinMany(Page page, ObjectFunc modelValueGetter, String targetColumnName, String[] attrs); + + public Page joinMany(Page page, ObjectFunc modelValueGetter, String targetColumnName, String joinName); + + public Page joinMany(Page page, ObjectFunc modelValueGetter, String targetColumnName, String joinName, String[] attrs); + + + public List joinMany(List models, ObjectFunc modelValueGetter, String targetColumnName); + + public List joinMany(List models, ObjectFunc modelValueGetter, String targetColumnName, String[] attrs); + + public List joinMany(List models, ObjectFunc modelValueGetter, String targetColumnName, String joinName); + + public List joinMany(List models, ObjectFunc modelValueGetter, String targetColumnName, String joinName, String[] attrs); + + + public M joinMany(M model, ObjectFunc modelValueGetter, String targetColumnName); + + public M joinMany(M model, ObjectFunc modelValueGetter, String targetColumnName, String[] attrs); + + public M joinMany(M model, ObjectFunc modelValueGetter, String targetColumnName, String joinName); + + public M joinMany(M model, ObjectFunc modelValueGetter, String targetColumnName, String joinName, String[] attrs); + + + public Page joinManyByTable(Page page, String tableName, String columnName, String targetColumnName); + + public Page joinManyByTable(Page page, String tableName, String columnName, String targetColumnName, String[] attrs); + + public Page joinManyByTable(Page page, String tableName, String columnName, String targetColumnName, String joinName); + + public Page joinManyByTable(Page page, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs); + + + public List joinManyByTable(List models, String tableName, String columnName, String targetColumnName); + + public List joinManyByTable(List models, String tableName, String columnName, String targetColumnName, String[] attrs); + + public List joinManyByTable(List models, String tableName, String columnName, String targetColumnName, String joinName); + + public List joinManyByTable(List models, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs); + + + public M joinManyByTable(M model, String tableName, String columnName, String targetColumnName); + + public M joinManyByTable(M model, String tableName, String columnName, String targetColumnName, String[] attrs); + + public M joinManyByTable(M model, String tableName, String columnName, String targetColumnName, String joinName); + + public M joinManyByTable(M model, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs); + + + public Page joinManyByTable(Page page, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName); + + public Page joinManyByTable(Page page, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String[] attrs); + + public Page joinManyByTable(Page page, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName); + + public Page joinManyByTable(Page page, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs); + + + public List joinManyByTable(List models, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName); + + public List joinManyByTable(List models, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String[] attrs); + + public List joinManyByTable(List models, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName); + + public List joinManyByTable(List models, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs); + + + public M joinManyByTable(M model, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName); + + public M joinManyByTable(M model, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String[] attrs); + + public M joinManyByTable(M model, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName); + + public M joinManyByTable(M model, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs); } diff --git a/src/main/java/io/jboot/service/JbootServiceJoinerImpl.java b/src/main/java/io/jboot/service/JbootServiceJoinerImpl.java index c4ee9c7ad67a22b28fa2ac6cae1d92c0ee9bea6d..07cb53c5895078dd7b6a8394fe047b2e104f2cfd 100644 --- a/src/main/java/io/jboot/service/JbootServiceJoinerImpl.java +++ b/src/main/java/io/jboot/service/JbootServiceJoinerImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,16 @@ package io.jboot.service; import com.jfinal.kit.StrKit; -import com.jfinal.plugin.activerecord.Model; import com.jfinal.plugin.activerecord.Page; +import com.jfinal.plugin.activerecord.Record; +import io.jboot.db.JbootDb; +import io.jboot.db.model.Columns; import io.jboot.db.model.JbootModel; import io.jboot.utils.ArrayUtil; +import io.jboot.utils.ObjectFunc; +import io.jboot.utils.StrUtil; +import java.util.ArrayList; import java.util.List; @@ -28,37 +33,37 @@ public abstract class JbootServiceJoinerImpl implements JbootServiceJoiner { @Override - public Page join(Page page, String joinOnField) { - join(page.getList(), joinOnField); + public Page join(Page page, String columnName) { + join(page.getList(), columnName); return page; } @Override - public Page join(Page page, String joinOnField, String[] attrs) { - join(page.getList(), joinOnField, attrs); + public Page join(Page page, String columnName, String[] attrs) { + join(page.getList(), columnName, attrs); return page; } @Override - public Page join(Page page, String joinOnField, String joinName) { - join(page.getList(), joinOnField, joinName); + public Page join(Page page, String columnName, String joinName) { + join(page.getList(), columnName, joinName); return page; } @Override - public Page join(Page page, String joinOnField, String joinName, String[] attrs) { - join(page.getList(), joinOnField, joinName, attrs); + public Page join(Page page, String columnName, String joinName, String[] attrs) { + join(page.getList(), columnName, joinName, attrs); return page; } @Override - public List join(List models, String joinOnField) { + public List join(List models, String columnName) { if (ArrayUtil.isNotEmpty(models)) { - for (Model m : models) { - join(m, joinOnField); + for (JbootModel m : models) { + join(m, columnName); } } return models; @@ -66,10 +71,10 @@ public abstract class JbootServiceJoinerImpl implements JbootServiceJoiner { @Override - public List join(List models, String joinOnField, String[] attrs) { + public List join(List models, String columnName, String[] attrs) { if (ArrayUtil.isNotEmpty(models)) { - for (Model m : models) { - join(m, joinOnField, attrs); + for (JbootModel m : models) { + join(m, columnName, attrs); } } return models; @@ -77,10 +82,10 @@ public abstract class JbootServiceJoinerImpl implements JbootServiceJoiner { @Override - public List join(List models, String joinOnField, String joinName) { + public List join(List models, String columnName, String joinName) { if (ArrayUtil.isNotEmpty(models)) { - for (Model m : models) { - join(m, joinOnField, joinName); + for (JbootModel m : models) { + join(m, columnName, joinName); } } return models; @@ -88,10 +93,10 @@ public abstract class JbootServiceJoinerImpl implements JbootServiceJoiner { @Override - public List join(List models, String joinOnField, String joinName, String[] attrs) { + public List join(List models, String columnName, String joinName, String[] attrs) { if (ArrayUtil.isNotEmpty(models)) { - for (Model m : models) { - join(m, joinOnField, joinName, attrs); + for (JbootModel m : models) { + join(m, columnName, joinName, attrs); } } return models; @@ -101,46 +106,24 @@ public abstract class JbootServiceJoinerImpl implements JbootServiceJoiner { /** * 添加关联数据到某个model中去,避免关联查询,提高性能。 * - * @param model 要添加到的model - * @param joinOnField model对于的关联字段 + * @param model 要添加到的model + * @param columnName model对于的关联字段 */ @Override - public M join(M model, String joinOnField) { - if (model == null) - return model; - Object id = model.get(joinOnField); - if (id == null) { - return model; - } - Model m = joinById(id); - if (m != null) { - model.put(StrKit.firstCharToLowerCase(m.getClass().getSimpleName()), m); - } - return model; + public M join(M model, String columnName) { + return join(model, columnName, null, null); } /** * 添加关联数据到某个model中去,避免关联查询,提高性能。 * * @param model - * @param joinOnField + * @param columnName * @param attrs */ @Override - public M join(M model, String joinOnField, String[] attrs) { - if (model == null) - return model; - Object id = model.get(joinOnField); - if (id == null) { - return model; - } - JbootModel m = joinById(id); - if (m != null) { - m = m.copy(); - m.keep(attrs); - model.put(StrKit.firstCharToLowerCase(m.getClass().getSimpleName()), m); - } - return model; + public M join(M model, String columnName, String[] attrs) { + return join(model, columnName, null, attrs); } @@ -148,22 +131,12 @@ public abstract class JbootServiceJoinerImpl implements JbootServiceJoiner { * 添加关联数据到某个model中去,避免关联查询,提高性能。 * * @param model - * @param joinOnField + * @param columnName * @param joinName */ @Override - public M join(M model, String joinOnField, String joinName) { - if (model == null) - return model; - Object id = model.get(joinOnField); - if (id == null) { - return model; - } - Model m = joinById(id); - if (m != null) { - model.put(joinName, m); - } - return model; + public M join(M model, String columnName, String joinName) { + return join(model, columnName, joinName, null); } @@ -171,35 +144,482 @@ public abstract class JbootServiceJoinerImpl implements JbootServiceJoiner { * 添加关联数据到某个model中去,避免关联查询,提高性能。 * * @param model - * @param joinOnField + * @param columnName * @param joinName * @param attrs */ @Override - public M join(M model, String joinOnField, String joinName, String[] attrs) { - if (model == null) - return model; - Object id = model.get(joinOnField); - if (id == null) { + public M join(M model, String columnName, String joinName, String[] attrs) { + if (model == null) { + return null; + } + Object value = model.get(columnName); + if (value == null) { return model; } - JbootModel m = joinById(id); + JbootModel m = joinByValue(value, model); if (m != null) { - m = m.copy(); - m.keep(attrs); - model.put(joinName, m); + joinName = StrUtil.isNotBlank(joinName) ? joinName : StrKit.firstCharToLowerCase(m.getClass().getSimpleName()); + model.put(joinName, ArrayUtil.isNotEmpty(attrs) ? m.copy().keep(attrs) : m); } return model; } /** - * 可以让子类去复写joinById ,比如默认只 join 部分字段等 + * 可以让子类去复写 joinByColumnValue ,比如默认只 join 部分字段等,或者不是根据主键进行查询等 + * 一般情况下,传入的 columnValue 是主键的值,但是也有可能不是,要看场景,如果不是的情况下可以通过 sourceModel 来进行判断 * - * @param id + * @param columnValue * @return */ - protected abstract JbootModel joinById(Object id); + protected abstract JbootModel joinByValue(Object columnValue, JbootModel sourceModel); + + +/////////////////joinMany start///////////////////////////// + + + @Override + public Page joinMany(Page page, String targetColumnName) { + joinMany(page.getList(), targetColumnName); + return page; + } + + @Override + public Page joinMany(Page page, String targetColumnName, String[] attrs) { + joinMany(page.getList(), targetColumnName, attrs); + return page; + } + + + @Override + public Page joinMany(Page page, String targetColumnName, String joinName) { + joinMany(page.getList(), targetColumnName, joinName); + return page; + } + + + @Override + public Page joinMany(Page page, String targetColumnName, String joinName, String[] attrs) { + joinMany(page.getList(), targetColumnName, joinName, attrs); + return page; + } + + + @Override + public List joinMany(List models, String targetColumnName) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinMany(m, targetColumnName); + } + } + return models; + } + + + @Override + public List joinMany(List models, String targetColumnName, String[] attrs) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinMany(m, targetColumnName, attrs); + } + } + return models; + } + + + @Override + public List joinMany(List models, String targetColumnName, String joinName) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinMany(m, targetColumnName, joinName); + } + } + return models; + } + + + @Override + public List joinMany(List models, String targetColumnName, String joinName, String[] attrs) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinMany(m, targetColumnName, joinName, attrs); + } + } + return models; + } + + + @Override + public M joinMany(M model, String targetColumnName) { + return joinMany(model, null, targetColumnName, null, null); + } + + @Override + public M joinMany(M model, String targetColumnName, String[] attrs) { + return joinMany(model, null, targetColumnName, null, attrs); + } + + @Override + public M joinMany(M model, String targetColumnName, String joinName) { + return joinMany(model, null, targetColumnName, joinName, null); + } + + + @Override + public M joinMany(M model, String targetColumnName, String joinName, String[] attrs) { + return joinMany(model, null, targetColumnName, joinName, attrs); + } + + + @Override + public Page joinMany(Page page, ObjectFunc modelValueGetter, String targetColumnName) { + joinMany(page.getList(), modelValueGetter, targetColumnName); + return page; + } + + @Override + public Page joinMany(Page page, ObjectFunc modelValueGetter, String targetColumnName, String[] attrs) { + joinMany(page.getList(), modelValueGetter, targetColumnName, attrs); + return page; + } + + + @Override + public Page joinMany(Page page, ObjectFunc modelValueGetter, String targetColumnName, String joinName) { + joinMany(page.getList(), modelValueGetter, targetColumnName, joinName); + return page; + } + + + @Override + public Page joinMany(Page page, ObjectFunc modelValueGetter, String targetColumnName, String joinName, String[] attrs) { + joinMany(page.getList(), modelValueGetter, targetColumnName, joinName, attrs); + return page; + } + + + @Override + public List joinMany(List models, ObjectFunc modelValueGetter, String targetColumnName) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinMany(m, modelValueGetter, targetColumnName); + } + } + return models; + } + + + @Override + public List joinMany(List models, ObjectFunc modelValueGetter, String targetColumnName, String[] attrs) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinMany(m, modelValueGetter, targetColumnName, attrs); + } + } + return models; + } + + + @Override + public List joinMany(List models, ObjectFunc modelValueGetter, String targetColumnName, String joinName) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinMany(m, modelValueGetter, targetColumnName, joinName); + } + } + return models; + } + + + @Override + public List joinMany(List models, ObjectFunc modelValueGetter, String targetColumnName, String joinName, String[] attrs) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinMany(m, modelValueGetter, targetColumnName, joinName, attrs); + } + } + return models; + } + + + @Override + public M joinMany(M model, ObjectFunc modelValueGetter, String targetColumnName) { + return joinMany(model, modelValueGetter, targetColumnName, null, null); + } + + @Override + public M joinMany(M model, ObjectFunc modelValueGetter, String targetColumnName, String[] attrs) { + return joinMany(model, modelValueGetter, targetColumnName, null, attrs); + } + + + @Override + public M joinMany(M model, ObjectFunc modelValueGetter, String targetColumnName, String joinName) { + return joinMany(model, modelValueGetter, targetColumnName, joinName, null); + } + + + @Override + public M joinMany(M model, ObjectFunc modelValueGetter, String targetColumnName, String joinName, String[] attrs) { + if (model == null) { + return null; + } + Object value = modelValueGetter != null ? modelValueGetter.get(model) : model._getIdValue(); + if (value == null) { + return model; + } + + List list = joinManyByValue(targetColumnName, value, model); + if (list != null && !list.isEmpty()) { + joinName = StrUtil.isNotBlank(joinName) ? joinName : StrKit.firstCharToLowerCase(list.get(0).getClass().getSimpleName()) + "List"; + model.put(joinName, ArrayUtil.isNotEmpty(attrs) ? keepModelListAttrs(list, attrs) : list); + } + + return model; + } + + + protected List keepModelListAttrs(List list, String[] attrs) { + if (list == null || list.isEmpty()) { + return null; + } + List retList = new ArrayList<>(list.size()); + for (M model : list) { + retList.add((M) model.copy().keep(attrs)); + } + return retList; + } + + + protected abstract List joinManyByValue(String columnName, Object value, M sourceModel); + + +/////////////////joinMany end///////////////////////////// + + +/////////////////joinManyByTable start///////////////////////////// + + @Override + public Page joinManyByTable(Page page, String tableName, String columnName, String targetColumnName) { + joinManyByTable(page.getList(), tableName, columnName, targetColumnName); + return page; + } + + @Override + public Page joinManyByTable(Page page, String tableName, String columnName, String targetColumnName, String[] attrs) { + joinManyByTable(page.getList(), tableName, columnName, targetColumnName, attrs); + return page; + } + + @Override + public Page joinManyByTable(Page page, String tableName, String columnName, String targetColumnName, String joinName) { + joinManyByTable(page.getList(), tableName, columnName, targetColumnName, joinName); + return page; + } + + + @Override + public Page joinManyByTable(Page page, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs) { + joinManyByTable(page.getList(), tableName, columnName, targetColumnName, joinName, attrs); + return page; + } + + + @Override + public List joinManyByTable(List models, String tableName, String columnName, String targetColumnName) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinManyByTable(m, tableName, columnName, targetColumnName); + } + } + return models; + } + + @Override + public List joinManyByTable(List models, String tableName, String columnName, String targetColumnName, String[] attrs) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinManyByTable(m, tableName, columnName, targetColumnName, attrs); + } + } + return models; + } + + @Override + public List joinManyByTable(List models, String tableName, String columnName, String targetColumnName, String joinName) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinManyByTable(m, tableName, columnName, targetColumnName, joinName); + } + } + return models; + } + + + @Override + public List joinManyByTable(List models, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinManyByTable(m, tableName, columnName, targetColumnName, joinName, attrs); + } + } + return models; + } + + + @Override + public M joinManyByTable(M model, String tableName, String columnName, String targetColumnName) { + return joinManyByTable(model, tableName, columnName, targetColumnName, null, null); + } + + @Override + public M joinManyByTable(M model, String tableName, String columnName, String targetColumnName, String[] attrs) { + return joinManyByTable(model, tableName, columnName, targetColumnName, null, attrs); + } + + @Override + public M joinManyByTable(M model, String tableName, String columnName, String targetColumnName, String joinName) { + return joinManyByTable(model, tableName, columnName, targetColumnName, joinName, null); + } + + @Override + public M joinManyByTable(M model, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs) { + return joinManyByTable(model, null, tableName, columnName, targetColumnName, joinName, attrs); + } + + + @Override + public Page joinManyByTable(Page page, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName) { + joinManyByTable(page.getList(), modelValueGetter, tableName, columnName, targetColumnName); + return page; + } + + @Override + public Page joinManyByTable(Page page, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String[] attrs) { + joinManyByTable(page.getList(), modelValueGetter, tableName, columnName, targetColumnName, attrs); + return page; + } + + @Override + public Page joinManyByTable(Page page, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName) { + joinManyByTable(page.getList(), modelValueGetter, tableName, columnName, targetColumnName, joinName); + return page; + } + + + @Override + public Page joinManyByTable(Page page, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs) { + joinManyByTable(page.getList(), modelValueGetter, tableName, columnName, targetColumnName, joinName, attrs); + return page; + } + + + @Override + public List joinManyByTable(List models, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinManyByTable(m, modelValueGetter, tableName, columnName, targetColumnName); + } + } + return models; + } + + @Override + public List joinManyByTable(List models, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String[] attrs) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinManyByTable(m, modelValueGetter, tableName, columnName, targetColumnName, attrs); + } + } + return models; + } + + @Override + public List joinManyByTable(List models, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinManyByTable(m, modelValueGetter, tableName, columnName, targetColumnName, joinName); + } + } + return models; + } + + + @Override + public List joinManyByTable(List models, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs) { + if (ArrayUtil.isNotEmpty(models)) { + for (M m : models) { + joinManyByTable(m, modelValueGetter, tableName, columnName, targetColumnName, joinName, attrs); + } + } + return models; + } + + + @Override + public M joinManyByTable(M model, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName) { + return joinManyByTable(model, modelValueGetter, tableName, columnName, targetColumnName, null, null); + } + + @Override + public M joinManyByTable(M model, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String[] attrs) { + return joinManyByTable(model, modelValueGetter, tableName, columnName, targetColumnName, null, attrs); + } + + @Override + public M joinManyByTable(M model, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName) { + return joinManyByTable(model, modelValueGetter, tableName, columnName, targetColumnName, joinName, null); + } + + @Override + public M joinManyByTable(M model, ObjectFunc modelValueGetter, String tableName, String columnName, String targetColumnName, String joinName, String[] attrs) { + if (model == null) { + return null; + } + + Object columnValue = modelValueGetter != null ? modelValueGetter.get(model) : model._getIdValue(); + if (columnValue == null) { + return model; + } + + List middleTableRecords = findMiddleTableRecords(tableName, columnName, columnValue); + if (middleTableRecords == null || middleTableRecords.isEmpty()) { + return model; + } + + List list = new ArrayList(); + for (Record record : middleTableRecords) { + Object targetTableValue = record.get(targetColumnName); + if (targetTableValue != null) { + M data = (M) joinByValue(targetTableValue, model); + if (data != null) { + list.add(data); + } + } + } + + if (!list.isEmpty()) { + joinName = StrUtil.isNotBlank(joinName) ? joinName : StrKit.firstCharToLowerCase(list.get(0).getClass().getSimpleName()) + "List"; + model.put(joinName, ArrayUtil.isNotEmpty(attrs) ? keepModelListAttrs(list, attrs) : list); + } + + return model; + } + + + /** + * 查询中间表数据,方便子类复写,比如:通过缓存获取等 + * @param tableName + * @param columnName + * @param columnValue + * @return + */ + protected List findMiddleTableRecords(String tableName, String columnName, Object columnValue) { + return JbootDb.find(tableName, Columns.create(columnName, columnValue)); + } + +/////////////////joinManyByTable end///////////////////////////// } diff --git a/src/main/java/io/jboot/web/fixedinterceptor/FixedInterceptor.java b/src/main/java/io/jboot/support/jwt/EnableJwt.java similarity index 67% rename from src/main/java/io/jboot/web/fixedinterceptor/FixedInterceptor.java rename to src/main/java/io/jboot/support/jwt/EnableJwt.java index 7e6c887db21afa6d40e5db250afc5374841f9432..7dded5ed7efff9c8070b4595756a6892e8e5d96c 100644 --- a/src/main/java/io/jboot/web/fixedinterceptor/FixedInterceptor.java +++ b/src/main/java/io/jboot/support/jwt/EnableJwt.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.web.fixedinterceptor; +package io.jboot.support.jwt; -import com.jfinal.aop.Invocation; +import java.lang.annotation.*; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) - * @title 不会被 @Clear 清除掉的 拦截器 - * @version V1.0 - * @Package io.jboot.web.handler */ -public interface FixedInterceptor { - void intercept(Invocation inv); +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface EnableJwt { + } diff --git a/src/main/java/io/jboot/support/jwt/JwtConfig.java b/src/main/java/io/jboot/support/jwt/JwtConfig.java index a68764111899d6a112187f1370c02d80242c6b37..7d7cdfefda119a762a5cf46513ee642b7b043c25 100644 --- a/src/main/java/io/jboot/support/jwt/JwtConfig.java +++ b/src/main/java/io/jboot/support/jwt/JwtConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,13 @@ import io.jboot.utils.StrUtil; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.jwt */ @ConfigModel(prefix = "jboot.web.jwt") public class JwtConfig { private String httpHeaderName = "Jwt"; + private String httpParameterKey; + private String secret; /** @@ -43,6 +44,14 @@ public class JwtConfig { this.httpHeaderName = httpHeaderName; } + public String getHttpParameterKey() { + return httpParameterKey; + } + + public void setHttpParameterKey(String httpParameterKey) { + this.httpParameterKey = httpParameterKey; + } + public String getSecret() { return secret; } diff --git a/src/main/java/io/jboot/support/jwt/JwtInterceptor.java b/src/main/java/io/jboot/support/jwt/JwtInterceptor.java index 94412627eb12effccb479a5e71ac10d3e080b339..988d3f4a6fb67d107d09a86b9cdc5ae8f7d2e994 100644 --- a/src/main/java/io/jboot/support/jwt/JwtInterceptor.java +++ b/src/main/java/io/jboot/support/jwt/JwtInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,11 @@ */ package io.jboot.support.jwt; +import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; -import io.jboot.utils.StrUtil; +import com.jfinal.core.Controller; import io.jboot.web.controller.JbootController; -import io.jboot.web.fixedinterceptor.FixedInterceptor; -import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; @@ -28,85 +27,69 @@ import java.util.Map; * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 * @Title: 用于对Jwt的设置 - * @Package io.jboot.web.jwt */ -public class JwtInterceptor implements FixedInterceptor { +public class JwtInterceptor implements Interceptor { public static final String ISUUED_AT = "isuuedAt"; @Override public void intercept(Invocation inv) { - if (!JwtManager.me().getConfig().isConfigOk()) { - inv.invoke(); - return; - } - - HttpServletRequest request = inv.getController().getRequest(); - String token = request.getHeader(JwtManager.me().getHttpHeaderName()); - - if (StrUtil.isBlank(token)) { - processInvoke(inv, null); - return; - } - - Map map = JwtManager.me().parseJwtToken(token); - if (map == null) { - processInvoke(inv, null); - return; - } - try { - JwtManager.me().holdJwts(map); - processInvoke(inv, map); + inv.invoke(); } finally { - JwtManager.me().releaseJwts(); - } - } - - private void processInvoke(Invocation inv, Map oldData) { + JbootController jbootController = (JbootController) inv.getController(); + Map jwtAttrs = jbootController.getJwtAttrs(); - inv.invoke(); - - - if (!(inv.getController() instanceof JbootController)) { - return; - } - - JbootController jbootController = (JbootController) inv.getController(); - Map jwtMap = jbootController.getJwtAttrs(); - - - if (jwtMap == null || jwtMap.isEmpty()) { - refreshIfNecessary(inv, oldData); - return; + if (jwtAttrs == null) { + refreshIfNecessary(jbootController, jbootController.getJwtParas()); + } else { + responseJwt(jbootController, jwtAttrs); + } } - - String token = JwtManager.me().createJwtToken(jwtMap); - HttpServletResponse response = inv.getController().getResponse(); - response.addHeader(JwtManager.me().getHttpHeaderName(), token); } - private void refreshIfNecessary(Invocation inv, Map oldData) { - if (oldData == null) { + /** + * 对 jwt 内容进行刷新 + * @param controller + * @param oldData + */ + private void refreshIfNecessary(Controller controller, Map oldData) { + if (oldData == null || oldData.isEmpty()) { return; } // Jwt token 的发布时间 Long isuuedAtMillis = (Long) oldData.get(ISUUED_AT); - if (isuuedAtMillis == null || JwtManager.me().getConfig().getValidityPeriod() <= 0) { + + // 有效期 + long validityPeriod = JwtManager.getConfig().getValidityPeriod(); + + // 永久有效,没必要刷新 Jwt + if (isuuedAtMillis == null || validityPeriod <= 0) { return; } - Long nowMillis = System.currentTimeMillis(); - long savedMillis = nowMillis - isuuedAtMillis; + // 已经发布的时间 + long savedMillis = System.currentTimeMillis() - isuuedAtMillis; - if (savedMillis > JwtManager.me().getConfig().getValidityPeriod() / 2) { - String token = JwtManager.me().createJwtToken(oldData); - HttpServletResponse response = inv.getController().getResponse(); - response.addHeader(JwtManager.me().getHttpHeaderName(), token); + // 已经发布的时间 大于有效期的一半,重新刷新 + if (savedMillis > validityPeriod / 2) { + responseJwt(controller, oldData); } + } + + /** + * 输出 jwt 内容到客户端 + * + * @param controller + * @param map + */ + private void responseJwt(Controller controller, Map map) { + String token = JwtManager.me().createJwtToken(map); + HttpServletResponse response = controller.getResponse(); + response.addHeader(JwtManager.me().getHttpHeaderName(), token); } } diff --git a/src/main/java/io/jboot/support/jwt/JwtInterceptorBuilder.java b/src/main/java/io/jboot/support/jwt/JwtInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..edee2f514bf84035a875045579a58a3376007c64 --- /dev/null +++ b/src/main/java/io/jboot/support/jwt/JwtInterceptorBuilder.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.jwt; + +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; + +import java.lang.reflect.Method; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +@AutoLoad +public class JwtInterceptorBuilder implements InterceptorBuilder { + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.isJbootController(targetClass) && Util.hasAnnotation(targetClass, method, EnableJwt.class)) { + interceptors.addToFirst(JwtInterceptor.class); + } + } + +} diff --git a/src/main/java/io/jboot/support/jwt/JwtManager.java b/src/main/java/io/jboot/support/jwt/JwtManager.java index e272532859bafecbfe3bd342a7026692e18d36b7..7fdd21f0962da708518c9804dde917b16aea18a2 100644 --- a/src/main/java/io/jboot/support/jwt/JwtManager.java +++ b/src/main/java/io/jboot/support/jwt/JwtManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,12 @@ */ package io.jboot.support.jwt; -import com.alibaba.fastjson.JSON; +import com.jfinal.core.Controller; +import com.jfinal.kit.JsonKit; +import com.jfinal.kit.LogKit; +import com.jfinal.log.Log; import io.jboot.Jboot; -import io.jboot.exception.JbootException; +import io.jboot.exception.JbootIllegalConfigException; import io.jboot.utils.StrUtil; import io.jsonwebtoken.*; @@ -31,88 +34,106 @@ import java.util.Map; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.jwt */ public class JwtManager { private static final JwtManager me = new JwtManager(); + private static final Log LOG = Log.getLog(JwtManager.class); + public static final Map EMPTY_MAP = new HashMap(); + private static JwtConfig config = Jboot.config(JwtConfig.class); + public static JwtManager me() { return me; } - private ThreadLocal jwtThreadLocal = new ThreadLocal<>(); - - public void holdJwts(Map map) { - jwtThreadLocal.set(map); + public String getHttpHeaderName() { + return config.getHttpHeaderName(); } - public void releaseJwts() { - jwtThreadLocal.remove(); + public String getHttpParameterKey() { + return config.getHttpParameterKey(); } - public T getPara(String key) { - Map map = jwtThreadLocal.get(); - return map == null ? null : (T) map.get(key); - } + /** + * 通过 Controller 解析 Map + * + * @param controller 控制器 + * @return 所有 JWT 数据 + */ + public Map parseJwtToken(Controller controller) { + + if (!config.isConfigOk()) { + LogKit.debug("Jwt secret not config well, please config jboot.web.jwt.secret in jboot.properties."); + return EMPTY_MAP; + } - public Map getParas() { - return jwtThreadLocal.get(); - } + String token = controller.getHeader(getHttpHeaderName()); - public String getHttpHeaderName() { - return getConfig().getHttpHeaderName(); + if (StrUtil.isBlank(token) && StrUtil.isNotBlank(getHttpParameterKey())) { + token = controller.get(getHttpParameterKey()); + } + + return StrUtil.isBlank(token) ? EMPTY_MAP : parseJwtToken(token); } + + /** + * 解析 JWT Token 内容 + * + * @param token 加密的 token + * @return 返回 JWT 的 MAP 数据 + */ public Map parseJwtToken(String token) { - SecretKey secretKey = generalKey(); + SecretKey secretKey = createSecretKey(); try { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token).getBody(); String jsonString = claims.getSubject(); - - if (StrUtil.isBlank(jsonString)) { - return null; + if (StrUtil.isNotBlank(jsonString)) { + return JsonKit.parse(jsonString, HashMap.class); } - - return JSON.parseObject(jsonString, HashMap.class); - - } catch (SignatureException | MalformedJwtException e) { + } catch (SignatureException | MalformedJwtException ex) { // don't trust the JWT! // jwt 签名错误或解析错误,可能是伪造的,不能相信 - } catch (ExpiredJwtException e) { + LOG.error("Do not trast the jwt. " + ex.getMessage()); + } catch (ExpiredJwtException ex) { // jwt 已经过期 - } catch (Throwable ex) { + LOG.error("Jwt is expired. " + ex.getMessage()); + } catch (Exception ex) { //其他错误 + LOG.error("Jwt parseJwtToken error. " + ex.getMessage()); } - return null; + return EMPTY_MAP; } + public String createJwtToken(Map map) { - if (!getConfig().isConfigOk()) { - throw new JbootException("can not create jwt, please config jboot.web.jwt.secret in jboot.properties."); + if (!config.isConfigOk()) { + throw new JbootIllegalConfigException("Can not create jwt, please config jboot.web.jwt.secret in jboot.properties."); } - SecretKey secretKey = generalKey(); + SecretKey secretKey = createSecretKey(); SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); + //追加保存 JWT 的生成时间 map.put(JwtInterceptor.ISUUED_AT, nowMillis); - String subject = JSON.toJSONString(map); + String subject = JsonKit.toJson(map); JwtBuilder builder = Jwts.builder() .setIssuedAt(now) .setSubject(subject) .signWith(signatureAlgorithm, secretKey); - if (getConfig().getValidityPeriod() > 0) { - long expMillis = nowMillis + getConfig().getValidityPeriod(); + if (config.getValidityPeriod() > 0) { + long expMillis = nowMillis + config.getValidityPeriod(); builder.setExpiration(new Date(expMillis)); } @@ -120,20 +141,16 @@ public class JwtManager { } - private SecretKey generalKey() { - byte[] encodedKey = DatatypeConverter.parseBase64Binary(getConfig().getSecret()); - SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); - return key; + private SecretKey createSecretKey() { + byte[] encodedKey = DatatypeConverter.parseBase64Binary(config.getSecret()); + return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); } - private JwtConfig config; - - public JwtConfig getConfig() { - if (config == null) { - config = Jboot.config(JwtConfig.class); - } + public static JwtConfig getConfig() { return config; } - + public static void setConfig(JwtConfig config) { + JwtManager.config = config; + } } diff --git a/src/main/java/io/jboot/support/metric/JbootHealthCheckServletContextListener.java b/src/main/java/io/jboot/support/metric/JbootHealthCheckServletContextListener.java index 3363bfe11473b5d68da6e0c5d1718ba6f93fa97e..d4e5261a4a05a2d810306eeac99fc6901ac58399 100644 --- a/src/main/java/io/jboot/support/metric/JbootHealthCheckServletContextListener.java +++ b/src/main/java/io/jboot/support/metric/JbootHealthCheckServletContextListener.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/JbootMetricConfig.java b/src/main/java/io/jboot/support/metric/JbootMetricConfig.java index 60cd0efc3b28c7aee923edf3baae006d44c942eb..622baf87b8c801eae6ef49045c4e7e63a3588959 100644 --- a/src/main/java/io/jboot/support/metric/JbootMetricConfig.java +++ b/src/main/java/io/jboot/support/metric/JbootMetricConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.jboot.support.metric; import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.utils.StrUtil; @ConfigModel(prefix = "jboot.metric") public class JbootMetricConfig { @@ -28,26 +29,36 @@ public class JbootMetricConfig { public static final String REPORTER_CONSOLE = "console"; public static final String REPORTER_CSV = "csv"; public static final String REPORTER_SLF4J = "slf4j"; + public static final String REPORTER_PROMETHEUS= "prometheus"; - private String url; - private String reporter; + private boolean enable = false; - public String getMappingUrl() { - //在metrics中,会访问到配置的二级目录,必须添加下 /* 才能正常访问 - if (url != null && !url.endsWith("/*")) { - return url + "/*"; - } - return url; + private String adminServletMapping; + private String reporter = REPORTER_CONSOLE; + + //是否启用 jvm 监控 + private boolean jvmMetricEnable = true; + + //是否启用请求监控 + private boolean requestMetricEnable = true; + private String requestMetricName = "jboot-http"; + + + public boolean isEnable() { + return enable; } - public String getUrl() { - return url; + public void setEnable(boolean enable) { + this.enable = enable; } - public void setUrl(String url) { - this.url = url; + public String getAdminServletMapping() { + return adminServletMapping; } + public void setAdminServletMapping(String adminServletMapping) { + this.adminServletMapping = adminServletMapping; + } public String getReporter() { return reporter; @@ -57,8 +68,32 @@ public class JbootMetricConfig { this.reporter = reporter; } + public boolean isJvmMetricEnable() { + return jvmMetricEnable; + } + + public void setJvmMetricEnable(boolean jvmMetricEnable) { + this.jvmMetricEnable = jvmMetricEnable; + } + + public boolean isRequestMetricEnable() { + return requestMetricEnable; + } + + public void setRequestMetricEnable(boolean requestMetricEnable) { + this.requestMetricEnable = requestMetricEnable; + } + + public String getRequestMetricName() { + return requestMetricName; + } + + public void setRequestMetricName(String requestMetricName) { + this.requestMetricName = requestMetricName; + } + public boolean isConfigOk() { - return url != null && reporter != null; + return StrUtil.isNotBlank(reporter); } } diff --git a/src/main/java/io/jboot/support/metric/JbootMetricInterceptor.java b/src/main/java/io/jboot/support/metric/JbootMetricInterceptor.java deleted file mode 100644 index 6f64acef992586462715d11111057caff3a2ef75..0000000000000000000000000000000000000000 --- a/src/main/java/io/jboot/support/metric/JbootMetricInterceptor.java +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.support.metric; - -import com.codahale.metrics.Counter; -import com.codahale.metrics.Histogram; -import com.codahale.metrics.Meter; -import com.codahale.metrics.Timer; -import com.jfinal.aop.Interceptor; -import com.jfinal.aop.Invocation; -import io.jboot.Jboot; -import io.jboot.support.metric.annotation.*; -import io.jboot.utils.AnnotationUtil; -import io.jboot.utils.StrUtil; -import io.jboot.web.fixedinterceptor.FixedInterceptor; - -/** - * 用于对controller的Metrics 统计 - * 注意:如果 Controller通过 @Clear 来把此 拦截器给清空,那么此方法(action)注入将会失效 - */ -public class JbootMetricInterceptor implements Interceptor, FixedInterceptor { - - private static JbootMetricConfig config = Jboot.config(JbootMetricConfig.class); - - - @Override - public void intercept(Invocation inv) { - - if (!config.isConfigOk()) { - inv.invoke(); - return; - } - - - Timer.Context timerContext = null; - EnableMetricCounter counterAnnotation = inv.getMethod().getAnnotation(EnableMetricCounter.class); - if (counterAnnotation != null) { - String value = AnnotationUtil.get(counterAnnotation.value()); - String name = StrUtil.isBlank(value) - ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".counter" - : value; - - - Counter counter = Jboot.getMetric().counter(name); - counter.inc(); - } - - - Counter concurrencyRecord = null; - EnableMetricConcurrency concurrencyAnnotation = inv.getMethod().getAnnotation(EnableMetricConcurrency.class); - if (concurrencyAnnotation != null) { - String value = AnnotationUtil.get(concurrencyAnnotation.value()); - String name = StrUtil.isBlank(value) - ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".concurrency" - : value; - - - concurrencyRecord = Jboot.getMetric().counter(name); - concurrencyRecord.inc(); - } - - - EnableMetricMeter meterAnnotation = inv.getMethod().getAnnotation(EnableMetricMeter.class); - if (meterAnnotation != null) { - String value = AnnotationUtil.get(meterAnnotation.value()); - String name = StrUtil.isBlank(value) - ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".meter" - : value; - - - Meter meter = Jboot.getMetric().meter(name); - meter.mark(); - } - - - EnableMetricHistogram histogramAnnotation = inv.getMethod().getAnnotation(EnableMetricHistogram.class); - if (histogramAnnotation != null) { - String value = AnnotationUtil.get(histogramAnnotation.value()); - String name = StrUtil.isBlank(value) - ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".histogram" - : value; - - - Histogram histogram = Jboot.getMetric().histogram(name); - histogram.update(histogramAnnotation.update()); - } - - - EnableMetricTimer timerAnnotation = inv.getMethod().getAnnotation(EnableMetricTimer.class); - if (timerAnnotation != null) { - String value = AnnotationUtil.get(timerAnnotation.value()); - String name = StrUtil.isBlank(value) - ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".timer" - : value; - - - Timer timer = Jboot.getMetric().timer(name); - timerContext = timer.time(); - } - - - try { - inv.invoke(); - } finally { - if (concurrencyRecord != null) { - concurrencyRecord.dec(); - } - if (timerContext != null) { - timerContext.stop(); - } - } - } - - -} diff --git a/src/main/java/io/jboot/support/metric/JbootMetricManager.java b/src/main/java/io/jboot/support/metric/JbootMetricManager.java index ede0aa392843006542a8191ec6206817ad5d0ff8..1473809354828afab562e1c922e312b1b75c51bd 100644 --- a/src/main/java/io/jboot/support/metric/JbootMetricManager.java +++ b/src/main/java/io/jboot/support/metric/JbootMetricManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,14 @@ */ package io.jboot.support.metric; +import com.codahale.metrics.Gauge; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.health.HealthCheckRegistry; +import com.codahale.metrics.health.jvm.ThreadDeadlockHealthCheck; +import com.codahale.metrics.jvm.*; import com.jfinal.log.Log; import io.jboot.Jboot; -import io.jboot.utils.StrUtil; +import io.jboot.core.spi.JbootSpiLoader; import io.jboot.support.metric.reporter.console.JbootConsoleReporter; import io.jboot.support.metric.reporter.csv.CSVReporter; import io.jboot.support.metric.reporter.elasticsearch.ElasticsearchReporter; @@ -27,12 +30,15 @@ import io.jboot.support.metric.reporter.ganglia.GangliaReporter; import io.jboot.support.metric.reporter.graphite.JbootGraphiteReporter; import io.jboot.support.metric.reporter.influxdb.InfluxdbReporter; import io.jboot.support.metric.reporter.jmx.JMXReporter; +import io.jboot.support.metric.reporter.prometheus.PrometheusReporter; import io.jboot.support.metric.reporter.slf4j.JbootSlf4jReporter; -import io.jboot.core.spi.JbootSpiLoader; import io.jboot.utils.ArrayUtil; +import io.jboot.utils.StrUtil; +import java.lang.management.ManagementFactory; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; public class JbootMetricManager { @@ -53,15 +59,39 @@ public class JbootMetricManager { private JbootMetricConfig metricsConfig = Jboot.config(JbootMetricConfig.class); private boolean enable = false; + private JbootMetricManager() { - if (!metricsConfig.isConfigOk()) { + if (!metricsConfig.isConfigOk() || !metricsConfig.isEnable()) { return; } + metricRegistry = new MetricRegistry(); healthCheckRegistry = new HealthCheckRegistry(); + + if (metricsConfig.isJvmMetricEnable()) { + + metricRegistry.register("jvm.uptime", (Gauge) () -> ManagementFactory.getRuntimeMXBean().getUptime()); + metricRegistry.register("jvm.start_time", (Gauge) () -> ManagementFactory.getRuntimeMXBean().getStartTime()); + metricRegistry.register("jvm.current_time", (Gauge) () -> System.nanoTime()); + metricRegistry.register("jvm.classes", new ClassLoadingGaugeSet()); + metricRegistry.register("jvm.attribute", new JvmAttributeGaugeSet()); + metricRegistry.register("jvm.fd", new FileDescriptorRatioGauge()); + metricRegistry.register("jvm.buffers", new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer())); + metricRegistry.register("jvm.gc", new GarbageCollectorMetricSet()); + metricRegistry.register("jvm.memory", new MemoryUsageGaugeSet()); + metricRegistry.register("jvm.threads", new CachedThreadStatesGaugeSet(10, TimeUnit.SECONDS)); + + metricRegistry.registerAll(new JvmCpuGaugeSet()); + metricRegistry.registerAll(new JvmGcMetrics(metricRegistry)); + metricRegistry.registerAll(new ProcessFilesGaugeSet()); + + healthCheckRegistry.register("jvm.thread_deadlocks", new ThreadDeadlockHealthCheck()); + } + + List reporters = getReporters(); if (ArrayUtil.isNullOrEmpty(reporters)) { return; @@ -90,7 +120,7 @@ public class JbootMetricManager { } List reporters = new ArrayList<>(); - String[] repoterStrings = repoterString.split(";"); + String[] repoterStrings = repoterString.split(","); for (String repoterName : repoterStrings) { JbootMetricReporter reporter = getReporterByName(repoterName); if (reporter != null) { @@ -131,6 +161,9 @@ public class JbootMetricManager { case JbootMetricConfig.REPORTER_SLF4J: reporter = new JbootSlf4jReporter(); break; + case JbootMetricConfig.REPORTER_PROMETHEUS: + reporter = new PrometheusReporter(); + break; default: reporter = JbootSpiLoader.load(JbootMetricReporter.class, repoterName); } diff --git a/src/main/java/io/jboot/support/metric/JbootMetricReporter.java b/src/main/java/io/jboot/support/metric/JbootMetricReporter.java index 37eab3e6e4264302d13444184f1c1d2c3ff2e95c..6a69afd68475fbfd7f16d98fd2b7ecf25d55be29 100644 --- a/src/main/java/io/jboot/support/metric/JbootMetricReporter.java +++ b/src/main/java/io/jboot/support/metric/JbootMetricReporter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.codahale.metrics.MetricRegistry; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.metrics */ public interface JbootMetricReporter { diff --git a/src/main/java/io/jboot/support/metric/JbootMetricServletContextListener.java b/src/main/java/io/jboot/support/metric/JbootMetricServletContextListener.java index bb9f711aced2276d9dcad718cc0f433314af853a..377dccfe56723a112a9493f83044b0c598a1755c 100644 --- a/src/main/java/io/jboot/support/metric/JbootMetricServletContextListener.java +++ b/src/main/java/io/jboot/support/metric/JbootMetricServletContextListener.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/JvmCpuGaugeSet.java b/src/main/java/io/jboot/support/metric/JvmCpuGaugeSet.java new file mode 100644 index 0000000000000000000000000000000000000000..df4d34c917b82300a7bf650f5ee1f36822849724 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/JvmCpuGaugeSet.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricSet; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +/** + * 参考 https://github.com/micrometer-metrics/micrometer/ + * blob/master/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/ProcessorMetrics.java + */ +public class JvmCpuGaugeSet implements MetricSet { + + + /** List of public, exported interface class names from supported JVM implementations. */ + private static final List OPERATING_SYSTEM_BEAN_CLASS_NAMES = Arrays.asList( + "com.ibm.lang.management.OperatingSystemMXBean", // J9 + "com.sun.management.OperatingSystemMXBean" // HotSpot + ); + + + private final OperatingSystemMXBean operatingSystemBean; + + private final Class operatingSystemBeanClass; + + private final Method systemCpuUsage; + + private final Method processCpuUsage; + + /** + * Creates a new set of gauges. + */ + public JvmCpuGaugeSet() { + this.operatingSystemBean = ManagementFactory.getOperatingSystemMXBean(); + this.operatingSystemBeanClass = getFirstClassFound(OPERATING_SYSTEM_BEAN_CLASS_NAMES); + Method getCpuLoad = detectMethod("getCpuLoad"); + this.systemCpuUsage = getCpuLoad != null ? getCpuLoad : detectMethod("getSystemCpuLoad"); + this.processCpuUsage = detectMethod("getProcessCpuLoad"); + } + + + @Override + public Map getMetrics() { + final Map gauges = new HashMap<>(); + + Runtime runtime = Runtime.getRuntime(); + gauges.put("system.cpu.count", (Gauge) () -> runtime.availableProcessors()); + + if (operatingSystemBean.getSystemLoadAverage() >= 0) { + gauges.put("system.load.average.1m", (Gauge) () -> operatingSystemBean.getSystemLoadAverage()); + } + + if (systemCpuUsage != null) { + gauges.put("system.cpu.usage", (Gauge) () -> invoke(systemCpuUsage)); + } + + if (processCpuUsage != null) { + gauges.put("process.cpu.usage", (Gauge) () -> invoke(processCpuUsage)); + } + + return Collections.unmodifiableMap(gauges); + } + + + + + private double invoke(Method method) { + try { + return method != null ? (double) method.invoke(operatingSystemBean) : Double.NaN; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + return Double.NaN; + } + } + + + private Method detectMethod(String name) { + if (operatingSystemBeanClass == null) { + return null; + } + try { + // ensure the Bean we have is actually an instance of the interface + operatingSystemBeanClass.cast(operatingSystemBean); + return operatingSystemBeanClass.getDeclaredMethod(name); + } catch (ClassCastException | NoSuchMethodException | SecurityException e) { + return null; + } + } + + private Class getFirstClassFound(List classNames) { + for (String className : classNames) { + try { + return Class.forName(className); + } catch (ClassNotFoundException ignore) { + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/support/metric/JvmGcMetrics.java b/src/main/java/io/jboot/support/metric/JvmGcMetrics.java new file mode 100644 index 0000000000000000000000000000000000000000..0ab5f15aa088bc2464281507e292fd7651af47ac --- /dev/null +++ b/src/main/java/io/jboot/support/metric/JvmGcMetrics.java @@ -0,0 +1,248 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric; + +import com.codahale.metrics.*; +import com.jfinal.log.Log; +import com.sun.management.GarbageCollectionNotificationInfo; +import com.sun.management.GcInfo; + +import javax.management.ListenerNotFoundException; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; +import javax.management.openmbean.CompositeData; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import static io.jboot.support.metric.JvmGcUtil.*; + +/** + * Record metrics that report a number of statistics related to garbage + * collection emanating from the MXBean and also adds information about GC causes. + *

+ * This provides metrics for OpenJDK garbage collectors: serial, parallel, G1, Shenandoah, ZGC. + * + * @author Jon Schneider + * @author Tommy Ludwig + * @see GarbageCollectorMXBean + */ +public class JvmGcMetrics implements MetricSet, AutoCloseable { + + private final static Log log = Log.getLog(JvmGcMetrics.class); + + private final boolean managementExtensionsPresent = isManagementExtensionsPresent(); + +private MetricRegistry metricRegistry; + + private String youngGenPoolName; + + private String oldGenPoolName; + + private String nonGenerationalMemoryPool; + + private final List notificationListenerCleanUpRunnables = new CopyOnWriteArrayList<>(); + + + public JvmGcMetrics(MetricRegistry metricRegistry) { + this.metricRegistry = metricRegistry; + + for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) { + String name = mbean.getName(); + if (isYoungGenPool(name)) { + youngGenPoolName = name; + } else if (isOldGenPool(name)) { + oldGenPoolName = name; + } else if (isNonGenerationalHeapPool(name)) { + nonGenerationalMemoryPool = name; + } + } + } + + @Override + public Map getMetrics() { + final Map gauges = new HashMap<>(); + if (!this.managementExtensionsPresent) { + return gauges; + } + + double maxLongLivedPoolBytes = getLongLivedHeapPool().map(mem -> getUsageValue(mem, MemoryUsage::getMax)).orElse(0.0); + + AtomicLong maxDataSize = new AtomicLong((long) maxLongLivedPoolBytes); + gauges.put("jvm.gc.max.data.size", (Gauge) () -> maxDataSize.get()); + + + AtomicLong liveDataSize = new AtomicLong(); + gauges.put("jvm.gc.live.data.size", (Gauge) () -> liveDataSize.get()); + + Counter allocatedBytes =metricRegistry.counter("jvm.gc.memory.allocated"); + Counter promotedBytes =(oldGenPoolName == null) ? null : metricRegistry.counter("jvm.gc.memory.promoted"); + + + + // start watching for GC notifications + final AtomicLong heapPoolSizeAfterGc = new AtomicLong(); + + for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (!(mbean instanceof NotificationEmitter)) { + continue; + } + NotificationListener notificationListener = (notification, ref) -> { + CompositeData cd = (CompositeData) notification.getUserData(); + GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd); + + String gcCause = notificationInfo.getGcCause(); + String gcAction = notificationInfo.getGcAction(); + GcInfo gcInfo = notificationInfo.getGcInfo(); + long duration = gcInfo.getDuration(); + if (isConcurrentPhase(gcCause, notificationInfo.getGcName())) { +// Timer.builder("jvm.gc.concurrent.phase.time") +// .tags(tags) +// .tags("action", gcAction, "cause", gcCause) +// .description("Time spent in concurrent phase") +// .register(registry) +// .record(duration, TimeUnit.MILLISECONDS); + metricRegistry.timer("jvm.gc.concurrent.phase.time") + .update(duration,TimeUnit.MICROSECONDS); + } else { +// Timer.builder("jvm.gc.pause") +// .tags(tags) +// .tags("action", gcAction, "cause", gcCause) +// .description("Time spent in GC pause") +// .register(registry) +// .record(duration, TimeUnit.MILLISECONDS); + metricRegistry.timer("jvm.gc.pause") + .update(duration,TimeUnit.MICROSECONDS); + } + + // Update promotion and allocation counters + final Map before = gcInfo.getMemoryUsageBeforeGc(); + final Map after = gcInfo.getMemoryUsageAfterGc(); + + if (nonGenerationalMemoryPool != null) { + countPoolSizeDelta(gcInfo.getMemoryUsageBeforeGc(), gcInfo.getMemoryUsageAfterGc(), allocatedBytes, + heapPoolSizeAfterGc, nonGenerationalMemoryPool); + if (after.get(nonGenerationalMemoryPool).getUsed() < before.get(nonGenerationalMemoryPool).getUsed()) { + liveDataSize.set(after.get(nonGenerationalMemoryPool).getUsed()); + final long longLivedMaxAfter = after.get(nonGenerationalMemoryPool).getMax(); + maxDataSize.set(longLivedMaxAfter); + } + return; + } + + if (oldGenPoolName != null) { + final long oldBefore = before.get(oldGenPoolName).getUsed(); + final long oldAfter = after.get(oldGenPoolName).getUsed(); + final long delta = oldAfter - oldBefore; + if (delta > 0L) { + promotedBytes.inc(delta); + } + + // Some GC implementations such as G1 can reduce the old gen size as part of a minor GC. To track the + // live data size we record the value if we see a reduction in the old gen heap size or + // after a major GC. + if (oldAfter < oldBefore || GcGenerationAge.fromName(notificationInfo.getGcName()) == GcGenerationAge.OLD) { + liveDataSize.set(oldAfter); + final long oldMaxAfter = after.get(oldGenPoolName).getMax(); + maxDataSize.set(oldMaxAfter); + } + } + + if (youngGenPoolName != null) { + countPoolSizeDelta(gcInfo.getMemoryUsageBeforeGc(), gcInfo.getMemoryUsageAfterGc(), allocatedBytes, + heapPoolSizeAfterGc, youngGenPoolName); + } + }; + NotificationEmitter notificationEmitter = (NotificationEmitter) mbean; + notificationEmitter.addNotificationListener(notificationListener, notification -> notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION), null); + notificationListenerCleanUpRunnables.add(() -> { + try { + notificationEmitter.removeNotificationListener(notificationListener); + } catch (ListenerNotFoundException ignore) { + } + }); + } + + return gauges; + } + + private void countPoolSizeDelta(Map before, Map after, Counter counter, + AtomicLong previousPoolSize, String poolName) { + final long beforeBytes = before.get(poolName).getUsed(); + final long afterBytes = after.get(poolName).getUsed(); + final long delta = beforeBytes - previousPoolSize.get(); + previousPoolSize.set(afterBytes); + if (delta > 0L) { + counter.inc(delta); + } + } + + private static boolean isManagementExtensionsPresent() { + if (ManagementFactory.getMemoryPoolMXBeans().isEmpty()) { + // Substrate VM, for example, doesn't provide or support these beans (yet) + log.warn("GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM"); + return false; + } + + try { + Class.forName("com.sun.management.GarbageCollectionNotificationInfo", false, + MemoryPoolMXBean.class.getClassLoader()); + return true; + } catch (Throwable e) { + // We are operating in a JVM without access to this level of detail + log.warn("GC notifications will not be available because " + + "com.sun.management.GarbageCollectionNotificationInfo is not present"); + return false; + } + } + + @Override + public void close() { + notificationListenerCleanUpRunnables.forEach(Runnable::run); + } + + /** + * Generalization of which parts of the heap are considered "young" or "old" for multiple GC implementations + */ + enum GcGenerationAge { + OLD, + YOUNG, + UNKNOWN; + + private static Map knownCollectors = new HashMap() {{ + put("ConcurrentMarkSweep", OLD); + put("Copy", YOUNG); + put("G1 Old Generation", OLD); + put("G1 Young Generation", YOUNG); + put("MarkSweepCompact", OLD); + put("PS MarkSweep", OLD); + put("PS Scavenge", YOUNG); + put("ParNew", YOUNG); + }}; + + static GcGenerationAge fromName(String name) { + return knownCollectors.getOrDefault(name, UNKNOWN); + } + } + +} diff --git a/src/main/java/io/jboot/support/metric/JvmGcUtil.java b/src/main/java/io/jboot/support/metric/JvmGcUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..b1e0e097fecf343d4f323bb1a40d7512b25d97c4 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/JvmGcUtil.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; +import java.lang.management.MemoryUsage; +import java.util.Optional; +import java.util.function.ToLongFunction; + +/** + * 参考 MicroMeter + */ +class JvmGcUtil { + + private JvmGcUtil() { + } + + static Optional getLongLivedHeapPool() { + return ManagementFactory + .getPlatformMXBeans(MemoryPoolMXBean.class) + .stream() + .filter(JvmGcUtil::isHeap) + .filter(mem -> isOldGenPool(mem.getName()) || isNonGenerationalHeapPool(mem.getName())) + .findAny(); + } + + static boolean isConcurrentPhase(String cause, String name) { + return "No GC".equals(cause) + || "Shenandoah Cycles".equals(name); + } + + static boolean isYoungGenPool(String name) { + return name != null && name.endsWith("Eden Space"); + } + + static boolean isOldGenPool(String name) { + return name != null && (name.endsWith("Old Gen") || name.endsWith("Tenured Gen")); + } + + static boolean isNonGenerationalHeapPool(String name) { + return "Shenandoah".equals(name) || "ZHeap".equals(name); + } + + private static boolean isHeap(MemoryPoolMXBean memoryPoolBean) { + return MemoryType.HEAP.equals(memoryPoolBean.getType()); + } + + static double getUsageValue(MemoryPoolMXBean memoryPoolMXBean, ToLongFunction getter) { + MemoryUsage usage = getUsage(memoryPoolMXBean); + if (usage == null) { + return Double.NaN; + } + return getter.applyAsLong(usage); + } + + private static MemoryUsage getUsage(MemoryPoolMXBean memoryPoolMXBean) { + try { + return memoryPoolMXBean.getUsage(); + } catch (InternalError e) { + // Defensive for potential InternalError with some specific JVM options. Based on its Javadoc, + // MemoryPoolMXBean.getUsage() should return null, not throwing InternalError, so it seems to be a JVM bug. + return null; + } + } +} diff --git a/src/main/java/io/jboot/web/handler/JbootFilterHandler.java b/src/main/java/io/jboot/support/metric/MetricServletHandler.java similarity index 60% rename from src/main/java/io/jboot/web/handler/JbootFilterHandler.java rename to src/main/java/io/jboot/support/metric/MetricServletHandler.java index 82a22420466f295fda125c1b8fe7292950e8b824..c1dc815fc5702540f607bc41a54738e5404d0185 100644 --- a/src/main/java/io/jboot/web/handler/JbootFilterHandler.java +++ b/src/main/java/io/jboot/support/metric/MetricServletHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,38 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.web.handler; +package io.jboot.support.metric; import com.jfinal.handler.Handler; -import io.jboot.Jboot; -import io.jboot.support.metric.JbootMetricConfig; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +public class MetricServletHandler extends Handler { -public class JbootFilterHandler extends Handler { + private String adminServletMapping; + public MetricServletHandler(String adminServletMapping) { + this.adminServletMapping = adminServletMapping; + } - private static JbootMetricConfig metricsConfig = Jboot.config(JbootMetricConfig.class); @Override public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { - - //static files - if (target.indexOf('.') != -1) { - return; - } - - // metrics - if (metricsConfig.isConfigOk() && target.startsWith(metricsConfig.getUrl())) { + if (target.startsWith(adminServletMapping)) { return; + } else { + next.handle(target, request, response, isHandled); } - - - next.handle(target, request, response, isHandled); - } - - } diff --git a/src/main/java/io/jboot/support/metric/ProcessFilesGaugeSet.java b/src/main/java/io/jboot/support/metric/ProcessFilesGaugeSet.java new file mode 100644 index 0000000000000000000000000000000000000000..78857dd7a8a4a1753d458f6244c6d00eecad1456 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/ProcessFilesGaugeSet.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricSet; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +/** + * 参考 https://github.com/micrometer-metrics/micrometer/ + * blob/master/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/FileDescriptorMetrics.java + */ +public class ProcessFilesGaugeSet implements MetricSet { + + + /** + * List of public, exported interface class names from supported JVM implementations. + */ + private static final List UNIX_OPERATING_SYSTEM_BEAN_CLASS_NAMES = Arrays.asList( + "com.sun.management.UnixOperatingSystemMXBean", // HotSpot + "com.ibm.lang.management.UnixOperatingSystemMXBean" // J9 + ); + + private final OperatingSystemMXBean osBean; + + private final Class osBeanClass; + + private final Method openFilesMethod; + + private final Method maxFilesMethod; + + + /** + * Creates a new set of gauges. + */ + public ProcessFilesGaugeSet() { + this.osBean = ManagementFactory.getOperatingSystemMXBean(); + this.osBeanClass = getFirstClassFound(UNIX_OPERATING_SYSTEM_BEAN_CLASS_NAMES); + this.openFilesMethod = detectMethod("getOpenFileDescriptorCount"); + this.maxFilesMethod = detectMethod("getMaxFileDescriptorCount"); + } + + + @Override + public Map getMetrics() { + final Map gauges = new HashMap<>(); + + + if (openFilesMethod != null) { + gauges.put("process.files.open", (Gauge) () -> invoke(openFilesMethod)); + } + + if (maxFilesMethod != null) { + gauges.put("process.files.max", (Gauge) () -> invoke(maxFilesMethod)); + } + + return Collections.unmodifiableMap(gauges); + } + + + private double invoke(Method method) { + try { + return method != null ? (double) (long) method.invoke(osBean) : Double.NaN; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + return Double.NaN; + } + } + + + private Method detectMethod(String name) { + if (osBeanClass == null) { + return null; + } + try { + // ensure the Bean we have is actually an instance of the interface + osBeanClass.cast(osBean); + return osBeanClass.getDeclaredMethod(name); + } catch (ClassCastException | NoSuchMethodException | SecurityException e) { + return null; + } + } + + + private Class getFirstClassFound(List classNames) { + for (String className : classNames) { + try { + return Class.forName(className); + } catch (ClassNotFoundException ignore) { + } + } + return null; + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/support/metric/annotation/EnableMetricConcurrency.java b/src/main/java/io/jboot/support/metric/annotation/EnableMetricConcurrency.java index fe203757f36b301f350956a0ce7b5a65cee9b512..4f03f047ef0061b6db44d5a634ce6fccb8deba5b 100644 --- a/src/main/java/io/jboot/support/metric/annotation/EnableMetricConcurrency.java +++ b/src/main/java/io/jboot/support/metric/annotation/EnableMetricConcurrency.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/annotation/EnableMetricCounter.java b/src/main/java/io/jboot/support/metric/annotation/EnableMetricCounter.java index 3ccbd1b9931e9c1007c1f13657f79c87cf4a2a65..2502c4dd98fb9083a69c825918172b86abc7617e 100644 --- a/src/main/java/io/jboot/support/metric/annotation/EnableMetricCounter.java +++ b/src/main/java/io/jboot/support/metric/annotation/EnableMetricCounter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/annotation/EnableMetricHistogram.java b/src/main/java/io/jboot/support/metric/annotation/EnableMetricHistogram.java index 4962e2daeb1df437d81f8dd5439803b0d72f235c..b47f34153685ca1c040f8dae96e7346d04bbc21b 100644 --- a/src/main/java/io/jboot/support/metric/annotation/EnableMetricHistogram.java +++ b/src/main/java/io/jboot/support/metric/annotation/EnableMetricHistogram.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/annotation/EnableMetricMeter.java b/src/main/java/io/jboot/support/metric/annotation/EnableMetricMeter.java index 7dbae386135af40bc93398cd093ae3c83d4ac454..21bd781b765052f01af8ad3a170890f9ac726dc9 100644 --- a/src/main/java/io/jboot/support/metric/annotation/EnableMetricMeter.java +++ b/src/main/java/io/jboot/support/metric/annotation/EnableMetricMeter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/annotation/EnableMetricTimer.java b/src/main/java/io/jboot/support/metric/annotation/EnableMetricTimer.java index 974adbadc65f9708b62490c7ee0b80c6151f3e9d..11e698cec035501e4a2d4871e9c145c71e39c55f 100644 --- a/src/main/java/io/jboot/support/metric/annotation/EnableMetricTimer.java +++ b/src/main/java/io/jboot/support/metric/annotation/EnableMetricTimer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/interceptor/MetricConcurrencyInterceptor.java b/src/main/java/io/jboot/support/metric/interceptor/MetricConcurrencyInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..39554da9899428abad227993725df68fe31cfc95 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/interceptor/MetricConcurrencyInterceptor.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.interceptor; + +import com.codahale.metrics.Counter; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.Jboot; +import io.jboot.support.metric.annotation.EnableMetricConcurrency; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.StrUtil; + +public class MetricConcurrencyInterceptor implements Interceptor { + + + @Override + public void intercept(Invocation inv) { + + Counter concurrencyRecord = null; + EnableMetricConcurrency concurrencyAnnotation = inv.getMethod().getAnnotation(EnableMetricConcurrency.class); + if (concurrencyAnnotation != null) { + String value = AnnotationUtil.get(concurrencyAnnotation.value()); + String name = StrUtil.isBlank(value) + ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".concurrency" + : value; + + + concurrencyRecord = Jboot.getMetric().counter(name); + concurrencyRecord.inc(); + } + + try { + inv.invoke(); + } finally { + if (concurrencyRecord != null) { + concurrencyRecord.dec(); + } + } + } + + +} diff --git a/src/main/java/io/jboot/support/metric/interceptor/MetricCounterInterceptor.java b/src/main/java/io/jboot/support/metric/interceptor/MetricCounterInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..6d9d5ea6c11ce603d99985ac9ee0a4a4d6b74f61 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/interceptor/MetricCounterInterceptor.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.interceptor; + +import com.codahale.metrics.Counter; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.Jboot; +import io.jboot.support.metric.annotation.EnableMetricCounter; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.StrUtil; + + +public class MetricCounterInterceptor implements Interceptor { + + + @Override + public void intercept(Invocation inv) { + + EnableMetricCounter counterAnnotation = inv.getMethod().getAnnotation(EnableMetricCounter.class); + if (counterAnnotation != null) { + String value = AnnotationUtil.get(counterAnnotation.value()); + String name = StrUtil.isBlank(value) + ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".counter" + : value; + + + Counter counter = Jboot.getMetric().counter(name); + counter.inc(); + } + + inv.invoke(); + } + + +} diff --git a/src/main/java/io/jboot/support/metric/interceptor/MetricHistogramInterceptor.java b/src/main/java/io/jboot/support/metric/interceptor/MetricHistogramInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..15236123c34a184b41dbc48701035502538beaee --- /dev/null +++ b/src/main/java/io/jboot/support/metric/interceptor/MetricHistogramInterceptor.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.interceptor; + +import com.codahale.metrics.Histogram; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.Jboot; +import io.jboot.support.metric.annotation.EnableMetricHistogram; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.StrUtil; + +public class MetricHistogramInterceptor implements Interceptor { + + + @Override + public void intercept(Invocation inv) { + + EnableMetricHistogram histogramAnnotation = inv.getMethod().getAnnotation(EnableMetricHistogram.class); + if (histogramAnnotation != null) { + String value = AnnotationUtil.get(histogramAnnotation.value()); + String name = StrUtil.isBlank(value) + ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".histogram" + : value; + + + Histogram histogram = Jboot.getMetric().histogram(name); + histogram.update(histogramAnnotation.update()); + } + + inv.invoke(); + } + + +} diff --git a/src/main/java/io/jboot/support/metric/interceptor/MetricInterceptorBuilder.java b/src/main/java/io/jboot/support/metric/interceptor/MetricInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..d8d6239c758d0462b57a14f15b9a104bf6262467 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/interceptor/MetricInterceptorBuilder.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.interceptor; + +import io.jboot.Jboot; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.support.metric.JbootMetricConfig; +import io.jboot.support.metric.annotation.*; + +import java.lang.reflect.Method; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +@AutoLoad +public class MetricInterceptorBuilder implements InterceptorBuilder { + + private static JbootMetricConfig config = Jboot.config(JbootMetricConfig.class); + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (!config.isConfigOk() || !config.isEnable()) { + return; + } + + if (Util.hasAnnotation(method, EnableMetricConcurrency.class)) { + interceptors.add(MetricConcurrencyInterceptor.class); + } + + if (Util.hasAnnotation(method, EnableMetricHistogram.class)) { + interceptors.add(MetricHistogramInterceptor.class); + } + + if (Util.hasAnnotation(method, EnableMetricCounter.class)) { + interceptors.add(MetricCounterInterceptor.class); + } + + if (Util.hasAnnotation(method, EnableMetricMeter.class)) { + interceptors.add(MetricMeterInterceptor.class); + } + + if (Util.hasAnnotation(method, EnableMetricTimer.class)) { + interceptors.add(MetricTimerInterceptor.class); + } + + } + + +} diff --git a/src/main/java/io/jboot/support/metric/interceptor/MetricMeterInterceptor.java b/src/main/java/io/jboot/support/metric/interceptor/MetricMeterInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..11d7ac83c72e0db89093094f4124abac0ed76920 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/interceptor/MetricMeterInterceptor.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.interceptor; + +import com.codahale.metrics.Meter; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.Jboot; +import io.jboot.support.metric.annotation.EnableMetricMeter; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.StrUtil; + +public class MetricMeterInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + + EnableMetricMeter meterAnnotation = inv.getMethod().getAnnotation(EnableMetricMeter.class); + if (meterAnnotation != null) { + String value = AnnotationUtil.get(meterAnnotation.value()); + String name = StrUtil.isBlank(value) + ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".meter" + : value; + + + Meter meter = Jboot.getMetric().meter(name); + meter.mark(); + } + + inv.invoke(); + } + + +} diff --git a/src/main/java/io/jboot/support/metric/interceptor/MetricTimerInterceptor.java b/src/main/java/io/jboot/support/metric/interceptor/MetricTimerInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..a0082607ae3e6494bb0b39b8d8b9ddfb81a2d807 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/interceptor/MetricTimerInterceptor.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.interceptor; + +import com.codahale.metrics.Timer; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.Jboot; +import io.jboot.support.metric.annotation.EnableMetricTimer; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.StrUtil; + +public class MetricTimerInterceptor implements Interceptor { + + + @Override + public void intercept(Invocation inv) { + + Timer.Context timerContext = null; + + EnableMetricTimer timerAnnotation = inv.getMethod().getAnnotation(EnableMetricTimer.class); + if (timerAnnotation != null) { + String value = AnnotationUtil.get(timerAnnotation.value()); + String name = StrUtil.isBlank(value) + ? inv.getController().getClass().getName() + "." + inv.getMethodName() + ".timer" + : value; + + + Timer timer = Jboot.getMetric().timer(name); + timerContext = timer.time(); + } + + + try { + inv.invoke(); + } finally { + if (timerContext != null) { + timerContext.stop(); + } + } + } + + +} diff --git a/src/main/java/io/jboot/support/metric/reporter/console/JbootConsoleReporter.java b/src/main/java/io/jboot/support/metric/reporter/console/JbootConsoleReporter.java index 518319797b4a9453005b75172dd7aa0a9df499ed..14791bebe874e8f6c6a796efad9c90ebec706fcf 100644 --- a/src/main/java/io/jboot/support/metric/reporter/console/JbootConsoleReporter.java +++ b/src/main/java/io/jboot/support/metric/reporter/console/JbootConsoleReporter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.util.concurrent.TimeUnit; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.metric.reporter.console */ public class JbootConsoleReporter implements JbootMetricReporter { @Override diff --git a/src/main/java/io/jboot/support/metric/reporter/csv/CSVReporter.java b/src/main/java/io/jboot/support/metric/reporter/csv/CSVReporter.java index 4c44863338eadff5d8b7cf7abaa2bd5f091694e4..7a84e10fa924d1e7b5a986c4ec8c19ed19f40ae6 100644 --- a/src/main/java/io/jboot/support/metric/reporter/csv/CSVReporter.java +++ b/src/main/java/io/jboot/support/metric/reporter/csv/CSVReporter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import java.util.concurrent.TimeUnit; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.metric.reporter.csv */ public class CSVReporter implements JbootMetricReporter { diff --git a/src/main/java/io/jboot/support/metric/reporter/csv/JbootMetricCVRReporterConfig.java b/src/main/java/io/jboot/support/metric/reporter/csv/JbootMetricCVRReporterConfig.java index 0a01d3ca4f796849450b4364814d324d4a554ac4..f0edafb40803af45b71056941887f03b8856f41b 100644 --- a/src/main/java/io/jboot/support/metric/reporter/csv/JbootMetricCVRReporterConfig.java +++ b/src/main/java/io/jboot/support/metric/reporter/csv/JbootMetricCVRReporterConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/reporter/elasticsearch/ElasticsearchReporter.java b/src/main/java/io/jboot/support/metric/reporter/elasticsearch/ElasticsearchReporter.java index 6e02784c6d92fd0810f72ad7c0921f394c22a27c..a8e096d96588a0780e9267e49c645eddff00debe 100644 --- a/src/main/java/io/jboot/support/metric/reporter/elasticsearch/ElasticsearchReporter.java +++ b/src/main/java/io/jboot/support/metric/reporter/elasticsearch/ElasticsearchReporter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import io.jboot.support.metric.JbootMetricReporter; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.metric.reporter.elasticsearch */ public class ElasticsearchReporter implements JbootMetricReporter { @Override diff --git a/src/main/java/io/jboot/support/metric/reporter/ganglia/GangliaReporter.java b/src/main/java/io/jboot/support/metric/reporter/ganglia/GangliaReporter.java index 62bb44c19d7ce42a56b55036c3cd3e0f7e582d63..00dfb450e5379bd8eecacff023c397bd8e6033c4 100644 --- a/src/main/java/io/jboot/support/metric/reporter/ganglia/GangliaReporter.java +++ b/src/main/java/io/jboot/support/metric/reporter/ganglia/GangliaReporter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import io.jboot.support.metric.JbootMetricReporter; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.metric.reporter.ganglia */ public class GangliaReporter implements JbootMetricReporter { @Override diff --git a/src/main/java/io/jboot/support/metric/reporter/graphite/JbootGraphiteReporter.java b/src/main/java/io/jboot/support/metric/reporter/graphite/JbootGraphiteReporter.java index 1d096ec8bc433092b2ab11ba70c47e6446081fc8..e98b54776e143c412c3449da21cf051efd16c427 100644 --- a/src/main/java/io/jboot/support/metric/reporter/graphite/JbootGraphiteReporter.java +++ b/src/main/java/io/jboot/support/metric/reporter/graphite/JbootGraphiteReporter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ import java.util.concurrent.TimeUnit; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.metric.reporter.graphite */ public class JbootGraphiteReporter implements JbootMetricReporter { @Override diff --git a/src/main/java/io/jboot/support/metric/reporter/graphite/JbootMetricGraphiteReporterConfig.java b/src/main/java/io/jboot/support/metric/reporter/graphite/JbootMetricGraphiteReporterConfig.java index f928176f7c22847ded7ad15801b16cdfc14404ba..59f55c8462dfbe44a0ecf5cc549671b6cc5221cf 100644 --- a/src/main/java/io/jboot/support/metric/reporter/graphite/JbootMetricGraphiteReporterConfig.java +++ b/src/main/java/io/jboot/support/metric/reporter/graphite/JbootMetricGraphiteReporterConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/reporter/influxdb/InfluxdbReporter.java b/src/main/java/io/jboot/support/metric/reporter/influxdb/InfluxdbReporter.java index adf8b82a211c7acff0f08212df3bca7056f4c250..fb4ccd97df8b59bd03d6ca4f0a9a8e4aab7fd62f 100644 --- a/src/main/java/io/jboot/support/metric/reporter/influxdb/InfluxdbReporter.java +++ b/src/main/java/io/jboot/support/metric/reporter/influxdb/InfluxdbReporter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import java.util.concurrent.TimeUnit; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.metric.reporter.jmx * url : https://github.com/davidB/metrics-influxdb */ public class InfluxdbReporter implements JbootMetricReporter { diff --git a/src/main/java/io/jboot/support/metric/reporter/influxdb/JbootMetricInfluxdbReporterConfig.java b/src/main/java/io/jboot/support/metric/reporter/influxdb/JbootMetricInfluxdbReporterConfig.java index c5493a25775ecce3da2f4d33907166ad73f7b155..acd2b38efe284d397969690f20d0e7aa3b54d9f2 100644 --- a/src/main/java/io/jboot/support/metric/reporter/influxdb/JbootMetricInfluxdbReporterConfig.java +++ b/src/main/java/io/jboot/support/metric/reporter/influxdb/JbootMetricInfluxdbReporterConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/metric/reporter/jmx/JMXReporter.java b/src/main/java/io/jboot/support/metric/reporter/jmx/JMXReporter.java index d189751863b7d574545c4549037b61b5eae54cf3..ca0f662f4719ff4a3250165ecdd6648fe12ed1a7 100644 --- a/src/main/java/io/jboot/support/metric/reporter/jmx/JMXReporter.java +++ b/src/main/java/io/jboot/support/metric/reporter/jmx/JMXReporter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import io.jboot.support.metric.JbootMetricReporter; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.metric.reporter.jmx */ public class JMXReporter implements JbootMetricReporter { @Override diff --git a/src/main/java/io/jboot/support/metric/reporter/prometheus/PrometheusExports.java b/src/main/java/io/jboot/support/metric/reporter/prometheus/PrometheusExports.java new file mode 100644 index 0000000000000000000000000000000000000000..4d435c2fc751087f52b06c4be319c6a8ed17bbf9 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/reporter/prometheus/PrometheusExports.java @@ -0,0 +1,260 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.reporter.prometheus; + +import com.codahale.metrics.Timer; +import com.codahale.metrics.*; +import io.jboot.Jboot; +import io.jboot.app.JbootApplicationConfig; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.NetUtil; +import io.jboot.utils.StrUtil; +import io.prometheus.client.dropwizard.DropwizardExports; +import io.prometheus.client.dropwizard.samplebuilder.DefaultSampleBuilder; +import io.prometheus.client.dropwizard.samplebuilder.SampleBuilder; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class PrometheusExports extends io.prometheus.client.Collector implements io.prometheus.client.Collector.Describable { + private static final Logger LOGGER = Logger.getLogger(DropwizardExports.class.getName()); + private MetricRegistry registry; + private SampleBuilder sampleBuilder; + private JbootApplicationConfig appConfig = Jboot.config(JbootApplicationConfig.class); + + /** + * Creates a new DropwizardExports with a {@link DefaultSampleBuilder}. + * + * @param registry a metric registry to export in prometheus. + */ + public PrometheusExports(MetricRegistry registry) { + this.registry = registry; + this.sampleBuilder = new DefaultSampleBuilder(); + } + + /** + * @param registry a metric registry to export in prometheus. + * @param sampleBuilder sampleBuilder to use to create prometheus samples. + */ + public PrometheusExports(MetricRegistry registry, SampleBuilder sampleBuilder) { + this.registry = registry; + this.sampleBuilder = sampleBuilder; + } + + private static String getHelpMessage(String metricName, Metric metric) { + return String.format("Generated from Jboot metric import (metric=%s, type=%s)", + metricName, ClassUtil.getUsefulClass(metric.getClass()).getName()); + } + + // List additionalLabelNames, List additionalLabelValues, + private List getDefaultAdditionalLabelNames(String... names) { + List array = getDefaultAdditionalLabelNames(); + array.addAll(Arrays.asList(names)); + return array; + } + + + private List getDefaultAdditionalLabelNames() { + List newArray = new ArrayList<>(); + newArray.add("application"); + newArray.add("instance"); + return newArray; + } + + + private List getDefaultAdditionalLabelValues(String... values) { + List array = getDefaultAdditionalLabelValues(); + array.addAll(Arrays.asList(values)); + return array; + } + + + private List getDefaultAdditionalLabelValues() { + List newArray = new ArrayList<>(); + newArray.add(appConfig.getName()); + newArray.add(getInstance()); + return newArray; + } + + private static String instance = null; + + private static final String getInstance() { + if (instance == null) { + String ipAddress = NetUtil.getLocalIpAddress(); + if (StrUtil.isNotBlank(ipAddress)) { + instance = ipAddress + ":" + Jboot.configValue("undertow.port"); + } else { + instance = ""; + } + } + return instance; + } + + + /** + * Export counter as Prometheus Gauge. + */ + MetricFamilySamples fromCounter(String dropwizardName, Counter counter) { + MetricFamilySamples.Sample sample = sampleBuilder.createSample(dropwizardName, "" + , getDefaultAdditionalLabelNames() + , getDefaultAdditionalLabelValues() + , new Long(counter.getCount()).doubleValue()); + return new MetricFamilySamples(sample.name, Type.GAUGE, getHelpMessage(dropwizardName, counter), Arrays.asList(sample)); + } + + /** + * Export gauge as a prometheus gauge. + */ + MetricFamilySamples fromGauge(String dropwizardName, Gauge gauge) { + Object obj = gauge.getValue(); + double value; + if (obj instanceof Number) { + value = ((Number) obj).doubleValue(); + } else if (obj instanceof Boolean) { + value = ((Boolean) obj) ? 1 : 0; + } else { + LOGGER.log(Level.FINE, String.format("Invalid type for Gauge %s: %s", sanitizeMetricName(dropwizardName), + obj == null ? "null" : obj.getClass().getName())); + return null; + } + MetricFamilySamples.Sample sample = sampleBuilder.createSample(dropwizardName, "" + , getDefaultAdditionalLabelNames() + , getDefaultAdditionalLabelValues() + , value); + return new MetricFamilySamples(sample.name, Type.GAUGE, getHelpMessage(dropwizardName, gauge), Arrays.asList(sample)); + } + + /** + * Export a histogram snapshot as a prometheus SUMMARY. + * + * @param dropwizardName metric name. + * @param snapshot the histogram snapshot. + * @param count the total sample count for this snapshot. + * @param factor a factor to apply to histogram values. + */ + MetricFamilySamples fromSnapshotAndCount(String dropwizardName, Snapshot snapshot, long count, double factor, String helpMessage) { + long sum = 0; + long[] values = snapshot.getValues(); + if (values != null){ + for (long value : values) { + sum += value; + } + } + List samples = Arrays.asList( + sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.5"), snapshot.getMedian() * factor), + sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.75"), snapshot.get75thPercentile() * factor), + sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.95"), snapshot.get95thPercentile() * factor), + sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.98"), snapshot.get98thPercentile() * factor), + sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.99"), snapshot.get99thPercentile() * factor), + sampleBuilder.createSample(dropwizardName, "", getDefaultAdditionalLabelNames("quantile"), getDefaultAdditionalLabelValues("0.999"), snapshot.get999thPercentile() * factor), + sampleBuilder.createSample(dropwizardName, "_count" + , getDefaultAdditionalLabelNames() + , getDefaultAdditionalLabelValues() + , count), + sampleBuilder.createSample(dropwizardName, "_sum" + , getDefaultAdditionalLabelNames() + , getDefaultAdditionalLabelValues() + , sum), + sampleBuilder.createSample(dropwizardName, "_mean" + , getDefaultAdditionalLabelNames() + , getDefaultAdditionalLabelValues() + , snapshot.getMean()), + sampleBuilder.createSample(dropwizardName, "_median" + , getDefaultAdditionalLabelNames() + , getDefaultAdditionalLabelValues() + , snapshot.getMedian()), + sampleBuilder.createSample(dropwizardName, "_min" + , getDefaultAdditionalLabelNames() + , getDefaultAdditionalLabelValues() + , snapshot.getMin()), + sampleBuilder.createSample(dropwizardName, "_max" + , getDefaultAdditionalLabelNames() + , getDefaultAdditionalLabelValues() + , snapshot.getMax()) + ); + return new MetricFamilySamples(samples.get(0).name, Type.SUMMARY, helpMessage, samples); + } + + /** + * Convert histogram snapshot. + */ + MetricFamilySamples fromHistogram(String dropwizardName, Histogram histogram) { + return fromSnapshotAndCount(dropwizardName, histogram.getSnapshot(), histogram.getCount(), 1.0, + getHelpMessage(dropwizardName, histogram)); + } + + /** + * Export Dropwizard Timer as a histogram. Use TIME_UNIT as time unit. + */ + MetricFamilySamples fromTimer(String dropwizardName, Timer timer) { + return fromSnapshotAndCount(dropwizardName, timer.getSnapshot(), timer.getCount(), + 1.0D / TimeUnit.SECONDS.toNanos(1L), getHelpMessage(dropwizardName, timer)); + } + + /** + * Export a Meter as as prometheus COUNTER. + */ + MetricFamilySamples fromMeter(String dropwizardName, Meter meter) { + final MetricFamilySamples.Sample sample = sampleBuilder.createSample(dropwizardName, "_total" + , getDefaultAdditionalLabelNames() + , getDefaultAdditionalLabelValues() + , meter.getCount()); + return new MetricFamilySamples(sample.name, Type.COUNTER, getHelpMessage(dropwizardName, meter), + Arrays.asList(sample)); + } + + @Override + public List collect() { + Map mfSamplesMap = new HashMap(); + + for (SortedMap.Entry entry : registry.getGauges().entrySet()) { + addToMap(mfSamplesMap, fromGauge(entry.getKey(), entry.getValue())); + } + for (SortedMap.Entry entry : registry.getCounters().entrySet()) { + addToMap(mfSamplesMap, fromCounter(entry.getKey(), entry.getValue())); + } + for (SortedMap.Entry entry : registry.getHistograms().entrySet()) { + addToMap(mfSamplesMap, fromHistogram(entry.getKey(), entry.getValue())); + } + for (SortedMap.Entry entry : registry.getTimers().entrySet()) { + addToMap(mfSamplesMap, fromTimer(entry.getKey(), entry.getValue())); + } + for (SortedMap.Entry entry : registry.getMeters().entrySet()) { + addToMap(mfSamplesMap, fromMeter(entry.getKey(), entry.getValue())); + } + return new ArrayList(mfSamplesMap.values()); + } + + private void addToMap(Map mfSamplesMap, MetricFamilySamples newMfSamples) { + if (newMfSamples != null) { + MetricFamilySamples currentMfSamples = mfSamplesMap.get(newMfSamples.name); + if (currentMfSamples == null) { + mfSamplesMap.put(newMfSamples.name, newMfSamples); + } else { + List samples = new ArrayList(currentMfSamples.samples); + samples.addAll(newMfSamples.samples); + mfSamplesMap.put(newMfSamples.name, new MetricFamilySamples(newMfSamples.name, currentMfSamples.type, currentMfSamples.help, samples)); + } + } + } + + @Override + public List describe() { + return new ArrayList(); + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/support/metric/reporter/prometheus/PrometheusReporter.java b/src/main/java/io/jboot/support/metric/reporter/prometheus/PrometheusReporter.java new file mode 100644 index 0000000000000000000000000000000000000000..623b635397de16aa240deca052e4d7298d593a4b --- /dev/null +++ b/src/main/java/io/jboot/support/metric/reporter/prometheus/PrometheusReporter.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.reporter.prometheus; + +import com.codahale.metrics.MetricRegistry; +import io.jboot.Jboot; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.support.metric.JbootMetricReporter; +import io.prometheus.client.exporter.HTTPServer; + +import java.io.IOException; + +/** + * @author Michael Yang 杨福海 (fuhai999@gmail.com) + * @version V1.0 + * @url https://github.com/prometheus/client_java + */ +public class PrometheusReporter implements JbootMetricReporter { + + private HTTPServer httpServer; + + public PrometheusReporter() { + PrometheusReporterConfig config = Jboot.config(PrometheusReporterConfig.class); + try { + httpServer = new HTTPServer(config.getHost(), config.getPort()); + String printMsg = "Prometheus Reporter Server started -> http://" + config.getHost() + ":" + config.getPort(); + System.out.println(printMsg); + } catch (IOException e) { + throw new JbootIllegalConfigException("Prometheus config is error, please check your jboot.properties. ", e); + } + +// if (httpServer != null) { +// Runtime.getRuntime().addShutdownHook(new Thread(() -> { +// if (httpServer != null) { +// try { +// httpServer.stop(); +// } catch (Exception ex) { +// } +// } +// }, "prometheus-httpserver-hook")); +// } + } + + @Override + public void report(MetricRegistry metricRegistry) { +// new DropwizardExports(metricRegistry).register(); + + // 使用 PrometheusExports 主要是可以添加 application 和 instance 的参数 + // 例如 jvm_memory_total_used{application="jboot",instance="192.168.3.24:8818",} 1.521354E8 + new PrometheusExports(metricRegistry).register(); + } +} diff --git a/src/main/java/io/jboot/components/mq/rabbitmq/JbootmqRabbitmqConfig.java b/src/main/java/io/jboot/support/metric/reporter/prometheus/PrometheusReporterConfig.java similarity index 51% rename from src/main/java/io/jboot/components/mq/rabbitmq/JbootmqRabbitmqConfig.java rename to src/main/java/io/jboot/support/metric/reporter/prometheus/PrometheusReporterConfig.java index 7323649ccf5c6f8f571981c1544d57a844ca8ecb..3e15fe85e1307adf13c6b489bb1300cb6ebabffd 100644 --- a/src/main/java/io/jboot/components/mq/rabbitmq/JbootmqRabbitmqConfig.java +++ b/src/main/java/io/jboot/support/metric/reporter/prometheus/PrometheusReporterConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,38 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.components.mq.rabbitmq; +package io.jboot.support.metric.reporter.prometheus; import io.jboot.app.config.annotation.ConfigModel; +/** + * @author Michael Yang 杨福海 (fuhai999@gmail.com) + * @version V1.0 + * @url https://github.com/prometheus/client_java + */ +@ConfigModel(prefix = "jboot.metric.reporter.prometheus") +public class PrometheusReporterConfig { -@ConfigModel(prefix = "jboot.mq.rabbitmq") -public class JbootmqRabbitmqConfig { - - - private String username = "guest"; - private String password = "guest"; - - private String host = "127.0.0.1"; - private int port = 5672; - private String virtualHost; - - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } + private String host = "0.0.0.0"; + private int port = 1234; public String getHost() { return host; @@ -61,13 +43,4 @@ public class JbootmqRabbitmqConfig { public void setPort(int port) { this.port = port; } - - public String getVirtualHost() { - return virtualHost; - } - - public void setVirtualHost(String virtualHost) { - this.virtualHost = virtualHost; - } - } diff --git a/src/main/java/io/jboot/support/metric/reporter/slf4j/JbootSlf4jReporter.java b/src/main/java/io/jboot/support/metric/reporter/slf4j/JbootSlf4jReporter.java index 8e33593d05bbdf2641d73375a40f8f4ecd6d6651..3f4ef42b120e8bcb193a6632a9368e2d14aad4b1 100644 --- a/src/main/java/io/jboot/support/metric/reporter/slf4j/JbootSlf4jReporter.java +++ b/src/main/java/io/jboot/support/metric/reporter/slf4j/JbootSlf4jReporter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import java.util.concurrent.TimeUnit; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.metric.reporter.slf4j */ public class JbootSlf4jReporter implements JbootMetricReporter { @Override diff --git a/src/main/java/io/jboot/support/metric/request/AbstractInstrumentedFilter.java b/src/main/java/io/jboot/support/metric/request/AbstractInstrumentedFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..ede358bf5bf14699e388dcd4781967948f796cd0 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/request/AbstractInstrumentedFilter.java @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.request; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.jfinal.handler.Handler; +import io.jboot.Jboot; +import io.jboot.support.metric.JbootMetricConfig; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.IOException; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.codahale.metrics.MetricRegistry.name; + +/** + * {@link Filter} implementation which captures request information and a breakdown of the response + * codes being returned. + */ +public abstract class AbstractInstrumentedFilter extends Handler { + + private final Map meterNamesByStatusCode; + + // initialized after call of init method + private ConcurrentMap metersByStatusCode; + private Meter otherMeter; + private Meter timeoutsMeter; + private Meter errorsMeter; + private Counter activeRequests; + private Timer requestTimer; + + private JbootMetricConfig jbootMetricConfig = Jboot.config(JbootMetricConfig.class); + + + /** + * Creates a new instance of the filter. + * + * @param meterNamesByStatusCode A map, keyed by status code, of meter names that we are + * interested in. + * @param otherMetricName The name used for the catch-all meter. + */ + protected AbstractInstrumentedFilter(Map meterNamesByStatusCode, String otherMetricName) { + this.meterNamesByStatusCode = meterNamesByStatusCode; + + + final MetricRegistry metricsRegistry = Jboot.getMetric(); + + String metricName = jbootMetricConfig.getRequestMetricName(); + if (metricName == null || metricName.isEmpty()) { + metricName = getClass().getName(); + } + + this.metersByStatusCode = new ConcurrentHashMap<>(meterNamesByStatusCode.size()); + for (Entry entry : meterNamesByStatusCode.entrySet()) { + metersByStatusCode.put(entry.getKey(), + metricsRegistry.meter(name(metricName, entry.getValue()))); + } + this.otherMeter = metricsRegistry.meter(name(metricName, otherMetricName)); + this.timeoutsMeter = metricsRegistry.meter(name(metricName, "timeouts")); + this.errorsMeter = metricsRegistry.meter(name(metricName, "errors")); + this.activeRequests = metricsRegistry.counter(name(metricName, "activeRequests")); + this.requestTimer = metricsRegistry.timer(name(metricName, "requests")); + + } + + + @Override + public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { + + final StatusExposingServletResponse wrappedResponse = new StatusExposingServletResponse(response); + activeRequests.inc(); + final Timer.Context context = requestTimer.time(); + boolean error = false; + try { + next.handle(target, request, wrappedResponse, isHandled); + } catch (Exception e) { + error = true; + throw e; + } finally { + if (!error && request.isAsyncStarted()) { + request.getAsyncContext().addListener(new AsyncResultListener(context)); + } else { + context.stop(); + activeRequests.dec(); + if (error) { + errorsMeter.mark(); + } else { + markMeterForStatusCode(wrappedResponse.getStatus()); + } + } + } + } + + private void markMeterForStatusCode(int status) { + final Meter metric = metersByStatusCode.get(status); + if (metric != null) { + metric.mark(); + } else { + otherMeter.mark(); + } + } + + private static class StatusExposingServletResponse extends HttpServletResponseWrapper { + // The Servlet spec says: calling setStatus is optional, if no status is set, the default is 200. + private int httpStatus = 200; + + public StatusExposingServletResponse(HttpServletResponse response) { + super(response); + } + + @Override + public void sendError(int sc) throws IOException { + httpStatus = sc; + super.sendError(sc); + } + + @Override + public void sendError(int sc, String msg) throws IOException { + httpStatus = sc; + super.sendError(sc, msg); + } + + @Override + public void setStatus(int sc) { + httpStatus = sc; + super.setStatus(sc); + } + + @Override + @SuppressWarnings("deprecation") + public void setStatus(int sc, String sm) { + httpStatus = sc; + super.setStatus(sc, sm); + } + + @Override + public int getStatus() { + return httpStatus; + } + } + + private class AsyncResultListener implements AsyncListener { + private Timer.Context context; + private boolean done = false; + + public AsyncResultListener(Timer.Context context) { + this.context = context; + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + if (!done) { + HttpServletResponse suppliedResponse = (HttpServletResponse) event.getSuppliedResponse(); + context.stop(); + activeRequests.dec(); + markMeterForStatusCode(suppliedResponse.getStatus()); + } + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + context.stop(); + activeRequests.dec(); + timeoutsMeter.mark(); + done = true; + } + + @Override + public void onError(AsyncEvent event) throws IOException { + context.stop(); + activeRequests.dec(); + errorsMeter.mark(); + done = true; + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + + } + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/support/metric/request/JbootRequestMetricHandler.java b/src/main/java/io/jboot/support/metric/request/JbootRequestMetricHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..2b254380148db1ee39fe096a2fc46f91cc242f00 --- /dev/null +++ b/src/main/java/io/jboot/support/metric/request/JbootRequestMetricHandler.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.metric.request; + +import java.util.HashMap; +import java.util.Map; + + +public class JbootRequestMetricHandler extends AbstractInstrumentedFilter { + + private static final String NAME_PREFIX = "responseCodes."; + private static final int OK = 200; + private static final int CREATED = 201; + private static final int NO_CONTENT = 204; + private static final int BAD_REQUEST = 400; + private static final int NOT_FOUND = 404; + private static final int SERVER_ERROR = 500; + + /** + * Creates a new instance of the request filter. + */ + public JbootRequestMetricHandler() { + super(createMeterNamesByStatusCode(), NAME_PREFIX + "other"); + } + + private static Map createMeterNamesByStatusCode() { + final Map meterNamesByStatusCode = new HashMap<>(6); + meterNamesByStatusCode.put(OK, NAME_PREFIX + "ok"); + meterNamesByStatusCode.put(CREATED, NAME_PREFIX + "created"); + meterNamesByStatusCode.put(NO_CONTENT, NAME_PREFIX + "noContent"); + meterNamesByStatusCode.put(BAD_REQUEST, NAME_PREFIX + "badRequest"); + meterNamesByStatusCode.put(NOT_FOUND, NAME_PREFIX + "notFound"); + meterNamesByStatusCode.put(SERVER_ERROR, NAME_PREFIX + "serverError"); + return meterNamesByStatusCode; + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/support/redis/JbootRedis.java b/src/main/java/io/jboot/support/redis/JbootRedis.java index 890a1f84af3b349b1d9c8f894a6f5becf9f56af9..78888fc0fe12f06c6d10078f09f9cec49a7d65c9 100644 --- a/src/main/java/io/jboot/support/redis/JbootRedis.java +++ b/src/main/java/io/jboot/support/redis/JbootRedis.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,37 +35,38 @@ public interface JbootRedis { * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。 * 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。 */ - public String set(Object key, Object value); + String set(Object key, Object value); /** * 当且仅当 key 不存在能成功设置 + * * @param key * @param value 设置成功,返回 1,设置失败,返回 0 * @return */ - public Long setnx(Object key, Object value); + Long setnx(Object key, Object value); /** * 存放 key value 对到 redis * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。 * 此方法用了修改 incr 等的值 */ - public String setWithoutSerialize(Object key, Object value); + String setWithoutSerialize(Object key, Object value); /** * 存放 key value 对到 redis,并将 key 的生存时间设为 seconds (以秒为单位)。 * 如果 key 已经存在, SETEX 命令将覆写旧值。 */ - public String setex(Object key, int seconds, Object value); + String setex(Object key, int seconds, Object value); /** * 返回 key 所关联的 value 值 * 如果 key 不存在那么返回特殊值 nil 。 */ @SuppressWarnings("unchecked") - public T get(Object key); + T get(Object key); /** * 获取 数据不进行反序列 , 如果之前设置的是非String类型,得到String后自行转化 @@ -73,19 +74,19 @@ public interface JbootRedis { * @param key * @return */ - public String getWithoutSerialize(Object key); + String getWithoutSerialize(Object key); /** * 删除给定的一个 key * 不存在的 key 会被忽略。 */ - public Long del(Object key); + Long del(Object key); /** * 删除给定的多个 key * 不存在的 key 会被忽略。 */ - public Long del(Object... keys); + Long del(Object... keys); /** * 查找所有符合给定模式 pattern 的 key 。 @@ -95,7 +96,7 @@ public interface JbootRedis { * KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。 * 特殊符号用 \ 隔开 */ - public Set keys(String pattern); + Set keys(String pattern); /** * 同时设置一个或多个 key-value 对。 @@ -107,14 +108,13 @@ public interface JbootRedis { * List list = cache.mget("k1", "k2"); // 利用多个键值得到上面代码放入的值 * */ - public String mset(Object... keysValues); + String mset(Object... keysValues); /** * 返回所有(一个或多个)给定 key 的值。 * 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。 */ - @SuppressWarnings("rawtypes") - public List mget(Object... keys); + List mget(Object... keys); /** * 将 key 中储存的数字值减一。 @@ -123,7 +123,7 @@ public interface JbootRedis { * 本操作的值限制在 64 位(bit)有符号数字表示之内。 * 关于递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 */ - public Long decr(Object key); + Long decr(Object key); /** * 将 key 所储存的值减去减量 decrement 。 @@ -132,7 +132,7 @@ public interface JbootRedis { * 本操作的值限制在 64 位(bit)有符号数字表示之内。 * 关于更多递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 */ - public Long decrBy(Object key, long value); + Long decrBy(Object key, long value); /** * 将 key 中储存的数字值增一。 @@ -140,7 +140,7 @@ public interface JbootRedis { * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 * 本操作的值限制在 64 位(bit)有符号数字表示之内。 */ - public Long incr(Object key); + Long incr(Object key); /** * 将 key 所储存的值加上增量 increment 。 @@ -149,36 +149,36 @@ public interface JbootRedis { * 本操作的值限制在 64 位(bit)有符号数字表示之内。 * 关于递增(increment) / 递减(decrement)操作的更多信息,参见 INCR 命令。 */ - public Long incrBy(Object key, long value); + Long incrBy(Object key, long value); /** * 检查给定 key 是否存在。 */ - public boolean exists(Object key); + boolean exists(Object key); /** * 从当前数据库中随机返回(不删除)一个 key 。 */ - public String randomKey(); + String randomKey(); /** * 将 key 改名为 newkey 。 * 当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。 * 当 newkey 已经存在时, RENAME 命令将覆盖旧值。 */ - public String rename(Object oldkey, Object newkey); + String rename(Object oldkey, Object newkey); /** * 将当前数据库的 key 移动到给定的数据库 db 当中。 * 如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。 * 因此,也可以利用这一特性,将 MOVE 当作锁(locking)原语(primitive)。 */ - public Long move(Object key, int dbIndex); + Long move(Object key, int dbIndex); /** * 将 key 原子性地从当前实例传送到目标实例的指定数据库上,一旦传送成功, key 保证会出现在目标实例上,而当前实例上的 key 会被删除。 */ - public String migrate(String host, int port, Object key, int destinationDb, int timeout); + String migrate(String host, int port, Object key, int destinationDb, int timeout); /** * 切换到指定的数据库,数据库索引号 index 用数字值指定,以 0 作为起始索引值。 @@ -189,123 +189,122 @@ public interface JbootRedis { * 2:使用 JbootRedis.call(ICallback) 进行操作 * 3:自行获取 Jedis 对象进行操作 */ - public String select(int databaseIndex); + String select(int databaseIndex); /** * 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。 * 在 JbootRedis 中,带有生存时间的 key 被称为『易失的』(volatile)。 */ - public Long expire(Object key, int seconds); + Long expire(Object key, int seconds); /** * EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 */ - public Long expireAt(Object key, long unixTime); + Long expireAt(Object key, long unixTime); /** * 这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置 key 的生存时间,而不像 EXPIRE 命令那样,以秒为单位。 */ - public Long pexpire(Object key, long milliseconds); + Long pexpire(Object key, long milliseconds); /** * 这个命令和 EXPIREAT 命令类似,但它以毫秒为单位设置 key 的过期 unix 时间戳,而不是像 EXPIREAT 那样,以秒为单位。 */ - public Long pexpireAt(Object key, long millisecondsTimestamp); + Long pexpireAt(Object key, long millisecondsTimestamp); /** * 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 * 当 key 存在但不是字符串类型时,返回一个错误。 */ - public T getSet(Object key, Object value); + T getSet(Object key, Object value); /** * 移除给定 key 的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )。 */ - public Long persist(Object key); + Long persist(Object key); /** * 返回 key 所储存的值的类型。 */ - public String type(Object key); + String type(Object key); /** * 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。 */ - public Long ttl(Object key); + Long ttl(Object key); /** * 这个命令类似于 TTL 命令,但它以毫秒为单位返回 key 的剩余生存时间,而不是像 TTL 命令那样,以秒为单位。 */ - public Long pttl(Object key); + Long pttl(Object key); /** * 对象被引用的数量 */ - public Long objectRefcount(Object key); + Long objectRefcount(Object key); /** * 对象没有被访问的空闲时间 */ - public Long objectIdletime(Object key); + Long objectIdletime(Object key); /** * 将哈希表 key 中的域 field 的值设为 value 。 * 如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。 * 如果域 field 已经存在于哈希表中,旧值将被覆盖。 */ - public Long hset(Object key, Object field, Object value); + Long hset(Object key, Object field, Object value); /** * 同时将多个 field-value (域-值)对设置到哈希表 key 中。 * 此命令会覆盖哈希表中已存在的域。 * 如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。 */ - public String hmset(Object key, Map hash); + String hmset(Object key, Map hash); /** * 返回哈希表 key 中给定域 field 的值。 */ - public T hget(Object key, Object field); + T hget(Object key, Object field); /** * 返回哈希表 key 中,一个或多个给定域的值。 * 如果给定的域不存在于哈希表,那么返回一个 nil 值。 * 因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。 */ - public List hmget(Object key, Object... fields); + List hmget(Object key, Object... fields); /** * 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 */ - public Long hdel(Object key, Object... fields); + Long hdel(Object key, Object... fields); /** * 查看哈希表 key 中,给定域 field 是否存在。 */ - public boolean hexists(Object key, Object field); + boolean hexists(Object key, Object field); /** * 返回哈希表 key 中,所有的域和值。 * 在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。 */ - public Map hgetAll(Object key); + Map hgetAll(Object key); /** * 返回哈希表 key 中所有域的值。 */ - @SuppressWarnings("rawtypes") - public List hvals(Object key); + List hvals(Object key); /** * 返回哈希表 key 中的所有域。 * 底层实现此方法取名为 hfields 更为合适,在此仅为与底层保持一致 */ - public Set hkeys(Object key); + Set hkeys(Object key); /** * 返回哈希表 key 中域的数量。 */ - public Long hlen(Object key); + Long hlen(Object key); /** * 为哈希表 key 中的域 field 的值加上增量 increment 。 @@ -315,7 +314,7 @@ public interface JbootRedis { * 对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。 * 本操作的值被限制在 64 位(bit)有符号数字表示之内。 */ - public Long hincrBy(Object key, Object field, long value); + Long hincrBy(Object key, Object field, long value); /** * 为哈希表 key 中的域 field 加上浮点数增量 increment 。 @@ -326,7 +325,7 @@ public interface JbootRedis { * 2:域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number) * HINCRBYFLOAT 命令的详细功能和 INCRBYFLOAT 命令类似,请查看 INCRBYFLOAT 命令获取更多相关信息。 */ - public Double hincrByFloat(Object key, Object field, double value); + Double hincrByFloat(Object key, Object field, double value); /** @@ -336,7 +335,7 @@ public interface JbootRedis { * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 * 如果 key 不是列表类型,返回一个错误。 */ - public T lindex(Object key, long index); + T lindex(Object key, long index); /** @@ -344,13 +343,13 @@ public interface JbootRedis { * 如果 key 不存在,则 key 被解释为一个空列表,返回 0 . * 如果 key 不是列表类型,返回一个错误。 */ - public Long llen(Object key); + Long llen(Object key); /** * 移除并返回列表 key 的头元素。 */ @SuppressWarnings("unchecked") - public T lpop(Object key); + T lpop(Object key); /** * 将一个或多个值 value 插入到列表 key 的表头 @@ -360,14 +359,14 @@ public interface JbootRedis { * 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。 * 当 key 存在但不是列表类型时,返回一个错误。 */ - public Long lpush(Object key, Object... values); + Long lpush(Object key, Object... values); /** * 将列表 key 下标为 index 的元素的值设置为 value 。 * 当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。 * 关于列表下标的更多信息,请参考 LINDEX 命令。 */ - public String lset(Object key, long index, Object value); + String lset(Object key, long index, Object value); /** * 根据参数 count 的值,移除列表中与参数 value 相等的元素。 @@ -376,7 +375,7 @@ public interface JbootRedis { * count 小于 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。 * count 等于 0 : 移除表中所有与 value 相等的值。 */ - public Long lrem(Object key, long count, Object value); + Long lrem(Object key, long count, Object value); /** * 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。 @@ -388,7 +387,7 @@ public interface JbootRedis { * 获取 list 中下标 1 到 3 的数据: cache.lrange(listKey, 1, 3); * */ - public List lrange(Object key, long start, long end); + List lrange(Object key, long start, long end); /** * 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 @@ -397,12 +396,12 @@ public interface JbootRedis { * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 * 当 key 不是列表类型时,返回一个错误。 */ - public String ltrim(Object key, long start, long end); + String ltrim(Object key, long start, long end); /** * 移除并返回列表 key 的尾元素。 */ - public T rpop(Object key); + T rpop(Object key); /** * 命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作: @@ -410,7 +409,7 @@ public interface JbootRedis { * 将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。 */ @SuppressWarnings("unchecked") - public T rpoplpush(Object srcKey, Object dstKey); + T rpoplpush(Object srcKey, Object dstKey); /** * 将一个或多个值 value 插入到列表 key 的表尾(最右边)。 @@ -420,21 +419,21 @@ public interface JbootRedis { * 如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。 * 当 key 存在但不是列表类型时,返回一个错误。 */ - public Long rpush(Object key, Object... values); + Long rpush(Object key, Object... values); /** * BLPOP 是列表的阻塞式(blocking)弹出原语。 * 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。 */ - public List blpop(Object... keys); + List blpop(Object... keys); /** * BLPOP 是列表的阻塞式(blocking)弹出原语。 * 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。 */ - public List blpop(Integer timeout, Object... keys); + List blpop(Integer timeout, Object... keys); /** * BRPOP 是列表的阻塞式(blocking)弹出原语。 @@ -442,7 +441,7 @@ public interface JbootRedis { * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。 * 关于阻塞操作的更多信息,请查看 BLPOP 命令, BRPOP 除了弹出元素的位置和 BLPOP 不同之外,其他表现一致。 */ - public List brpop(Object... keys); + List brpop(Object... keys); /** * BRPOP 是列表的阻塞式(blocking)弹出原语。 @@ -450,52 +449,52 @@ public interface JbootRedis { * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。 * 关于阻塞操作的更多信息,请查看 BLPOP 命令, BRPOP 除了弹出元素的位置和 BLPOP 不同之外,其他表现一致。 */ - public List brpop(Integer timeout, Object... keys); + List brpop(Integer timeout, Object... keys); /** * 使用客户端向 JbootRedis 服务器发送一个 PING ,如果服务器运作正常的话,会返回一个 PONG 。 * 通常用于测试与服务器的连接是否仍然生效,或者用于测量延迟值。 */ - public String ping(); + String ping(); /** * 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。 * 假如 key 不存在,则创建一个只包含 member 元素作成员的集合。 * 当 key 不是集合类型时,返回一个错误。 */ - public Long sadd(Object key, Object... members); + Long sadd(Object key, Object... members); /** * 返回集合 key 的基数(集合中元素的数量)。 */ - public Long scard(Object key); + Long scard(Object key); /** * 移除并返回集合中的一个随机元素。 * 如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用 SRANDMEMBER 命令。 */ - public T spop(Object key); + T spop(Object key); /** * 返回集合 key 中的所有成员。 * 不存在的 key 被视为空集合。 */ - public Set smembers(Object key); + Set smembers(Object key); /** * 判断 member 元素是否集合 key 的成员。 */ - public boolean sismember(Object key, Object member); + boolean sismember(Object key, Object member); /** * 返回多个集合的交集,多个集合由 keys 指定 */ - public Set sinter(Object... keys); + Set sinter(Object... keys); /** * 返回集合中的一个随机元素。 */ - public T srandmember(Object key); + T srandmember(Object key); /** * 返回集合中的 count 个随机元素。 @@ -504,49 +503,49 @@ public interface JbootRedis { * 如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。 * 该操作和 SPOP 相似,但 SPOP 将随机元素从集合中移除并返回,而 SRANDMEMBER 则仅仅返回随机元素,而不对集合进行任何改动。 */ - public List srandmember(Object key, int count); + List srandmember(Object key, int count); /** * 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。 */ - public Long srem(Object key, Object... members); + Long srem(Object key, Object... members); /** * 返回多个集合的并集,多个集合由 keys 指定 * 不存在的 key 被视为空集。 */ - public Set sunion(Object... keys); + Set sunion(Object... keys); /** * 返回一个集合的全部成员,该集合是所有给定集合之间的差集。 * 不存在的 key 被视为空集。 */ - public Set sdiff(Object... keys); + Set sdiff(Object... keys); /** * 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。 * 如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值, * 并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。 */ - public Long zadd(Object key, double score, Object member); + Long zadd(Object key, double score, Object member); - public Long zadd(Object key, Map scoreMembers); + Long zadd(Object key, Map scoreMembers); /** * 返回有序集 key 的基数。 */ - public Long zcard(Object key); + Long zcard(Object key); /** * 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。 * 关于参数 min 和 max 的详细使用方法,请参考 ZRANGEBYSCORE 命令。 */ - public Long zcount(Object key, double min, double max); + Long zcount(Object key, double min, double max); /** * 为有序集 key 的成员 member 的 score 值加上增量 increment 。 */ - public Double zincrby(Object key, double score, Object member); + Double zincrby(Object key, double score, Object member); /** * 返回有序集 key 中,指定区间内的成员。 @@ -554,7 +553,7 @@ public interface JbootRedis { * 具有相同 score 值的成员按字典序(lexicographical order )来排列。 * 如果你需要成员按 score 值递减(从大到小)来排列,请使用 ZREVRANGE 命令。 */ - public Set zrange(Object key, long start, long end); + Set zrange(Object key, long start, long end); /** * 返回有序集 key 中,指定区间内的成员。 @@ -562,39 +561,39 @@ public interface JbootRedis { * 具有相同 score 值的成员按字典序的逆序(reverse lexicographical order)排列。 * 除了成员按 score 值递减的次序排列这一点外, ZREVRANGE 命令的其他方面和 ZRANGE 命令一样。 */ - public Set zrevrange(Object key, long start, long end); + Set zrevrange(Object key, long start, long end); /** * 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。 * 有序集成员按 score 值递增(从小到大)次序排列。 */ - public Set zrangeByScore(Object key, double min, double max); + Set zrangeByScore(Object key, double min, double max); /** * 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。 * 排名以 0 为底,也就是说, score 值最小的成员排名为 0 。 * 使用 ZREVRANK 命令可以获得成员按 score 值递减(从大到小)排列的排名。 */ - public Long zrank(Object key, Object member); + Long zrank(Object key, Object member); /** * 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递减(从大到小)排序。 * 排名以 0 为底,也就是说, score 值最大的成员排名为 0 。 * 使用 ZRANK 命令可以获得成员按 score 值递增(从小到大)排列的排名。 */ - public Long zrevrank(Object key, Object member); + Long zrevrank(Object key, Object member); /** * 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。 * 当 key 存在但不是有序集类型时,返回一个错误。 */ - public Long zrem(Object key, Object... members); + Long zrem(Object key, Object... members); /** * 返回有序集 key 中,成员 member 的 score 值。 * 如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。 */ - public Double zscore(Object key, Object member); + Double zscore(Object key, Object member); /** * 发布 @@ -602,7 +601,7 @@ public interface JbootRedis { * @param channel * @param message */ - public void publish(String channel, String message); + void publish(String channel, String message); /** * 发布 @@ -610,7 +609,7 @@ public interface JbootRedis { * @param channel * @param message */ - public void publish(byte[] channel, byte[] message); + void publish(byte[] channel, byte[] message); /** @@ -619,7 +618,7 @@ public interface JbootRedis { * @param listener * @param channels */ - public void subscribe(JedisPubSub listener, final String... channels); + void subscribe(JedisPubSub listener, final String... channels); /** * 订阅 @@ -627,28 +626,38 @@ public interface JbootRedis { * @param binaryListener * @param channels */ - public void subscribe(BinaryJedisPubSub binaryListener, final byte[]... channels); + void subscribe(BinaryJedisPubSub binaryListener, final byte[]... channels); + + /** + * 扫描 + * + * @param pattern + * @param scanCount + * @return + */ + RedisScanResult scan(String pattern, String cursor, int scanCount); - public byte[] keyToBytes(Object key); + byte[] keyToBytes(Object key); - public String bytesToKey(byte[] bytes); + String bytesToKey(byte[] bytes); - public byte[][] keysToBytesArray(Object... keys); + byte[][] keysToBytesArray(Object... keys); - public void fieldSetFromBytesSet(Set data, Set result); + void fieldSetFromBytesSet(Set data, Set result); - public byte[] valueToBytes(Object value); + byte[] valueToBytes(Object value); - public Object valueFromBytes(byte[] bytes); + Object valueFromBytes(byte[] bytes); - public byte[][] valuesToBytesArray(Object... valuesArray); + byte[][] valuesToBytesArray(Object... valuesArray); - public void valueSetFromBytesSet(Set data, Set result); + void valueSetFromBytesSet(Set data, Set result); - public List valueListFromBytesList(Collection data); + List valueListFromBytesList(Collection data); + Object eval(String script, int keyCount, String... params); } diff --git a/src/main/java/io/jboot/support/redis/JbootRedisBase.java b/src/main/java/io/jboot/support/redis/JbootRedisBase.java index 60f21c8cc281c56b6b3bdbfa87f0058e0992dd98..c2a55dbca5dd6eb3474009e319330ecc7d8e6043 100644 --- a/src/main/java/io/jboot/support/redis/JbootRedisBase.java +++ b/src/main/java/io/jboot/support/redis/JbootRedisBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,9 +32,8 @@ import java.util.Set; */ public abstract class JbootRedisBase implements JbootRedis { - private static final Log LOG = Log.getLog(JbootRedisBase.class); - private final JbootSerializer serializer; + private boolean close = false; public JbootRedisBase(JbootRedisConfig config) { if (config == null || StrUtil.isBlank(config.getSerializer())) { @@ -42,17 +41,30 @@ public abstract class JbootRedisBase implements JbootRedis { } else { serializer = JbootSerializerManager.me().getSerializer(config.getSerializer()); } + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + close = true; + System.err.println("JbootApplication exited, redis disconnection."); + }, "jboot-redis-hook")); + } + + + public boolean isClose() { + return close; } + @Override public byte[] keyToBytes(Object key) { return key.toString().getBytes(); } + @Override public String bytesToKey(byte[] bytes) { return new String(bytes); } + @Override public byte[][] keysToBytesArray(Object... keys) { byte[][] result = new byte[keys.length][]; for (int i = 0; i < result.length; i++) @@ -61,16 +73,19 @@ public abstract class JbootRedisBase implements JbootRedis { } + @Override public void fieldSetFromBytesSet(Set data, Set result) { for (byte[] fieldBytes : data) { result.add(valueFromBytes(fieldBytes)); } } + @Override public byte[] valueToBytes(Object value) { return serializer.serialize(value); } + @Override public Object valueFromBytes(byte[] bytes) { if (bytes == null || bytes.length == 0) { return null; @@ -78,6 +93,7 @@ public abstract class JbootRedisBase implements JbootRedis { return serializer.deserialize(bytes); } + @Override public byte[][] valuesToBytesArray(Object... valuesArray) { byte[][] data = new byte[valuesArray.length][]; for (int i = 0; i < data.length; i++) @@ -85,12 +101,14 @@ public abstract class JbootRedisBase implements JbootRedis { return data; } + @Override public void valueSetFromBytesSet(Set data, Set result) { for (byte[] valueBytes : data) { result.add(valueFromBytes(valueBytes)); } } + @Override @SuppressWarnings("rawtypes") public List valueListFromBytesList(Collection data) { List result = new ArrayList(); diff --git a/src/main/java/io/jboot/support/redis/JbootRedisConfig.java b/src/main/java/io/jboot/support/redis/JbootRedisConfig.java index f9efce55d5e4a4b6a19b403307272d1f88cc7c38..58c15157c71529139529d19faeb1d9ad1740bc1e 100644 --- a/src/main/java/io/jboot/support/redis/JbootRedisConfig.java +++ b/src/main/java/io/jboot/support/redis/JbootRedisConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,11 +29,12 @@ public class JbootRedisConfig { public static final String TYPE_REDISSON = "redisson"; public static final String TYPE_LETTUCE = "lettuce"; + private String name = "default"; private String host; private Integer port = 6379; private Integer timeout = 2000; private String password; - private Integer database; + private Integer database = 0; private String clientName; private Boolean testOnCreate; private Boolean testOnBorrow; @@ -51,6 +52,13 @@ public class JbootRedisConfig { private String serializer; + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } public String getHost() { return host; diff --git a/src/main/java/io/jboot/support/redis/JbootRedisLock.java b/src/main/java/io/jboot/support/redis/JbootRedisLock.java index 1d009caf6cf8b3b3d8003908c94113bcb5632361..9387206b18a9b8dbfa8cc7016d791716f7077783 100644 --- a/src/main/java/io/jboot/support/redis/JbootRedisLock.java +++ b/src/main/java/io/jboot/support/redis/JbootRedisLock.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -16,6 +16,7 @@ package io.jboot.support.redis; import io.jboot.Jboot; +import io.jboot.utils.QuietlyUtil; /** * Created by michael. @@ -45,10 +46,13 @@ import io.jboot.Jboot; */ public class JbootRedisLock { - long expireMsecs = 1000 * 60;//60秒expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放 - long timeoutMsecs = 0;// 锁等待超时 + // 60 秒 expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放 + long expireMsecs = 1000 * 60; - private String lockName; + // 锁等待超时 + long timeoutMsecs = 0; + + private final String lockName; private boolean locked = false; private JbootRedis redis; @@ -59,7 +63,7 @@ public class JbootRedisLock { */ public JbootRedisLock(String lockName) { if (lockName == null) { - throw new NullPointerException("lockName must not null !"); + throw new NullPointerException("lockName must not be null."); } this.lockName = lockName; this.redis = Jboot.getRedis(); @@ -73,7 +77,7 @@ public class JbootRedisLock { */ public JbootRedisLock(String lockName, long timeoutMsecs) { if (lockName == null) { - throw new NullPointerException("lockName must not null !"); + throw new NullPointerException("lockName must not be null."); } this.lockName = lockName; this.timeoutMsecs = timeoutMsecs; @@ -81,6 +85,39 @@ public class JbootRedisLock { } + /** + * @param lockName 锁名称 + * @param timeoutMsecs 获取锁的时候,等待时长 + * @param expireMsecs 超时时长 + */ + public JbootRedisLock(String lockName, long timeoutMsecs, long expireMsecs) { + if (lockName == null) { + throw new NullPointerException("lockName must not be null."); + } + this.lockName = lockName; + this.timeoutMsecs = timeoutMsecs; + this.expireMsecs = expireMsecs; + + this.redis = Jboot.getRedis(); + } + + + public long getTimeoutMsecs() { + return timeoutMsecs; + } + + public void setTimeoutMsecs(long timeoutMsecs) { + this.timeoutMsecs = timeoutMsecs; + } + + public long getExpireMsecs() { + return expireMsecs; + } + + public void setExpireMsecs(long expireMsecs) { + this.expireMsecs = expireMsecs; + } + public void runIfAcquired(Runnable runnable) { if (runnable == null) { throw new NullPointerException("runnable must not null!"); @@ -103,44 +140,51 @@ public class JbootRedisLock { */ public boolean acquire() { long timeout = timeoutMsecs; - do { - long expires = System.currentTimeMillis() + expireMsecs + 1; + try { + //超时时间 + long expiredTime = System.currentTimeMillis() + expireMsecs + 1; - Long result = redis.setnx(lockName, expires); - if (result != null && result == 1) { - // lock acquired - locked = true; - return true; - } + Long result = redis.setnx(lockName, expiredTime); - Long currentValue = redis.get(lockName); - if (currentValue != null && currentValue < System.currentTimeMillis()) { - //判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的 - // lock is expired - - Long oldValue = redis.getSet(lockName, expires); - //获取上一个锁到期时间,并设置现在的锁到期时间, - //只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的 - if (oldValue != null && oldValue.equals(currentValue)) { - //如果这个时候,多个线程恰好都到了这里 - //只有一个线程的设置值和当前值相同,他才有权利获取锁 - //lock acquired + if (result == null) { + continue; + } + + // lock acquired + if (result == 1) { locked = true; return true; } - } - if (timeout > 0) { - timeout -= 100; - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); + //之前的保存时间 + Long savedValue = redis.get(lockName); + + //这个锁已经过期了,此时可以去设置新的key + if (savedValue != null && savedValue < System.currentTimeMillis()) { + + + //获取上一个锁到期时间,并设置现在的锁到期时间, + //只有一获个线程才能取上一个线上的设置时间,因为jedis.getSet是同步的 + Long oldValue = redis.getSet(lockName, expiredTime); + + + if (oldValue != null && oldValue.equals(savedValue)) { + //如果这个时候,多个线程恰好都到了这里 + //只有一个线程的设置值和当前值相同,他才有权利获取锁 + //lock acquired + locked = true; + return true; + } + } + } finally { + if (timeout > 0) { + timeout -= 100; + QuietlyUtil.sleepQuietly(100); } } - } while (timeout > 0); + return false; } diff --git a/src/main/java/io/jboot/support/redis/JbootRedisManager.java b/src/main/java/io/jboot/support/redis/JbootRedisManager.java index 8be2ee7047856d06c004c43ce4b7471803f95c7c..8c436671a64789c32721e730be841c0beb06c018 100644 --- a/src/main/java/io/jboot/support/redis/JbootRedisManager.java +++ b/src/main/java/io/jboot/support/redis/JbootRedisManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,14 @@ package io.jboot.support.redis; import io.jboot.Jboot; +import io.jboot.utils.ConfigUtil; +import io.jboot.exception.JbootException; +import io.jboot.exception.JbootIllegalConfigException; import io.jboot.support.redis.jedis.JbootJedisClusterImpl; import io.jboot.support.redis.jedis.JbootJedisImpl; -import io.jboot.exception.JbootException; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * 参考: com.jfinal.plugin.redis @@ -37,6 +42,7 @@ public class JbootRedisManager { } private JbootRedis redis; + private Map jbootRedisMap = new ConcurrentHashMap<>(); public JbootRedis getRedis() { if (redis == null) { @@ -47,6 +53,28 @@ public class JbootRedisManager { return redis; } + public JbootRedis getRedis(String name) { + JbootRedis redis = jbootRedisMap.get(name); + if (redis == null) { + synchronized (this) { + redis = jbootRedisMap.get(name); + if (redis == null) { + Map configModels = ConfigUtil.getConfigModels(JbootRedisConfig.class); + if (!configModels.containsKey(name)) { + throw new JbootIllegalConfigException("Please config \"jboot.redis." + name + ".host\" in your jboot.properties."); + } + JbootRedisConfig jbootRedisConfig = configModels.get(name); + redis = getRedis(jbootRedisConfig); + if (redis != null) { + jbootRedisMap.put(name, redis); + } + } + } + } + return redis; + } + + public JbootRedis getRedis(JbootRedisConfig config) { if (config == null || !config.isConfigOk()) { return null; @@ -54,19 +82,17 @@ public class JbootRedisManager { switch (config.getType()) { case JbootRedisConfig.TYPE_JEDIS: - return getJedisClinet(config); + return getJedisClient(config); case JbootRedisConfig.TYPE_LETTUCE: return getLettuceClient(config); case JbootRedisConfig.TYPE_REDISSON: return getRedissonClient(config); } - return null; - } - private JbootRedis getJedisClinet(JbootRedisConfig config) { + private JbootRedis getJedisClient(JbootRedisConfig config) { if (config.isCluster()) { return new JbootJedisClusterImpl(config); } else { diff --git a/src/main/java/io/jboot/support/redis/RedisScanResult.java b/src/main/java/io/jboot/support/redis/RedisScanResult.java new file mode 100644 index 0000000000000000000000000000000000000000..a7d02598b2940d430a9fa0fa130111fbea0296d7 --- /dev/null +++ b/src/main/java/io/jboot/support/redis/RedisScanResult.java @@ -0,0 +1,40 @@ +package io.jboot.support.redis; + + +import java.io.Serializable; +import java.util.List; + +public class RedisScanResult implements Serializable { + private String cursor; + private List results; + + public RedisScanResult() { + + } + + public RedisScanResult(String cursor, List results) { + this.cursor = cursor; + this.results = results; + } + + public String getCursor() { + return cursor; + } + + public void setCursor(String cursor) { + this.cursor = cursor; + } + + public List getResults() { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + public boolean isCompleteIteration() { + return "0".equals(getCursor()); + } + +} diff --git a/src/main/java/io/jboot/support/redis/jedis/JbootJedisClusterImpl.java b/src/main/java/io/jboot/support/redis/jedis/JbootJedisClusterImpl.java index f0b05112cedd2046662cb531dad0bf66166c4a5e..df7dd63aa8495aab325d5e8130dbdb21ddc4eee6 100644 --- a/src/main/java/io/jboot/support/redis/jedis/JbootJedisClusterImpl.java +++ b/src/main/java/io/jboot/support/redis/jedis/JbootJedisClusterImpl.java @@ -1,1227 +1,1332 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.support.redis.jedis; - -import com.jfinal.log.Log; -import io.jboot.utils.StrUtil; -import io.jboot.support.redis.JbootRedisBase; -import io.jboot.support.redis.JbootRedisConfig; -import io.jboot.exception.JbootException; -import org.apache.commons.pool2.impl.GenericObjectPoolConfig; -import redis.clients.jedis.*; -import redis.clients.jedis.exceptions.JedisConnectionException; - -import java.util.*; -import java.util.Map.Entry; - -/** - * 参考: com.jfinal.plugin.redis - * JbootRedis 命令文档: http://redisdoc.com/ - */ -public class JbootJedisClusterImpl extends JbootRedisBase { - - protected JedisCluster jedisCluster; - private int timeout = 2000; - - static final Log LOG = Log.getLog(JbootJedisClusterImpl.class); - - - public JbootJedisClusterImpl(JbootRedisConfig config) { - - super(config); - - Integer timeout = config.getTimeout(); - String password = config.getPassword(); - Integer maxAttempts = config.getMaxAttempts(); - - if (timeout != null) { - this.timeout = timeout; - } - - - GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); - - if (StrUtil.isNotBlank(config.getTestWhileIdle())) { - poolConfig.setTestWhileIdle(config.getTestWhileIdle()); - } - - if (StrUtil.isNotBlank(config.getTestOnBorrow())) { - poolConfig.setTestOnBorrow(config.getTestOnBorrow()); - } - - if (StrUtil.isNotBlank(config.getTestOnCreate())) { - poolConfig.setTestOnCreate(config.getTestOnCreate()); - } - - if (StrUtil.isNotBlank(config.getTestOnReturn())) { - poolConfig.setTestOnReturn(config.getTestOnReturn()); - } - - if (StrUtil.isNotBlank(config.getMinEvictableIdleTimeMillis())) { - poolConfig.setMinEvictableIdleTimeMillis(config.getMinEvictableIdleTimeMillis()); - } - - if (StrUtil.isNotBlank(config.getTimeBetweenEvictionRunsMillis())) { - poolConfig.setTimeBetweenEvictionRunsMillis(config.getTimeBetweenEvictionRunsMillis()); - } - - if (StrUtil.isNotBlank(config.getNumTestsPerEvictionRun())) { - poolConfig.setNumTestsPerEvictionRun(config.getNumTestsPerEvictionRun()); - } - - if (StrUtil.isNotBlank(config.getMaxTotal())) { - poolConfig.setMaxTotal(config.getMaxTotal()); - } - - if (StrUtil.isNotBlank(config.getMaxIdle())) { - poolConfig.setMaxIdle(config.getMaxIdle()); - } - - if (StrUtil.isNotBlank(config.getMinIdle())) { - poolConfig.setMinIdle(config.getMinIdle()); - } - - if (StrUtil.isNotBlank(config.getMaxWaitMillis())) { - poolConfig.setMaxWaitMillis(config.getMaxWaitMillis()); - } - this.jedisCluster = newJedisCluster(config.getHostAndPorts(), timeout, maxAttempts, password, poolConfig); - } - - public static JedisCluster newJedisCluster(Set haps, Integer timeout, - Integer maxAttempts, String password, GenericObjectPoolConfig poolConfig) { - JedisCluster jedisCluster; - - if (timeout != null && maxAttempts != null && password != null && poolConfig != null) { - jedisCluster = new JedisCluster(haps, timeout, timeout, maxAttempts, password, poolConfig); - } else if (timeout != null && maxAttempts != null && poolConfig != null) { - jedisCluster = new JedisCluster(haps, timeout, maxAttempts, poolConfig); - } else if (timeout != null && maxAttempts != null) { - jedisCluster = new JedisCluster(haps, timeout, maxAttempts); - } else if (timeout != null && poolConfig != null) { - jedisCluster = new JedisCluster(haps, timeout, poolConfig); - } else if (timeout != null) { - jedisCluster = new JedisCluster(haps, timeout); - } else { - jedisCluster = new JedisCluster(haps); - } - return jedisCluster; - } - - public JbootJedisClusterImpl(JedisCluster jedisCluster) { - super(null); - this.jedisCluster = jedisCluster; - } - - /** - * 存放 key value 对到 redis - * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。 - * 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。 - */ - @Override - public String set(Object key, Object value) { - return jedisCluster.set(keyToBytes(key), valueToBytes(value)); - } - - @Override - public Long setnx(Object key, Object value) { - return jedisCluster.setnx(keyToBytes(key), valueToBytes(value)); - } - - /** - * 存放 key value 对到 redis - * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。 - * 此方法用了修改 incr 等的值 - */ - public String setWithoutSerialize(Object key, Object value) { - return jedisCluster.set(keyToBytes(key), value.toString().getBytes()); - } - - - /** - * 存放 key value 对到 redis,并将 key 的生存时间设为 seconds (以秒为单位)。 - * 如果 key 已经存在, SETEX 命令将覆写旧值。 - */ - public String setex(Object key, int seconds, Object value) { - - return jedisCluster.setex(keyToBytes(key), seconds, valueToBytes(value)); - - } - - /** - * 返回 key 所关联的 value 值 - * 如果 key 不存在那么返回特殊值 nil 。 - */ - @SuppressWarnings("unchecked") - public T get(Object key) { - - return (T) valueFromBytes(jedisCluster.get(keyToBytes(key))); - - } - - @Override - public String getWithoutSerialize(Object key) { - byte[] bytes = jedisCluster.get(keyToBytes(key)); - if (bytes == null || bytes.length == 0) { - return null; - } - return new String(jedisCluster.get(keyToBytes(key))); - } - - /** - * 删除给定的一个 key - * 不存在的 key 会被忽略。 - */ - public Long del(Object key) { - return jedisCluster.del(keyToBytes(key)); - } - - /** - * 删除给定的多个 key - * 不存在的 key 会被忽略。 - */ - public Long del(Object... keys) { - - return jedisCluster.del(keysToBytesArray(keys)); - - } - - /** - * 查找所有符合给定模式 pattern 的 key 。 - * KEYS * 匹配数据库中所有 key 。 - * KEYS h?llo 匹配 hello , hallo 和 hxllo 等。 - * KEYS h*llo 匹配 hllo 和 heeeeello 等。 - * KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。 - * 特殊符号用 \ 隔开 - */ - public Set keys(String pattern) { - HashSet keys = new HashSet<>(); - Map clusterNodes = jedisCluster.getClusterNodes(); - for (String k : clusterNodes.keySet()) { - JedisPool jp = clusterNodes.get(k); - Jedis jedis = jp.getResource(); - try { - keys.addAll(jedis.keys(pattern)); - } catch (Exception e) { - LOG.error(e.toString(), e); - } finally { - jedis.close(); //用完一定要close这个链接!!! - } - } - return keys; - } - - - /** - * 同时设置一个或多个 key-value 对。 - * 如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是你所希望的效果,请考虑使用 MSETNX 命令:它只会在所有给定 key 都不存在的情况下进行设置操作。 - * MSET 是一个原子性(atomic)操作,所有给定 key 都会在同一时间内被设置,某些给定 key 被更新而另一些给定 key 没有改变的情况,不可能发生。 - *

-     * 例子:
-     * Cache cache = RedisKit.use();			// 使用 JbootRedis 的 cache
-     * cache.mset("k1", "v1", "k2", "v2");		// 放入多个 key value 键值对
-     * List list = cache.mget("k1", "k2");		// 利用多个键值得到上面代码放入的值
-     * 
- */ - public String mset(Object... keysValues) { - if (keysValues.length % 2 != 0) - throw new IllegalArgumentException("wrong number of arguments for met, keysValues length can not be odd"); - - byte[][] kv = new byte[keysValues.length][]; - for (int i = 0; i < keysValues.length; i++) { - if (i % 2 == 0) - kv[i] = keyToBytes(keysValues[i]); - else - kv[i] = valueToBytes(keysValues[i]); - } - return jedisCluster.mset(kv); - - } - - /** - * 返回所有(一个或多个)给定 key 的值。 - * 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。 - */ - @SuppressWarnings("rawtypes") - public List mget(Object... keys) { - - byte[][] keysBytesArray = keysToBytesArray(keys); - List data = jedisCluster.mget(keysBytesArray); - return valueListFromBytesList(data); - - } - - /** - * 将 key 中储存的数字值减一。 - * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECR 操作。 - * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 - * 本操作的值限制在 64 位(bit)有符号数字表示之内。 - * 关于递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 - */ - public Long decr(Object key) { - - return jedisCluster.decr(keyToBytes(key)); - - } - - /** - * 将 key 所储存的值减去减量 decrement 。 - * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECRBY 操作。 - * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 - * 本操作的值限制在 64 位(bit)有符号数字表示之内。 - * 关于更多递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 - */ - public Long decrBy(Object key, long longValue) { - - return jedisCluster.decrBy(keyToBytes(key), longValue); - - } - - /** - * 将 key 中储存的数字值增一。 - * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。 - * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 - * 本操作的值限制在 64 位(bit)有符号数字表示之内。 - */ - public Long incr(Object key) { - - return jedisCluster.incr(keyToBytes(key)); - - } - - /** - * 将 key 所储存的值加上增量 increment 。 - * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。 - * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 - * 本操作的值限制在 64 位(bit)有符号数字表示之内。 - * 关于递增(increment) / 递减(decrement)操作的更多信息,参见 INCR 命令。 - */ - public Long incrBy(Object key, long longValue) { - return jedisCluster.incrBy(keyToBytes(key), longValue); - - } - - /** - * 检查给定 key 是否存在。 - */ - public boolean exists(Object key) { - - return jedisCluster.exists(keyToBytes(key)); - - } - - /** - * 从当前数据库中随机返回(不删除)一个 key 。 - */ - public String randomKey() { - - throw new JbootException("not support randomKey commmand in redis cluster."); - - } - - /** - * 将 key 改名为 newkey 。 - * 当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。 - * 当 newkey 已经存在时, RENAME 命令将覆盖旧值。 - */ - public String rename(Object oldkey, Object newkey) { - - return jedisCluster.rename(keyToBytes(oldkey), keyToBytes(newkey)); - - } - - /** - * 将当前数据库的 key 移动到给定的数据库 db 当中。 - * 如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。 - * 因此,也可以利用这一特性,将 MOVE 当作锁(locking)原语(primitive)。 - */ - public Long move(Object key, int dbIndex) { - -// return jedisCluster.move(keyToBytes(key), dbIndex); - throw new JbootException("not support move commmand in redis cluster."); - - } - - /** - * 将 key 原子性地从当前实例传送到目标实例的指定数据库上,一旦传送成功, key 保证会出现在目标实例上,而当前实例上的 key 会被删除。 - */ - public String migrate(String host, int port, Object key, int destinationDb, int timeout) { - - throw new JbootException("not support migrate commmand in redis cluster."); - - } - - /** - * 切换到指定的数据库,数据库索引号 index 用数字值指定,以 0 作为起始索引值。 - * 默认使用 0 号数据库。 - * 注意:在 Jedis 对象被关闭时,数据库又会重新被设置为初始值,所以本方法 select(...) - * 正常工作需要使用如下方式之一: - * 1:使用 RedisInterceptor,在本线程内共享同一个 Jedis 对象 - * 2:使用 JbootRedis.call(ICallback) 进行操作 - * 3:自行获取 Jedis 对象进行操作 - */ - public String select(int databaseIndex) { - - return jedisCluster.select(databaseIndex); - - } - - /** - * 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。 - * 在 JbootRedis 中,带有生存时间的 key 被称为『易失的』(volatile)。 - */ - public Long expire(Object key, int seconds) { - - return jedisCluster.expire(keyToBytes(key), seconds); - - } - - /** - * EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 - */ - public Long expireAt(Object key, long unixTime) { - - return jedisCluster.expireAt(keyToBytes(key), unixTime); - - } - - /** - * 这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置 key 的生存时间,而不像 EXPIRE 命令那样,以秒为单位。 - */ - public Long pexpire(Object key, long milliseconds) { - - return jedisCluster.pexpire(keyToBytes(key), milliseconds); - - } - - /** - * 这个命令和 EXPIREAT 命令类似,但它以毫秒为单位设置 key 的过期 unix 时间戳,而不是像 EXPIREAT 那样,以秒为单位。 - */ - public Long pexpireAt(Object key, long millisecondsTimestamp) { - - return jedisCluster.pexpireAt(keyToBytes(key), millisecondsTimestamp); - - } - - /** - * 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 - * 当 key 存在但不是字符串类型时,返回一个错误。 - */ - @SuppressWarnings("unchecked") - public T getSet(Object key, Object value) { - - return (T) valueFromBytes(jedisCluster.getSet(keyToBytes(key), valueToBytes(value))); - - } - - /** - * 移除给定 key 的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )。 - */ - public Long persist(Object key) { - - return jedisCluster.persist(keyToBytes(key)); - - } - - /** - * 返回 key 所储存的值的类型。 - */ - public String type(Object key) { - - return jedisCluster.type(keyToBytes(key)); - - } - - /** - * 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。 - */ - public Long ttl(Object key) { - - return jedisCluster.ttl(keyToBytes(key)); - - } - - /** - * 这个命令类似于 TTL 命令,但它以毫秒为单位返回 key 的剩余生存时间,而不是像 TTL 命令那样,以秒为单位。 - */ - public Long pttl(Object key) { - - return jedisCluster.pttl(key.toString()); - - } - - /** - * 对象被引用的数量 - */ - public Long objectRefcount(Object key) { - -// return jedisCluster.objectRefcount(keyToBytes(key)); - throw new JbootException("not support move objectRefcount in redis cluster."); - } - - /** - * 对象没有被访问的空闲时间 - */ - public Long objectIdletime(Object key) { - -// return jedisCluster.objectIdletime(keyToBytes(key)); - throw new JbootException("not support move objectIdletime in redis cluster."); - - } - - /** - * 将哈希表 key 中的域 field 的值设为 value 。 - * 如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。 - * 如果域 field 已经存在于哈希表中,旧值将被覆盖。 - */ - public Long hset(Object key, Object field, Object value) { - - return jedisCluster.hset(keyToBytes(key), valueToBytes(field), valueToBytes(value)); - - } - - /** - * 同时将多个 field-value (域-值)对设置到哈希表 key 中。 - * 此命令会覆盖哈希表中已存在的域。 - * 如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。 - */ - public String hmset(Object key, Map hash) { - - Map para = new HashMap(); - for (Entry e : hash.entrySet()) - para.put(valueToBytes(e.getKey()), valueToBytes(e.getValue())); - return jedisCluster.hmset(keyToBytes(key), para); - - } - - /** - * 返回哈希表 key 中给定域 field 的值。 - */ - @SuppressWarnings("unchecked") - public T hget(Object key, Object field) { - - return (T) valueFromBytes(jedisCluster.hget(keyToBytes(key), valueToBytes(field))); - - } - - /** - * 返回哈希表 key 中,一个或多个给定域的值。 - * 如果给定的域不存在于哈希表,那么返回一个 nil 值。 - * 因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。 - */ - @SuppressWarnings("rawtypes") - public List hmget(Object key, Object... fields) { - - List data = jedisCluster.hmget(keyToBytes(key), valuesToBytesArray(fields)); - return valueListFromBytesList(data); - - } - - /** - * 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 - */ - public Long hdel(Object key, Object... fields) { - - return jedisCluster.hdel(keyToBytes(key), valuesToBytesArray(fields)); - - } - - /** - * 查看哈希表 key 中,给定域 field 是否存在。 - */ - public boolean hexists(Object key, Object field) { - - return jedisCluster.hexists(keyToBytes(key), valueToBytes(field)); - - } - - /** - * 返回哈希表 key 中,所有的域和值。 - * 在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。 - */ - @SuppressWarnings("rawtypes") - public Map hgetAll(Object key) { - - Map data = jedisCluster.hgetAll(keyToBytes(key)); - Map result = new HashMap(); - for (Entry e : data.entrySet()) - result.put(valueFromBytes(e.getKey()), valueFromBytes(e.getValue())); - return result; - - } - - /** - * 返回哈希表 key 中所有域的值。 - */ - @SuppressWarnings("rawtypes") - public List hvals(Object key) { - - Collection data = jedisCluster.hvals(keyToBytes(key)); - return valueListFromBytesList(data); - - } - - /** - * 返回哈希表 key 中的所有域。 - * 底层实现此方法取名为 hfields 更为合适,在此仅为与底层保持一致 - */ - public Set hkeys(Object key) { - - Set fieldSet = jedisCluster.hkeys(keyToBytes(key)); - Set result = new HashSet(); - fieldSetFromBytesSet(fieldSet, result); - return result; - - } - - /** - * 返回哈希表 key 中域的数量。 - */ - public Long hlen(Object key) { - - return jedisCluster.hlen(keyToBytes(key)); - - } - - /** - * 为哈希表 key 中的域 field 的值加上增量 increment 。 - * 增量也可以为负数,相当于对给定域进行减法操作。 - * 如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。 - * 如果域 field 不存在,那么在执行命令前,域的值被初始化为 0 。 - * 对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。 - * 本操作的值被限制在 64 位(bit)有符号数字表示之内。 - */ - public Long hincrBy(Object key, Object field, long value) { - - return jedisCluster.hincrBy(keyToBytes(key), valueToBytes(field), value); - - } - - /** - * 为哈希表 key 中的域 field 加上浮点数增量 increment 。 - * 如果哈希表中没有域 field ,那么 HINCRBYFLOAT 会先将域 field 的值设为 0 ,然后再执行加法操作。 - * 如果键 key 不存在,那么 HINCRBYFLOAT 会先创建一个哈希表,再创建域 field ,最后再执行加法操作。 - * 当以下任意一个条件发生时,返回一个错误: - * 1:域 field 的值不是字符串类型(因为 redis 中的数字和浮点数都以字符串的形式保存,所以它们都属于字符串类型) - * 2:域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number) - * HINCRBYFLOAT 命令的详细功能和 INCRBYFLOAT 命令类似,请查看 INCRBYFLOAT 命令获取更多相关信息。 - */ - public Double hincrByFloat(Object key, Object field, double value) { - - return jedisCluster.hincrByFloat(keyToBytes(key), valueToBytes(field), value); - - } - - /** - * 返回列表 key 中,下标为 index 的元素。 - * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 - * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 - * 如果 key 不是列表类型,返回一个错误。 - */ - @SuppressWarnings("unchecked") - - /** - * 返回列表 key 中,下标为 index 的元素。 - * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素, - * 以 1 表示列表的第二个元素,以此类推。 - * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 - * 如果 key 不是列表类型,返回一个错误。 - */ - public T lindex(Object key, long index) { - - return (T) valueFromBytes(jedisCluster.lindex(keyToBytes(key), index)); - - } - - - /** - * 返回列表 key 的长度。 - * 如果 key 不存在,则 key 被解释为一个空列表,返回 0 . - * 如果 key 不是列表类型,返回一个错误。 - */ - public Long llen(Object key) { - - return jedisCluster.llen(keyToBytes(key)); - - } - - /** - * 移除并返回列表 key 的头元素。 - */ - @SuppressWarnings("unchecked") - public T lpop(Object key) { - - return (T) valueFromBytes(jedisCluster.lpop(keyToBytes(key))); - - } - - /** - * 将一个或多个值 value 插入到列表 key 的表头 - * 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头: 比如说, - * 对空列表 mylist 执行命令 LPUSH mylist a b c ,列表的值将是 c b a , - * 这等同于原子性地执行 LPUSH mylist a 、 LPUSH mylist b 和 LPUSH mylist c 三个命令。 - * 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。 - * 当 key 存在但不是列表类型时,返回一个错误。 - */ - public Long lpush(Object key, Object... values) { - - return jedisCluster.lpush(keyToBytes(key), valuesToBytesArray(values)); - - } - - /** - * 将列表 key 下标为 index 的元素的值设置为 value 。 - * 当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。 - * 关于列表下标的更多信息,请参考 LINDEX 命令。 - */ - public String lset(Object key, long index, Object value) { - - return jedisCluster.lset(keyToBytes(key), index, valueToBytes(value)); - - } - - /** - * 根据参数 count 的值,移除列表中与参数 value 相等的元素。 - * count 的值可以是以下几种: - * count 大于 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。 - * count 小于 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。 - * count 等于 0 : 移除表中所有与 value 相等的值。 - */ - public Long lrem(Object key, long count, Object value) { - - return jedisCluster.lrem(keyToBytes(key), count, valueToBytes(value)); - - } - - /** - * 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。 - * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 - * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 - *
-     * 例子:
-     * 获取 list 中所有数据:cache.lrange(listKey, 0, -1);
-     * 获取 list 中下标 1 到 3 的数据: cache.lrange(listKey, 1, 3);
-     * 
- */ - @SuppressWarnings("rawtypes") - public List lrange(Object key, long start, long end) { - - List data = jedisCluster.lrange(keyToBytes(key), start, end); - if (data != null) { - return valueListFromBytesList(data); - } else { - return new ArrayList(0); - } - - } - - /** - * 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 - * 举个例子,执行命令 LTRIM list 0 2 ,表示只保留列表 list 的前三个元素,其余元素全部删除。 - * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 - * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 - * 当 key 不是列表类型时,返回一个错误。 - */ - public String ltrim(Object key, long start, long end) { - - return jedisCluster.ltrim(keyToBytes(key), start, end); - - } - - /** - * 移除并返回列表 key 的尾元素。 - */ - @SuppressWarnings("unchecked") - public T rpop(Object key) { - - return (T) valueFromBytes(jedisCluster.rpop(keyToBytes(key))); - - } - - /** - * 命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作: - * 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。 - * 将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。 - */ - @SuppressWarnings("unchecked") - public T rpoplpush(Object srcKey, Object dstKey) { - - return (T) valueFromBytes(jedisCluster.rpoplpush(keyToBytes(srcKey), keyToBytes(dstKey))); - - } - - /** - * 将一个或多个值 value 插入到列表 key 的表尾(最右边)。 - * 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表尾:比如 - * 对一个空列表 mylist 执行 RPUSH mylist a b c ,得出的结果列表为 a b c , - * 等同于执行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。 - * 如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。 - * 当 key 存在但不是列表类型时,返回一个错误。 - */ - public Long rpush(Object key, Object... values) { - - return jedisCluster.rpush(keyToBytes(key), valuesToBytesArray(values)); - - } - - /** - * BLPOP 是列表的阻塞式(blocking)弹出原语。 - * 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 - * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。 - */ - @SuppressWarnings("rawtypes") - public List blpop(Object... keys) { -// String[] keysStrings = new String[keys.length]; -// for (int i = 0; i < keys.length; i++) { -// keysStrings[i] = keys[i].toString(); -// } - - List data = jedisCluster.blpop(timeout, keysToBytesArray(keys)); - - if (data != null && data.size() == 2) { - List objects = new ArrayList<>(); - objects.add(new String(data.get(0))); - objects.add(valueFromBytes(data.get(1))); - return objects; - } - - return valueListFromBytesList(data); - - } - - /** - * BLPOP 是列表的阻塞式(blocking)弹出原语。 - * 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 - * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。 - */ - @SuppressWarnings("rawtypes") - public List blpop(Integer timeout, Object... keys) { - - List data = jedisCluster.blpop(timeout, keysToBytesArray(keys)); - return valueListFromBytesList(data); - - } - - /** - * BRPOP 是列表的阻塞式(blocking)弹出原语。 - * 它是 RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 - * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。 - * 关于阻塞操作的更多信息,请查看 BLPOP 命令, BRPOP 除了弹出元素的位置和 BLPOP 不同之外,其他表现一致。 - */ - @SuppressWarnings("rawtypes") - public List brpop(Object... keys) { - - List data = jedisCluster.brpop(timeout, keysToBytesArray(keys)); - return valueListFromBytesList(data); - - } - - /** - * BRPOP 是列表的阻塞式(blocking)弹出原语。 - * 它是 RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 - * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。 - * 关于阻塞操作的更多信息,请查看 BLPOP 命令, BRPOP 除了弹出元素的位置和 BLPOP 不同之外,其他表现一致。 - */ - @SuppressWarnings("rawtypes") - public List brpop(Integer timeout, Object... keys) { - - List data = jedisCluster.brpop(timeout, keysToBytesArray(keys)); - return valueListFromBytesList(data); - - } - - /** - * 使用客户端向 JbootRedis 服务器发送一个 PING ,如果服务器运作正常的话,会返回一个 PONG 。 - * 通常用于测试与服务器的连接是否仍然生效,或者用于测量延迟值。 - */ - public String ping() { - - return jedisCluster.ping(); - - } - - /** - * 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。 - * 假如 key 不存在,则创建一个只包含 member 元素作成员的集合。 - * 当 key 不是集合类型时,返回一个错误。 - */ - public Long sadd(Object key, Object... members) { - - return jedisCluster.sadd(keyToBytes(key), valuesToBytesArray(members)); - - } - - /** - * 返回集合 key 的基数(集合中元素的数量)。 - */ - public Long scard(Object key) { - - return jedisCluster.scard(keyToBytes(key)); - - } - - /** - * 移除并返回集合中的一个随机元素。 - * 如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用 SRANDMEMBER 命令。 - */ - @SuppressWarnings("unchecked") - public T spop(Object key) { - - return (T) valueFromBytes(jedisCluster.spop(keyToBytes(key))); - - } - - /** - * 返回集合 key 中的所有成员。 - * 不存在的 key 被视为空集合。 - */ - @SuppressWarnings("rawtypes") - public Set smembers(Object key) { - - Set data = jedisCluster.smembers(keyToBytes(key)); - Set result = new HashSet(); - valueSetFromBytesSet(data, result); - return result; - - } - - /** - * 判断 member 元素是否集合 key 的成员。 - */ - public boolean sismember(Object key, Object member) { - - return jedisCluster.sismember(keyToBytes(key), valueToBytes(member)); - - } - - /** - * 返回多个集合的交集,多个集合由 keys 指定 - */ - @SuppressWarnings("rawtypes") - public Set sinter(Object... keys) { - - Set data = jedisCluster.sinter(keysToBytesArray(keys)); - Set result = new HashSet(); - valueSetFromBytesSet(data, result); - return result; - - } - - /** - * 返回集合中的一个随机元素。 - */ - @SuppressWarnings("unchecked") - public T srandmember(Object key) { - - return (T) valueFromBytes(jedisCluster.srandmember(keyToBytes(key))); - - } - - /** - * 返回集合中的 count 个随机元素。 - * 从 JbootRedis 2.6 版本开始, SRANDMEMBER 命令接受可选的 count 参数: - * 如果 count 为正数,且小于集合基数,那么命令返回一个包含 count 个元素的数组,数组中的元素各不相同。 - * 如果 count 大于等于集合基数,那么返回整个集合。 - * 如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。 - * 该操作和 SPOP 相似,但 SPOP 将随机元素从集合中移除并返回,而 SRANDMEMBER 则仅仅返回随机元素,而不对集合进行任何改动。 - */ - @SuppressWarnings("rawtypes") - public List srandmember(Object key, int count) { - - List data = jedisCluster.srandmember(keyToBytes(key), count); - return valueListFromBytesList(data); - - } - - /** - * 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。 - */ - public Long srem(Object key, Object... members) { - - return jedisCluster.srem(keyToBytes(key), valuesToBytesArray(members)); - - } - - /** - * 返回多个集合的并集,多个集合由 keys 指定 - * 不存在的 key 被视为空集。 - */ - @SuppressWarnings("rawtypes") - public Set sunion(Object... keys) { - - Set data = jedisCluster.sunion(keysToBytesArray(keys)); - Set result = new HashSet(); - valueSetFromBytesSet(data, result); - return result; - - } - - /** - * 返回一个集合的全部成员,该集合是所有给定集合之间的差集。 - * 不存在的 key 被视为空集。 - */ - @SuppressWarnings("rawtypes") - public Set sdiff(Object... keys) { - - Set data = jedisCluster.sdiff(keysToBytesArray(keys)); - Set result = new HashSet(); - valueSetFromBytesSet(data, result); - return result; - - } - - /** - * 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。 - * 如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值, - * 并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。 - */ - public Long zadd(Object key, double score, Object member) { - - return jedisCluster.zadd(keyToBytes(key), score, valueToBytes(member)); - - } - - public Long zadd(Object key, Map scoreMembers) { - - Map para = new HashMap(); - for (Entry e : scoreMembers.entrySet()) - para.put(valueToBytes(e.getKey()), e.getValue()); // valueToBytes is important - return jedisCluster.zadd(keyToBytes(key), para); - - } - - /** - * 返回有序集 key 的基数。 - */ - public Long zcard(Object key) { - - return jedisCluster.zcard(keyToBytes(key)); - - } - - /** - * 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。 - * 关于参数 min 和 max 的详细使用方法,请参考 ZRANGEBYSCORE 命令。 - */ - public Long zcount(Object key, double min, double max) { - - return jedisCluster.zcount(keyToBytes(key), min, max); - - } - - /** - * 为有序集 key 的成员 member 的 score 值加上增量 increment 。 - */ - public Double zincrby(Object key, double score, Object member) { - - return jedisCluster.zincrby(keyToBytes(key), score, valueToBytes(member)); - - } - - /** - * 返回有序集 key 中,指定区间内的成员。 - * 其中成员的位置按 score 值递增(从小到大)来排序。 - * 具有相同 score 值的成员按字典序(lexicographical order )来排列。 - * 如果你需要成员按 score 值递减(从大到小)来排列,请使用 ZREVRANGE 命令。 - */ - @SuppressWarnings("rawtypes") - public Set zrange(Object key, long start, long end) { - - Set data = jedisCluster.zrange(keyToBytes(key), start, end); - Set result = new LinkedHashSet(); // 有序集合必须 LinkedHashSet - valueSetFromBytesSet(data, result); - return result; - - } - - /** - * 返回有序集 key 中,指定区间内的成员。 - * 其中成员的位置按 score 值递减(从大到小)来排列。 - * 具有相同 score 值的成员按字典序的逆序(reverse lexicographical order)排列。 - * 除了成员按 score 值递减的次序排列这一点外, ZREVRANGE 命令的其他方面和 ZRANGE 命令一样。 - */ - @SuppressWarnings("rawtypes") - public Set zrevrange(Object key, long start, long end) { - - Set data = jedisCluster.zrevrange(keyToBytes(key), start, end); - Set result = new LinkedHashSet(); // 有序集合必须 LinkedHashSet - valueSetFromBytesSet(data, result); - return result; - - } - - /** - * 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。 - * 有序集成员按 score 值递增(从小到大)次序排列。 - */ - @SuppressWarnings("rawtypes") - public Set zrangeByScore(Object key, double min, double max) { - - Set data = jedisCluster.zrangeByScore(keyToBytes(key), min, max); - Set result = new LinkedHashSet(); // 有序集合必须 LinkedHashSet - valueSetFromBytesSet(data, result); - return result; - - } - - /** - * 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。 - * 排名以 0 为底,也就是说, score 值最小的成员排名为 0 。 - * 使用 ZREVRANK 命令可以获得成员按 score 值递减(从大到小)排列的排名。 - */ - public Long zrank(Object key, Object member) { - - return jedisCluster.zrank(keyToBytes(key), valueToBytes(member)); - - } - - /** - * 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递减(从大到小)排序。 - * 排名以 0 为底,也就是说, score 值最大的成员排名为 0 。 - * 使用 ZRANK 命令可以获得成员按 score 值递增(从小到大)排列的排名。 - */ - public Long zrevrank(Object key, Object member) { - - return jedisCluster.zrevrank(keyToBytes(key), valueToBytes(member)); - - } - - /** - * 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。 - * 当 key 存在但不是有序集类型时,返回一个错误。 - */ - public Long zrem(Object key, Object... members) { - - return jedisCluster.zrem(keyToBytes(key), valuesToBytesArray(members)); - - } - - /** - * 返回有序集 key 中,成员 member 的 score 值。 - * 如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。 - */ - public Double zscore(Object key, Object member) { - - return jedisCluster.zscore(keyToBytes(key), valueToBytes(member)); - - } - - /** - * 发布 - * - * @param channel - * @param message - */ - public void publish(String channel, String message) { - - jedisCluster.publish(channel, message); - - } - - /** - * 发布 - * - * @param channel - * @param message - */ - public void publish(byte[] channel, byte[] message) { - - jedisCluster.publish(channel, message); - - } - - - /** - * 订阅 - * - * @param listener - * @param channels - */ - public void subscribe(JedisPubSub listener, final String... channels) { - /** - * https://github.com/xetorthio/jedis/wiki/AdvancedUsage - * Note that subscribe is a blocking operation because it will poll JbootRedis for responses on the thread that calls subscribe. - * A single JedisPubSub instance can be used to subscribe to multiple channels. - * You can call subscribe or psubscribe on an existing JedisPubSub instance to change your subscriptions. - */ - new Thread("jboot-redisCluster-subscribe-JedisPubSub") { - @Override - public void run() { - while (true) { - //订阅线程断开连接,需要进行重连 - try { - jedisCluster.subscribe(listener, channels); - LOG.warn("Disconnect to redis channel in subscribe JedisPubSub!"); - break; - } catch (JedisConnectionException e) { - LOG.error("failed connect to redis, reconnect it.", e); - try { - Thread.sleep(1000); - } catch (InterruptedException ie) { - break; - } - } - } - } - }.start(); - } - - /** - * 订阅 - * - * @param binaryListener - * @param channels - */ - public void subscribe(BinaryJedisPubSub binaryListener, final byte[]... channels) { - /** - * https://github.com/xetorthio/jedis/wiki/AdvancedUsage - * Note that subscribe is a blocking operation because it will poll JbootRedis for responses on the thread that calls subscribe. - * A single JedisPubSub instance can be used to subscribe to multiple channels. - * You can call subscribe or psubscribe on an existing JedisPubSub instance to change your subscriptions. - */ - new Thread("jboot-redisCluster-subscribe-BinaryJedisPubSub") { - @Override - public void run() { - while (true) { - //订阅线程断开连接,需要进行重连 - try { - jedisCluster.subscribe(binaryListener, channels); - LOG.warn("Disconnect to redis channel in subscribe BinaryJedisPubSub!"); - break; - } catch (Throwable e) { - LOG.error("failed connect to redis, reconnect it.", e); - try { - Thread.sleep(1000); - } catch (InterruptedException ie) { - break; - } - } - } - } - }.start(); - } - - - public JedisCluster getJedisCluster() { - return jedisCluster; - } - -} - - - - - - +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.redis.jedis; + +import com.jfinal.log.Log; +import io.jboot.exception.JbootException; +import io.jboot.support.redis.JbootRedisBase; +import io.jboot.support.redis.JbootRedisConfig; +import io.jboot.support.redis.RedisScanResult; +import io.jboot.utils.QuietlyUtil; +import io.jboot.utils.StrUtil; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import redis.clients.jedis.*; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.util.*; +import java.util.Map.Entry; + +/** + * 参考: com.jfinal.plugin.redis + * JbootRedis 命令文档: http://redisdoc.com/ + */ +public class JbootJedisClusterImpl extends JbootRedisBase { + + protected JedisCluster jedisCluster; + private int timeout = 2000; + private int maxAttempts = 5; + + static final Log LOG = Log.getLog(JbootJedisClusterImpl.class); + + + public JbootJedisClusterImpl(JbootRedisConfig config) { + + super(config); + + Integer timeout = config.getTimeout(); + String password = config.getPassword(); + Integer maxAttempts = config.getMaxAttempts(); + + if (timeout != null) { + this.timeout = timeout; + } + if (maxAttempts == null) { + maxAttempts = this.maxAttempts; + } + + GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); + + if (StrUtil.isNotBlank(config.getTestWhileIdle())) { + poolConfig.setTestWhileIdle(config.getTestWhileIdle()); + } + + if (StrUtil.isNotBlank(config.getTestOnBorrow())) { + poolConfig.setTestOnBorrow(config.getTestOnBorrow()); + } + + if (StrUtil.isNotBlank(config.getTestOnCreate())) { + poolConfig.setTestOnCreate(config.getTestOnCreate()); + } + + if (StrUtil.isNotBlank(config.getTestOnReturn())) { + poolConfig.setTestOnReturn(config.getTestOnReturn()); + } + + if (StrUtil.isNotBlank(config.getMinEvictableIdleTimeMillis())) { + poolConfig.setMinEvictableIdleTimeMillis(config.getMinEvictableIdleTimeMillis()); + } + + if (StrUtil.isNotBlank(config.getTimeBetweenEvictionRunsMillis())) { + poolConfig.setTimeBetweenEvictionRunsMillis(config.getTimeBetweenEvictionRunsMillis()); + } + + if (StrUtil.isNotBlank(config.getNumTestsPerEvictionRun())) { + poolConfig.setNumTestsPerEvictionRun(config.getNumTestsPerEvictionRun()); + } + + if (StrUtil.isNotBlank(config.getMaxTotal())) { + poolConfig.setMaxTotal(config.getMaxTotal()); + } + + if (StrUtil.isNotBlank(config.getMaxIdle())) { + poolConfig.setMaxIdle(config.getMaxIdle()); + } + + if (StrUtil.isNotBlank(config.getMinIdle())) { + poolConfig.setMinIdle(config.getMinIdle()); + } + + if (StrUtil.isNotBlank(config.getMaxWaitMillis())) { + poolConfig.setMaxWaitMillis(config.getMaxWaitMillis()); + } + this.jedisCluster = newJedisCluster(config.getHostAndPorts(), timeout, maxAttempts, password, poolConfig); + } + + public static JedisCluster newJedisCluster(Set haps, Integer timeout, + Integer maxAttempts, String password, GenericObjectPoolConfig poolConfig) { + JedisCluster jedisCluster; + + if (timeout != null && maxAttempts != null && password != null && poolConfig != null) { + jedisCluster = new JedisCluster(haps, timeout, timeout, maxAttempts, password, poolConfig); + } else if (timeout != null && maxAttempts != null && poolConfig != null) { + jedisCluster = new JedisCluster(haps, timeout, maxAttempts, poolConfig); + } else if (timeout != null && maxAttempts != null) { + jedisCluster = new JedisCluster(haps, timeout, maxAttempts); + } else if (timeout != null && poolConfig != null) { + jedisCluster = new JedisCluster(haps, timeout, poolConfig); + } else if (timeout != null) { + jedisCluster = new JedisCluster(haps, timeout); + } else { + jedisCluster = new JedisCluster(haps); + } + return jedisCluster; + } + + public JbootJedisClusterImpl(JedisCluster jedisCluster) { + super(null); + this.jedisCluster = jedisCluster; + } + + /** + * 存放 key value 对到 redis + * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。 + * 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。 + */ + @Override + public String set(Object key, Object value) { + return jedisCluster.set(keyToBytes(key), valueToBytes(value)); + } + + @Override + public Long setnx(Object key, Object value) { + return jedisCluster.setnx(keyToBytes(key), valueToBytes(value)); + } + + /** + * 存放 key value 对到 redis + * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。 + * 此方法用了修改 incr 等的值 + */ + @Override + public String setWithoutSerialize(Object key, Object value) { + return jedisCluster.set(keyToBytes(key), value.toString().getBytes()); + } + + + /** + * 存放 key value 对到 redis,并将 key 的生存时间设为 seconds (以秒为单位)。 + * 如果 key 已经存在, SETEX 命令将覆写旧值。 + */ + @Override + public String setex(Object key, int seconds, Object value) { + + return jedisCluster.setex(keyToBytes(key), seconds, valueToBytes(value)); + + } + + /** + * 返回 key 所关联的 value 值 + * 如果 key 不存在那么返回特殊值 nil 。 + */ + @Override + @SuppressWarnings("unchecked") + public T get(Object key) { + + return (T) valueFromBytes(jedisCluster.get(keyToBytes(key))); + + } + + @Override + public String getWithoutSerialize(Object key) { + byte[] bytes = jedisCluster.get(keyToBytes(key)); + if (bytes == null || bytes.length == 0) { + return null; + } + return new String(jedisCluster.get(keyToBytes(key))); + } + + /** + * 删除给定的一个 key + * 不存在的 key 会被忽略。 + */ + @Override + public Long del(Object key) { + return jedisCluster.del(keyToBytes(key)); + } + + /** + * 删除给定的多个 key + * 不存在的 key 会被忽略。 + */ + @Override + public Long del(Object... keys) { + + return jedisCluster.del(keysToBytesArray(keys)); + + } + + /** + * 查找所有符合给定模式 pattern 的 key 。 + * KEYS * 匹配数据库中所有 key 。 + * KEYS h?llo 匹配 hello , hallo 和 hxllo 等。 + * KEYS h*llo 匹配 hllo 和 heeeeello 等。 + * KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。 + * 特殊符号用 \ 隔开 + */ + @Override + public Set keys(String pattern) { + HashSet keys = new HashSet<>(); + Map clusterNodes = jedisCluster.getClusterNodes(); + for (String k : clusterNodes.keySet()) { + JedisPool jp = clusterNodes.get(k); + Jedis jedis = jp.getResource(); + try { + keys.addAll(jedis.keys(pattern)); + } catch (Exception e) { + LOG.error(e.toString(), e); + } finally { + jedis.close(); //用完一定要close这个链接!!! + } + } + return keys; + } + + + /** + * 同时设置一个或多个 key-value 对。 + * 如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是你所希望的效果,请考虑使用 MSETNX 命令:它只会在所有给定 key 都不存在的情况下进行设置操作。 + * MSET 是一个原子性(atomic)操作,所有给定 key 都会在同一时间内被设置,某些给定 key 被更新而另一些给定 key 没有改变的情况,不可能发生。 + *

+     * 例子:
+     * Cache cache = RedisKit.use();			// 使用 JbootRedis 的 cache
+     * cache.mset("k1", "v1", "k2", "v2");		// 放入多个 key value 键值对
+     * List list = cache.mget("k1", "k2");		// 利用多个键值得到上面代码放入的值
+     * 
+ */ + @Override + public String mset(Object... keysValues) { + if (keysValues.length % 2 != 0) + throw new IllegalArgumentException("wrong number of arguments for met, keysValues length can not be odd"); + + byte[][] kv = new byte[keysValues.length][]; + for (int i = 0; i < keysValues.length; i++) { + if (i % 2 == 0) + kv[i] = keyToBytes(keysValues[i]); + else + kv[i] = valueToBytes(keysValues[i]); + } + return jedisCluster.mset(kv); + + } + + /** + * 返回所有(一个或多个)给定 key 的值。 + * 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。 + */ + @Override + @SuppressWarnings("rawtypes") + public List mget(Object... keys) { + + byte[][] keysBytesArray = keysToBytesArray(keys); + List data = jedisCluster.mget(keysBytesArray); + return valueListFromBytesList(data); + + } + + /** + * 将 key 中储存的数字值减一。 + * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECR 操作。 + * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 + * 本操作的值限制在 64 位(bit)有符号数字表示之内。 + * 关于递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 + */ + @Override + public Long decr(Object key) { + + return jedisCluster.decr(keyToBytes(key)); + + } + + /** + * 将 key 所储存的值减去减量 decrement 。 + * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECRBY 操作。 + * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 + * 本操作的值限制在 64 位(bit)有符号数字表示之内。 + * 关于更多递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 + */ + @Override + public Long decrBy(Object key, long longValue) { + + return jedisCluster.decrBy(keyToBytes(key), longValue); + + } + + /** + * 将 key 中储存的数字值增一。 + * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。 + * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 + * 本操作的值限制在 64 位(bit)有符号数字表示之内。 + */ + @Override + public Long incr(Object key) { + + return jedisCluster.incr(keyToBytes(key)); + + } + + /** + * 将 key 所储存的值加上增量 increment 。 + * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。 + * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 + * 本操作的值限制在 64 位(bit)有符号数字表示之内。 + * 关于递增(increment) / 递减(decrement)操作的更多信息,参见 INCR 命令。 + */ + @Override + public Long incrBy(Object key, long longValue) { + return jedisCluster.incrBy(keyToBytes(key), longValue); + + } + + /** + * 检查给定 key 是否存在。 + */ + @Override + public boolean exists(Object key) { + + return jedisCluster.exists(keyToBytes(key)); + + } + + /** + * 从当前数据库中随机返回(不删除)一个 key 。 + */ + @Override + public String randomKey() { + + throw new JbootException("not support randomKey commmand in redis cluster."); + + } + + /** + * 将 key 改名为 newkey 。 + * 当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。 + * 当 newkey 已经存在时, RENAME 命令将覆盖旧值。 + */ + @Override + public String rename(Object oldkey, Object newkey) { + + return jedisCluster.rename(keyToBytes(oldkey), keyToBytes(newkey)); + + } + + /** + * 将当前数据库的 key 移动到给定的数据库 db 当中。 + * 如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。 + * 因此,也可以利用这一特性,将 MOVE 当作锁(locking)原语(primitive)。 + */ + @Override + public Long move(Object key, int dbIndex) { + +// return jedisCluster.move(keyToBytes(key), dbIndex); + throw new JbootException("not support move commmand in redis cluster."); + + } + + /** + * 将 key 原子性地从当前实例传送到目标实例的指定数据库上,一旦传送成功, key 保证会出现在目标实例上,而当前实例上的 key 会被删除。 + */ + @Override + public String migrate(String host, int port, Object key, int destinationDb, int timeout) { + + throw new JbootException("not support migrate commmand in redis cluster."); + + } + + /** + * 切换到指定的数据库,数据库索引号 index 用数字值指定,以 0 作为起始索引值。 + * 默认使用 0 号数据库。 + * 注意:在 Jedis 对象被关闭时,数据库又会重新被设置为初始值,所以本方法 select(...) + * 正常工作需要使用如下方式之一: + * 1:使用 RedisInterceptor,在本线程内共享同一个 Jedis 对象 + * 2:使用 JbootRedis.call(ICallback) 进行操作 + * 3:自行获取 Jedis 对象进行操作 + */ + @Override + public String select(int databaseIndex) { + +// return jedisCluster.select(databaseIndex); + throw new IllegalStateException("Redis Cluster does not support multiple databases like the stand alone version of Redis, " + + "there is just database 0, and SELECT is not allowed."); + } + + /** + * 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。 + * 在 JbootRedis 中,带有生存时间的 key 被称为『易失的』(volatile)。 + */ + @Override + public Long expire(Object key, int seconds) { + + return jedisCluster.expire(keyToBytes(key), seconds); + + } + + /** + * EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 + */ + @Override + public Long expireAt(Object key, long unixTime) { + + return jedisCluster.expireAt(keyToBytes(key), unixTime); + + } + + /** + * 这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置 key 的生存时间,而不像 EXPIRE 命令那样,以秒为单位。 + */ + @Override + public Long pexpire(Object key, long milliseconds) { + + return jedisCluster.pexpire(keyToBytes(key), milliseconds); + + } + + /** + * 这个命令和 EXPIREAT 命令类似,但它以毫秒为单位设置 key 的过期 unix 时间戳,而不是像 EXPIREAT 那样,以秒为单位。 + */ + @Override + public Long pexpireAt(Object key, long millisecondsTimestamp) { + + return jedisCluster.pexpireAt(keyToBytes(key), millisecondsTimestamp); + + } + + /** + * 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 + * 当 key 存在但不是字符串类型时,返回一个错误。 + */ + @Override + @SuppressWarnings("unchecked") + public T getSet(Object key, Object value) { + + return (T) valueFromBytes(jedisCluster.getSet(keyToBytes(key), valueToBytes(value))); + + } + + /** + * 移除给定 key 的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )。 + */ + @Override + public Long persist(Object key) { + + return jedisCluster.persist(keyToBytes(key)); + + } + + /** + * 返回 key 所储存的值的类型。 + */ + @Override + public String type(Object key) { + + return jedisCluster.type(keyToBytes(key)); + + } + + /** + * 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。 + */ + @Override + public Long ttl(Object key) { + + return jedisCluster.ttl(keyToBytes(key)); + + } + + /** + * 这个命令类似于 TTL 命令,但它以毫秒为单位返回 key 的剩余生存时间,而不是像 TTL 命令那样,以秒为单位。 + */ + @Override + public Long pttl(Object key) { + + return jedisCluster.pttl(key.toString()); + + } + + /** + * 对象被引用的数量 + */ + @Override + public Long objectRefcount(Object key) { + +// return jedisCluster.objectRefcount(keyToBytes(key)); + throw new JbootException("not support move objectRefcount in redis cluster."); + } + + /** + * 对象没有被访问的空闲时间 + */ + @Override + public Long objectIdletime(Object key) { + +// return jedisCluster.objectIdletime(keyToBytes(key)); + throw new JbootException("not support move objectIdletime in redis cluster."); + + } + + /** + * 将哈希表 key 中的域 field 的值设为 value 。 + * 如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。 + * 如果域 field 已经存在于哈希表中,旧值将被覆盖。 + */ + @Override + public Long hset(Object key, Object field, Object value) { + + return jedisCluster.hset(keyToBytes(key), valueToBytes(field), valueToBytes(value)); + + } + + /** + * 同时将多个 field-value (域-值)对设置到哈希表 key 中。 + * 此命令会覆盖哈希表中已存在的域。 + * 如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。 + */ + @Override + public String hmset(Object key, Map hash) { + + Map para = new HashMap(); + for (Entry e : hash.entrySet()) + para.put(valueToBytes(e.getKey()), valueToBytes(e.getValue())); + return jedisCluster.hmset(keyToBytes(key), para); + + } + + /** + * 返回哈希表 key 中给定域 field 的值。 + */ + @Override + @SuppressWarnings("unchecked") + public T hget(Object key, Object field) { + + return (T) valueFromBytes(jedisCluster.hget(keyToBytes(key), valueToBytes(field))); + + } + + /** + * 返回哈希表 key 中,一个或多个给定域的值。 + * 如果给定的域不存在于哈希表,那么返回一个 nil 值。 + * 因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。 + */ + @Override + @SuppressWarnings("rawtypes") + public List hmget(Object key, Object... fields) { + + List data = jedisCluster.hmget(keyToBytes(key), valuesToBytesArray(fields)); + return valueListFromBytesList(data); + + } + + /** + * 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 + */ + @Override + public Long hdel(Object key, Object... fields) { + + return jedisCluster.hdel(keyToBytes(key), valuesToBytesArray(fields)); + + } + + /** + * 查看哈希表 key 中,给定域 field 是否存在。 + */ + @Override + public boolean hexists(Object key, Object field) { + + return jedisCluster.hexists(keyToBytes(key), valueToBytes(field)); + + } + + /** + * 返回哈希表 key 中,所有的域和值。 + * 在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。 + */ + @Override + @SuppressWarnings("rawtypes") + public Map hgetAll(Object key) { + + Map data = jedisCluster.hgetAll(keyToBytes(key)); + Map result = new HashMap(); + for (Entry e : data.entrySet()) + result.put(valueFromBytes(e.getKey()), valueFromBytes(e.getValue())); + return result; + + } + + /** + * 返回哈希表 key 中所有域的值。 + */ + @Override + @SuppressWarnings("rawtypes") + public List hvals(Object key) { + + Collection data = jedisCluster.hvals(keyToBytes(key)); + return valueListFromBytesList(data); + + } + + /** + * 返回哈希表 key 中的所有域。 + * 底层实现此方法取名为 hfields 更为合适,在此仅为与底层保持一致 + */ + @Override + public Set hkeys(Object key) { + + Set fieldSet = jedisCluster.hkeys(keyToBytes(key)); + Set result = new HashSet(); + fieldSetFromBytesSet(fieldSet, result); + return result; + + } + + /** + * 返回哈希表 key 中域的数量。 + */ + @Override + public Long hlen(Object key) { + + return jedisCluster.hlen(keyToBytes(key)); + + } + + /** + * 为哈希表 key 中的域 field 的值加上增量 increment 。 + * 增量也可以为负数,相当于对给定域进行减法操作。 + * 如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。 + * 如果域 field 不存在,那么在执行命令前,域的值被初始化为 0 。 + * 对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。 + * 本操作的值被限制在 64 位(bit)有符号数字表示之内。 + */ + @Override + public Long hincrBy(Object key, Object field, long value) { + + return jedisCluster.hincrBy(keyToBytes(key), valueToBytes(field), value); + + } + + /** + * 为哈希表 key 中的域 field 加上浮点数增量 increment 。 + * 如果哈希表中没有域 field ,那么 HINCRBYFLOAT 会先将域 field 的值设为 0 ,然后再执行加法操作。 + * 如果键 key 不存在,那么 HINCRBYFLOAT 会先创建一个哈希表,再创建域 field ,最后再执行加法操作。 + * 当以下任意一个条件发生时,返回一个错误: + * 1:域 field 的值不是字符串类型(因为 redis 中的数字和浮点数都以字符串的形式保存,所以它们都属于字符串类型) + * 2:域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number) + * HINCRBYFLOAT 命令的详细功能和 INCRBYFLOAT 命令类似,请查看 INCRBYFLOAT 命令获取更多相关信息。 + */ + @Override + public Double hincrByFloat(Object key, Object field, double value) { + + return jedisCluster.hincrByFloat(keyToBytes(key), valueToBytes(field), value); + + } + + /** + * 返回列表 key 中,下标为 index 的元素。 + * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 + * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 + * 如果 key 不是列表类型,返回一个错误。 + */ + @Override + @SuppressWarnings("unchecked") + + /** + * 返回列表 key 中,下标为 index 的元素。 + * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素, + * 以 1 表示列表的第二个元素,以此类推。 + * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 + * 如果 key 不是列表类型,返回一个错误。 + */ + public T lindex(Object key, long index) { + + return (T) valueFromBytes(jedisCluster.lindex(keyToBytes(key), index)); + + } + + + /** + * 返回列表 key 的长度。 + * 如果 key 不存在,则 key 被解释为一个空列表,返回 0 . + * 如果 key 不是列表类型,返回一个错误。 + */ + @Override + public Long llen(Object key) { + + return jedisCluster.llen(keyToBytes(key)); + + } + + /** + * 移除并返回列表 key 的头元素。 + */ + @Override + @SuppressWarnings("unchecked") + public T lpop(Object key) { + + return (T) valueFromBytes(jedisCluster.lpop(keyToBytes(key))); + + } + + /** + * 将一个或多个值 value 插入到列表 key 的表头 + * 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头: 比如说, + * 对空列表 mylist 执行命令 LPUSH mylist a b c ,列表的值将是 c b a , + * 这等同于原子性地执行 LPUSH mylist a 、 LPUSH mylist b 和 LPUSH mylist c 三个命令。 + * 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。 + * 当 key 存在但不是列表类型时,返回一个错误。 + */ + @Override + public Long lpush(Object key, Object... values) { + + return jedisCluster.lpush(keyToBytes(key), valuesToBytesArray(values)); + + } + + /** + * 将列表 key 下标为 index 的元素的值设置为 value 。 + * 当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。 + * 关于列表下标的更多信息,请参考 LINDEX 命令。 + */ + @Override + public String lset(Object key, long index, Object value) { + + return jedisCluster.lset(keyToBytes(key), index, valueToBytes(value)); + + } + + /** + * 根据参数 count 的值,移除列表中与参数 value 相等的元素。 + * count 的值可以是以下几种: + * count 大于 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。 + * count 小于 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。 + * count 等于 0 : 移除表中所有与 value 相等的值。 + */ + @Override + public Long lrem(Object key, long count, Object value) { + + return jedisCluster.lrem(keyToBytes(key), count, valueToBytes(value)); + + } + + /** + * 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。 + * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 + * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 + *
+     * 例子:
+     * 获取 list 中所有数据:cache.lrange(listKey, 0, -1);
+     * 获取 list 中下标 1 到 3 的数据: cache.lrange(listKey, 1, 3);
+     * 
+ */ + @Override + @SuppressWarnings("rawtypes") + public List lrange(Object key, long start, long end) { + + List data = jedisCluster.lrange(keyToBytes(key), start, end); + if (data != null) { + return valueListFromBytesList(data); + } else { + return new ArrayList(0); + } + + } + + /** + * 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 + * 举个例子,执行命令 LTRIM list 0 2 ,表示只保留列表 list 的前三个元素,其余元素全部删除。 + * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 + * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 + * 当 key 不是列表类型时,返回一个错误。 + */ + @Override + public String ltrim(Object key, long start, long end) { + + return jedisCluster.ltrim(keyToBytes(key), start, end); + + } + + /** + * 移除并返回列表 key 的尾元素。 + */ + @Override + @SuppressWarnings("unchecked") + public T rpop(Object key) { + + return (T) valueFromBytes(jedisCluster.rpop(keyToBytes(key))); + + } + + /** + * 命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作: + * 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。 + * 将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。 + */ + @Override + @SuppressWarnings("unchecked") + public T rpoplpush(Object srcKey, Object dstKey) { + + return (T) valueFromBytes(jedisCluster.rpoplpush(keyToBytes(srcKey), keyToBytes(dstKey))); + + } + + /** + * 将一个或多个值 value 插入到列表 key 的表尾(最右边)。 + * 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表尾:比如 + * 对一个空列表 mylist 执行 RPUSH mylist a b c ,得出的结果列表为 a b c , + * 等同于执行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。 + * 如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。 + * 当 key 存在但不是列表类型时,返回一个错误。 + */ + @Override + public Long rpush(Object key, Object... values) { + + return jedisCluster.rpush(keyToBytes(key), valuesToBytesArray(values)); + + } + + /** + * BLPOP 是列表的阻塞式(blocking)弹出原语。 + * 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 + * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。 + */ + @Override + @SuppressWarnings("rawtypes") + public List blpop(Object... keys) { +// String[] keysStrings = new String[keys.length]; +// for (int i = 0; i < keys.length; i++) { +// keysStrings[i] = keys[i].toString(); +// } + + List data = jedisCluster.blpop(timeout, keysToBytesArray(keys)); + + if (data != null && data.size() == 2) { + List objects = new ArrayList<>(); + objects.add(new String(data.get(0))); + objects.add(valueFromBytes(data.get(1))); + return objects; + } + + return valueListFromBytesList(data); + + } + + /** + * BLPOP 是列表的阻塞式(blocking)弹出原语。 + * 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 + * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。 + */ + @Override + @SuppressWarnings("rawtypes") + public List blpop(Integer timeout, Object... keys) { + + List data = jedisCluster.blpop(timeout, keysToBytesArray(keys)); + return valueListFromBytesList(data); + + } + + /** + * BRPOP 是列表的阻塞式(blocking)弹出原语。 + * 它是 RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 + * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。 + * 关于阻塞操作的更多信息,请查看 BLPOP 命令, BRPOP 除了弹出元素的位置和 BLPOP 不同之外,其他表现一致。 + */ + @Override + @SuppressWarnings("rawtypes") + public List brpop(Object... keys) { + + List data = jedisCluster.brpop(timeout, keysToBytesArray(keys)); + return valueListFromBytesList(data); + + } + + /** + * BRPOP 是列表的阻塞式(blocking)弹出原语。 + * 它是 RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 + * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。 + * 关于阻塞操作的更多信息,请查看 BLPOP 命令, BRPOP 除了弹出元素的位置和 BLPOP 不同之外,其他表现一致。 + */ + @Override + @SuppressWarnings("rawtypes") + public List brpop(Integer timeout, Object... keys) { + + List data = jedisCluster.brpop(timeout, keysToBytesArray(keys)); + return valueListFromBytesList(data); + + } + + /** + * 使用客户端向 JbootRedis 服务器发送一个 PING ,如果服务器运作正常的话,会返回一个 PONG 。 + * 通常用于测试与服务器的连接是否仍然生效,或者用于测量延迟值。 + */ + @Override + public String ping() { +// jedisCluster.getClusterNodes().get("aa").getResource().ping +// return jedisCluster..ping(); + + Map nodes = jedisCluster.getClusterNodes(); + if (nodes != null) { + for (JedisPool pool : nodes.values()) { + try (Jedis node = pool.getResource()) { + String ret = node.ping(); + if (ret != null) { + return ret; + } + } + } + } + return null; + } + + /** + * 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。 + * 假如 key 不存在,则创建一个只包含 member 元素作成员的集合。 + * 当 key 不是集合类型时,返回一个错误。 + */ + @Override + public Long sadd(Object key, Object... members) { + + return jedisCluster.sadd(keyToBytes(key), valuesToBytesArray(members)); + + } + + /** + * 返回集合 key 的基数(集合中元素的数量)。 + */ + @Override + public Long scard(Object key) { + + return jedisCluster.scard(keyToBytes(key)); + + } + + /** + * 移除并返回集合中的一个随机元素。 + * 如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用 SRANDMEMBER 命令。 + */ + @Override + @SuppressWarnings("unchecked") + public T spop(Object key) { + + return (T) valueFromBytes(jedisCluster.spop(keyToBytes(key))); + + } + + /** + * 返回集合 key 中的所有成员。 + * 不存在的 key 被视为空集合。 + */ + @Override + @SuppressWarnings("rawtypes") + public Set smembers(Object key) { + + Set data = jedisCluster.smembers(keyToBytes(key)); + Set result = new HashSet(); + valueSetFromBytesSet(data, result); + return result; + + } + + /** + * 判断 member 元素是否集合 key 的成员。 + */ + @Override + public boolean sismember(Object key, Object member) { + + return jedisCluster.sismember(keyToBytes(key), valueToBytes(member)); + + } + + /** + * 返回多个集合的交集,多个集合由 keys 指定 + */ + @Override + @SuppressWarnings("rawtypes") + public Set sinter(Object... keys) { + + Set data = jedisCluster.sinter(keysToBytesArray(keys)); + Set result = new HashSet(); + valueSetFromBytesSet(data, result); + return result; + + } + + /** + * 返回集合中的一个随机元素。 + */ + @Override + @SuppressWarnings("unchecked") + public T srandmember(Object key) { + + return (T) valueFromBytes(jedisCluster.srandmember(keyToBytes(key))); + + } + + /** + * 返回集合中的 count 个随机元素。 + * 从 JbootRedis 2.6 版本开始, SRANDMEMBER 命令接受可选的 count 参数: + * 如果 count 为正数,且小于集合基数,那么命令返回一个包含 count 个元素的数组,数组中的元素各不相同。 + * 如果 count 大于等于集合基数,那么返回整个集合。 + * 如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。 + * 该操作和 SPOP 相似,但 SPOP 将随机元素从集合中移除并返回,而 SRANDMEMBER 则仅仅返回随机元素,而不对集合进行任何改动。 + */ + @Override + @SuppressWarnings("rawtypes") + public List srandmember(Object key, int count) { + + List data = jedisCluster.srandmember(keyToBytes(key), count); + return valueListFromBytesList(data); + + } + + /** + * 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。 + */ + @Override + public Long srem(Object key, Object... members) { + + return jedisCluster.srem(keyToBytes(key), valuesToBytesArray(members)); + + } + + /** + * 返回多个集合的并集,多个集合由 keys 指定 + * 不存在的 key 被视为空集。 + */ + @Override + @SuppressWarnings("rawtypes") + public Set sunion(Object... keys) { + + Set data = jedisCluster.sunion(keysToBytesArray(keys)); + Set result = new HashSet(); + valueSetFromBytesSet(data, result); + return result; + + } + + /** + * 返回一个集合的全部成员,该集合是所有给定集合之间的差集。 + * 不存在的 key 被视为空集。 + */ + @Override + @SuppressWarnings("rawtypes") + public Set sdiff(Object... keys) { + + Set data = jedisCluster.sdiff(keysToBytesArray(keys)); + Set result = new HashSet(); + valueSetFromBytesSet(data, result); + return result; + + } + + /** + * 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。 + * 如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值, + * 并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。 + */ + @Override + public Long zadd(Object key, double score, Object member) { + + return jedisCluster.zadd(keyToBytes(key), score, valueToBytes(member)); + + } + + @Override + public Long zadd(Object key, Map scoreMembers) { + + Map para = new HashMap(); + for (Entry e : scoreMembers.entrySet()) + para.put(valueToBytes(e.getKey()), e.getValue()); // valueToBytes is important + return jedisCluster.zadd(keyToBytes(key), para); + + } + + /** + * 返回有序集 key 的基数。 + */ + @Override + public Long zcard(Object key) { + + return jedisCluster.zcard(keyToBytes(key)); + + } + + /** + * 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。 + * 关于参数 min 和 max 的详细使用方法,请参考 ZRANGEBYSCORE 命令。 + */ + @Override + public Long zcount(Object key, double min, double max) { + + return jedisCluster.zcount(keyToBytes(key), min, max); + + } + + /** + * 为有序集 key 的成员 member 的 score 值加上增量 increment 。 + */ + @Override + public Double zincrby(Object key, double score, Object member) { + + return jedisCluster.zincrby(keyToBytes(key), score, valueToBytes(member)); + + } + + /** + * 返回有序集 key 中,指定区间内的成员。 + * 其中成员的位置按 score 值递增(从小到大)来排序。 + * 具有相同 score 值的成员按字典序(lexicographical order )来排列。 + * 如果你需要成员按 score 值递减(从大到小)来排列,请使用 ZREVRANGE 命令。 + */ + @Override + @SuppressWarnings("rawtypes") + public Set zrange(Object key, long start, long end) { + + Set data = jedisCluster.zrange(keyToBytes(key), start, end); + Set result = new LinkedHashSet(); // 有序集合必须 LinkedHashSet + valueSetFromBytesSet(data, result); + return result; + + } + + /** + * 返回有序集 key 中,指定区间内的成员。 + * 其中成员的位置按 score 值递减(从大到小)来排列。 + * 具有相同 score 值的成员按字典序的逆序(reverse lexicographical order)排列。 + * 除了成员按 score 值递减的次序排列这一点外, ZREVRANGE 命令的其他方面和 ZRANGE 命令一样。 + */ + @Override + @SuppressWarnings("rawtypes") + public Set zrevrange(Object key, long start, long end) { + + Set data = jedisCluster.zrevrange(keyToBytes(key), start, end); + Set result = new LinkedHashSet(); // 有序集合必须 LinkedHashSet + valueSetFromBytesSet(data, result); + return result; + + } + + /** + * 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。 + * 有序集成员按 score 值递增(从小到大)次序排列。 + */ + @Override + @SuppressWarnings("rawtypes") + public Set zrangeByScore(Object key, double min, double max) { + + Set data = jedisCluster.zrangeByScore(keyToBytes(key), min, max); + Set result = new LinkedHashSet(); // 有序集合必须 LinkedHashSet + valueSetFromBytesSet(data, result); + return result; + + } + + /** + * 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。 + * 排名以 0 为底,也就是说, score 值最小的成员排名为 0 。 + * 使用 ZREVRANK 命令可以获得成员按 score 值递减(从大到小)排列的排名。 + */ + @Override + public Long zrank(Object key, Object member) { + + return jedisCluster.zrank(keyToBytes(key), valueToBytes(member)); + + } + + /** + * 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递减(从大到小)排序。 + * 排名以 0 为底,也就是说, score 值最大的成员排名为 0 。 + * 使用 ZRANK 命令可以获得成员按 score 值递增(从小到大)排列的排名。 + */ + @Override + public Long zrevrank(Object key, Object member) { + + return jedisCluster.zrevrank(keyToBytes(key), valueToBytes(member)); + + } + + /** + * 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。 + * 当 key 存在但不是有序集类型时,返回一个错误。 + */ + @Override + public Long zrem(Object key, Object... members) { + + return jedisCluster.zrem(keyToBytes(key), valuesToBytesArray(members)); + + } + + /** + * 返回有序集 key 中,成员 member 的 score 值。 + * 如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。 + */ + @Override + public Double zscore(Object key, Object member) { + + return jedisCluster.zscore(keyToBytes(key), valueToBytes(member)); + + } + + /** + * 发布 + * + * @param channel + * @param message + */ + @Override + public void publish(String channel, String message) { + + jedisCluster.publish(channel, message); + + } + + /** + * 发布 + * + * @param channel + * @param message + */ + @Override + public void publish(byte[] channel, byte[] message) { + jedisCluster.publish(channel, message); + } + + + /** + * 订阅 + * + * @param listener + * @param channels + */ + @Override + public void subscribe(JedisPubSub listener, final String... channels) { + /** + * https://github.com/xetorthio/jedis/wiki/AdvancedUsage + * Note that subscribe is a blocking operation because it will poll JbootRedis for responses on the thread that calls subscribe. + * A single JedisPubSub instance can be used to subscribe to multiple channels. + * You can call subscribe or psubscribe on an existing JedisPubSub instance to change your subscriptions. + */ + new Thread("jboot-redisCluster-subscribe-JedisPubSub") { + @Override + public void run() { + while (true) { + //订阅线程断开连接,需要进行重连 + try { + jedisCluster.subscribe(listener, channels); + LOG.warn("Disconnect to redis channel in subscribe JedisPubSub!"); + break; + } catch (JedisConnectionException e) { + LOG.error("failed connect to redis, reconnect it.", e); + QuietlyUtil.sleepQuietly(1000); + } + } + } + }.start(); + } + + /** + * 订阅 + * + * @param binaryListener + * @param channels + */ + @Override + public void subscribe(BinaryJedisPubSub binaryListener, final byte[]... channels) { + /** + * https://github.com/xetorthio/jedis/wiki/AdvancedUsage + * Note that subscribe is a blocking operation because it will poll JbootRedis for responses on the thread that calls subscribe. + * A single JedisPubSub instance can be used to subscribe to multiple channels. + * You can call subscribe or psubscribe on an existing JedisPubSub instance to change your subscriptions. + */ + new Thread("jboot-redisCluster-subscribe-BinaryJedisPubSub") { + @Override + public void run() { + while (!isClose()) { + //订阅线程断开连接,需要进行重连 + try { + jedisCluster.subscribe(binaryListener, channels); + LOG.warn("Disconnect to redis channel in subscribe BinaryJedisPubSub!"); + break; + } catch (Throwable e) { + LOG.error("failed connect to redis, reconnect it.", e); + QuietlyUtil.sleepQuietly(1000); + } + } + } + }.start(); + } + + + @Override + public RedisScanResult scan(String pattern, String cursor, int scanCount) { + ScanParams params = new ScanParams(); + params.match(pattern).count(scanCount); + ScanResult scanResult = jedisCluster.scan(cursor, params); + return new RedisScanResult<>(scanResult.getCursor(), scanResult.getResult()); + } + + @Override + public Object eval(String script, int keyCount, String... params) { + return jedisCluster.eval(script, keyCount, params); + } + + public JedisCluster getJedisCluster() { + return jedisCluster; + } + +} + + + + + + diff --git a/src/main/java/io/jboot/support/redis/jedis/JbootJedisImpl.java b/src/main/java/io/jboot/support/redis/jedis/JbootJedisImpl.java index ed379a1ff0cc80fbc59406384a7fe4350619769d..6b58582a549bfd7b4fd5132686dd376eb5a4b6ce 100644 --- a/src/main/java/io/jboot/support/redis/jedis/JbootJedisImpl.java +++ b/src/main/java/io/jboot/support/redis/jedis/JbootJedisImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,12 @@ package io.jboot.support.redis.jedis; import com.jfinal.log.Log; -import io.jboot.utils.StrUtil; +import io.jboot.exception.JbootIllegalConfigException; import io.jboot.support.redis.JbootRedisBase; import io.jboot.support.redis.JbootRedisConfig; -import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.support.redis.RedisScanResult; +import io.jboot.utils.QuietlyUtil; +import io.jboot.utils.StrUtil; import redis.clients.jedis.*; import redis.clients.jedis.exceptions.JedisConnectionException; @@ -34,6 +36,7 @@ public class JbootJedisImpl extends JbootRedisBase { protected JedisPool jedisPool; protected JbootRedisConfig config; + private static final Log LOG = Log.getLog(JbootJedisImpl.class); public JbootJedisImpl(JbootRedisConfig config) { @@ -99,28 +102,9 @@ public class JbootJedisImpl extends JbootRedisBase { poolConfig.setMaxWaitMillis(config.getMaxWaitMillis()); } - this.jedisPool = newJedisPool(poolConfig, host, port, timeout, password, database, clientName); - + this.jedisPool = new JedisPool(poolConfig, host, port, timeout, timeout, password, database, clientName); } - public static JedisPool newJedisPool(JedisPoolConfig jedisPoolConfig, String host, Integer port, Integer timeout, String password, Integer database, String clientName) { - JedisPool jedisPool; - if (port != null && timeout != null && password != null && database != null && clientName != null) - jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password, database, clientName); - else if (port != null && timeout != null && password != null && database != null) - jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password, database); - else if (port != null && timeout != null && password != null) - jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password); - else if (port != null && timeout != null) - jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout); - else if (port != null) - jedisPool = new JedisPool(jedisPoolConfig, host, port); - else - jedisPool = new JedisPool(jedisPoolConfig, host); - - - return jedisPool; - } public JbootJedisImpl(JedisPool jedisPool) { super(null); @@ -132,6 +116,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。 * 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。 */ + @Override public String set(Object key, Object value) { Jedis jedis = getJedis(); try { @@ -157,6 +142,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。 * 此方法用了修改 incr 等的值 */ + @Override public String setWithoutSerialize(Object key, Object value) { Jedis jedis = getJedis(); try { @@ -171,6 +157,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 存放 key value 对到 redis,并将 key 的生存时间设为 seconds (以秒为单位)。 * 如果 key 已经存在, SETEX 命令将覆写旧值。 */ + @Override public String setex(Object key, int seconds, Object value) { Jedis jedis = getJedis(); try { @@ -184,6 +171,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回 key 所关联的 value 值 * 如果 key 不存在那么返回特殊值 nil 。 */ + @Override @SuppressWarnings("unchecked") public T get(Object key) { Jedis jedis = getJedis(); @@ -213,6 +201,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 删除给定的一个 key * 不存在的 key 会被忽略。 */ + @Override public Long del(Object key) { Jedis jedis = getJedis(); try { @@ -226,7 +215,11 @@ public class JbootJedisImpl extends JbootRedisBase { * 删除给定的多个 key * 不存在的 key 会被忽略。 */ + @Override public Long del(Object... keys) { + if (keys == null || keys.length == 0) { + return 0L; + } Jedis jedis = getJedis(); try { return jedis.del(keysToBytesArray(keys)); @@ -243,6 +236,7 @@ public class JbootJedisImpl extends JbootRedisBase { * KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。 * 特殊符号用 \ 隔开 */ + @Override public Set keys(String pattern) { Jedis jedis = getJedis(); try { @@ -263,17 +257,20 @@ public class JbootJedisImpl extends JbootRedisBase { * List list = cache.mget("k1", "k2"); // 利用多个键值得到上面代码放入的值 * */ + @Override public String mset(Object... keysValues) { - if (keysValues.length % 2 != 0) + if (keysValues.length % 2 != 0) { throw new IllegalArgumentException("wrong number of arguments for met, keysValues length can not be odd"); + } Jedis jedis = getJedis(); try { byte[][] kv = new byte[keysValues.length][]; for (int i = 0; i < keysValues.length; i++) { - if (i % 2 == 0) + if (i % 2 == 0) { kv[i] = keyToBytes(keysValues[i]); - else + } else { kv[i] = valueToBytes(keysValues[i]); + } } return jedis.mset(kv); } finally { @@ -285,6 +282,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回所有(一个或多个)给定 key 的值。 * 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。 */ + @Override @SuppressWarnings("rawtypes") public List mget(Object... keys) { Jedis jedis = getJedis(); @@ -304,6 +302,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 本操作的值限制在 64 位(bit)有符号数字表示之内。 * 关于递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 */ + @Override public Long decr(Object key) { Jedis jedis = getJedis(); try { @@ -320,6 +319,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 本操作的值限制在 64 位(bit)有符号数字表示之内。 * 关于更多递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 */ + @Override public Long decrBy(Object key, long longValue) { Jedis jedis = getJedis(); try { @@ -335,6 +335,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 * 本操作的值限制在 64 位(bit)有符号数字表示之内。 */ + @Override public Long incr(Object key) { Jedis jedis = getJedis(); try { @@ -351,6 +352,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 本操作的值限制在 64 位(bit)有符号数字表示之内。 * 关于递增(increment) / 递减(decrement)操作的更多信息,参见 INCR 命令。 */ + @Override public Long incrBy(Object key, long longValue) { Jedis jedis = getJedis(); try { @@ -363,6 +365,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 检查给定 key 是否存在。 */ + @Override public boolean exists(Object key) { Jedis jedis = getJedis(); try { @@ -375,6 +378,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 从当前数据库中随机返回(不删除)一个 key 。 */ + @Override public String randomKey() { Jedis jedis = getJedis(); try { @@ -389,6 +393,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。 * 当 newkey 已经存在时, RENAME 命令将覆盖旧值。 */ + @Override public String rename(Object oldkey, Object newkey) { Jedis jedis = getJedis(); try { @@ -403,6 +408,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。 * 因此,也可以利用这一特性,将 MOVE 当作锁(locking)原语(primitive)。 */ + @Override public Long move(Object key, int dbIndex) { Jedis jedis = getJedis(); try { @@ -415,6 +421,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 将 key 原子性地从当前实例传送到目标实例的指定数据库上,一旦传送成功, key 保证会出现在目标实例上,而当前实例上的 key 会被删除。 */ + @Override public String migrate(String host, int port, Object key, int destinationDb, int timeout) { Jedis jedis = getJedis(); try { @@ -433,6 +440,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 2:使用 JbootRedis.call(ICallback) 进行操作 * 3:自行获取 Jedis 对象进行操作 */ + @Override public String select(int databaseIndex) { Jedis jedis = getJedis(); try { @@ -446,6 +454,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。 * 在 JbootRedis 中,带有生存时间的 key 被称为『易失的』(volatile)。 */ + @Override public Long expire(Object key, int seconds) { Jedis jedis = getJedis(); try { @@ -458,6 +467,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 */ + @Override public Long expireAt(Object key, long unixTime) { Jedis jedis = getJedis(); try { @@ -470,6 +480,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置 key 的生存时间,而不像 EXPIRE 命令那样,以秒为单位。 */ + @Override public Long pexpire(Object key, long milliseconds) { Jedis jedis = getJedis(); try { @@ -482,6 +493,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 这个命令和 EXPIREAT 命令类似,但它以毫秒为单位设置 key 的过期 unix 时间戳,而不是像 EXPIREAT 那样,以秒为单位。 */ + @Override public Long pexpireAt(Object key, long millisecondsTimestamp) { Jedis jedis = getJedis(); try { @@ -495,6 +507,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 * 当 key 存在但不是字符串类型时,返回一个错误。 */ + @Override @SuppressWarnings("unchecked") public T getSet(Object key, Object value) { Jedis jedis = getJedis(); @@ -508,6 +521,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 移除给定 key 的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )。 */ + @Override public Long persist(Object key) { Jedis jedis = getJedis(); try { @@ -520,6 +534,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 返回 key 所储存的值的类型。 */ + @Override public String type(Object key) { Jedis jedis = getJedis(); try { @@ -532,6 +547,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。 */ + @Override public Long ttl(Object key) { Jedis jedis = getJedis(); try { @@ -544,6 +560,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 这个命令类似于 TTL 命令,但它以毫秒为单位返回 key 的剩余生存时间,而不是像 TTL 命令那样,以秒为单位。 */ + @Override public Long pttl(Object key) { Jedis jedis = getJedis(); try { @@ -556,6 +573,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 对象被引用的数量 */ + @Override public Long objectRefcount(Object key) { Jedis jedis = getJedis(); try { @@ -568,6 +586,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 对象没有被访问的空闲时间 */ + @Override public Long objectIdletime(Object key) { Jedis jedis = getJedis(); try { @@ -582,6 +601,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。 * 如果域 field 已经存在于哈希表中,旧值将被覆盖。 */ + @Override public Long hset(Object key, Object field, Object value) { Jedis jedis = getJedis(); try { @@ -596,6 +616,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 此命令会覆盖哈希表中已存在的域。 * 如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。 */ + @Override public String hmset(Object key, Map hash) { Jedis jedis = getJedis(); try { @@ -611,6 +632,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 返回哈希表 key 中给定域 field 的值。 */ + @Override @SuppressWarnings("unchecked") public T hget(Object key, Object field) { Jedis jedis = getJedis(); @@ -626,6 +648,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果给定的域不存在于哈希表,那么返回一个 nil 值。 * 因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。 */ + @Override @SuppressWarnings("rawtypes") public List hmget(Object key, Object... fields) { Jedis jedis = getJedis(); @@ -640,6 +663,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 */ + @Override public Long hdel(Object key, Object... fields) { Jedis jedis = getJedis(); try { @@ -652,6 +676,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 查看哈希表 key 中,给定域 field 是否存在。 */ + @Override public boolean hexists(Object key, Object field) { Jedis jedis = getJedis(); try { @@ -665,6 +690,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回哈希表 key 中,所有的域和值。 * 在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。 */ + @Override @SuppressWarnings("rawtypes") public Map hgetAll(Object key) { Jedis jedis = getJedis(); @@ -682,6 +708,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 返回哈希表 key 中所有域的值。 */ + @Override @SuppressWarnings("rawtypes") public List hvals(Object key) { Jedis jedis = getJedis(); @@ -697,6 +724,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回哈希表 key 中的所有域。 * 底层实现此方法取名为 hfields 更为合适,在此仅为与底层保持一致 */ + @Override public Set hkeys(Object key) { Jedis jedis = getJedis(); try { @@ -712,6 +740,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 返回哈希表 key 中域的数量。 */ + @Override public Long hlen(Object key) { Jedis jedis = getJedis(); try { @@ -729,6 +758,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。 * 本操作的值被限制在 64 位(bit)有符号数字表示之内。 */ + @Override public Long hincrBy(Object key, Object field, long value) { Jedis jedis = getJedis(); try { @@ -747,6 +777,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 2:域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number) * HINCRBYFLOAT 命令的详细功能和 INCRBYFLOAT 命令类似,请查看 INCRBYFLOAT 命令获取更多相关信息。 */ + @Override public Double hincrByFloat(Object key, Object field, double value) { Jedis jedis = getJedis(); try { @@ -771,6 +802,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 * 如果 key 不是列表类型,返回一个错误。 */ + @Override public T lindex(Object key, long index) { Jedis jedis = getJedis(); try { @@ -786,6 +818,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果 key 不存在,则 key 被解释为一个空列表,返回 0 . * 如果 key 不是列表类型,返回一个错误。 */ + @Override public Long llen(Object key) { Jedis jedis = getJedis(); try { @@ -798,6 +831,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 移除并返回列表 key 的头元素。 */ + @Override @SuppressWarnings("unchecked") public T lpop(Object key) { Jedis jedis = getJedis(); @@ -816,6 +850,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。 * 当 key 存在但不是列表类型时,返回一个错误。 */ + @Override public Long lpush(Object key, Object... values) { Jedis jedis = getJedis(); try { @@ -830,6 +865,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。 * 关于列表下标的更多信息,请参考 LINDEX 命令。 */ + @Override public String lset(Object key, long index, Object value) { Jedis jedis = getJedis(); try { @@ -846,6 +882,7 @@ public class JbootJedisImpl extends JbootRedisBase { * count 小于 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。 * count 等于 0 : 移除表中所有与 value 相等的值。 */ + @Override public Long lrem(Object key, long count, Object value) { Jedis jedis = getJedis(); try { @@ -865,6 +902,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 获取 list 中下标 1 到 3 的数据: cache.lrange(listKey, 1, 3); * */ + @Override @SuppressWarnings("rawtypes") public List lrange(Object key, long start, long end) { Jedis jedis = getJedis(); @@ -887,6 +925,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 * 当 key 不是列表类型时,返回一个错误。 */ + @Override public String ltrim(Object key, long start, long end) { Jedis jedis = getJedis(); try { @@ -899,6 +938,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 移除并返回列表 key 的尾元素。 */ + @Override @SuppressWarnings("unchecked") public T rpop(Object key) { Jedis jedis = getJedis(); @@ -914,6 +954,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。 * 将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。 */ + @Override @SuppressWarnings("unchecked") public T rpoplpush(Object srcKey, Object dstKey) { Jedis jedis = getJedis(); @@ -932,6 +973,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。 * 当 key 存在但不是列表类型时,返回一个错误。 */ + @Override public Long rpush(Object key, Object... values) { Jedis jedis = getJedis(); try { @@ -946,6 +988,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。 */ + @Override @SuppressWarnings("rawtypes") public List blpop(Object... keys) { Jedis jedis = getJedis(); @@ -962,6 +1005,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。 * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。 */ + @Override @SuppressWarnings("rawtypes") public List blpop(Integer timeout, Object... keys) { Jedis jedis = getJedis(); @@ -990,6 +1034,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。 * 关于阻塞操作的更多信息,请查看 BLPOP 命令, BRPOP 除了弹出元素的位置和 BLPOP 不同之外,其他表现一致。 */ + @Override @SuppressWarnings("rawtypes") public List brpop(Object... keys) { Jedis jedis = getJedis(); @@ -1007,6 +1052,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。 * 关于阻塞操作的更多信息,请查看 BLPOP 命令, BRPOP 除了弹出元素的位置和 BLPOP 不同之外,其他表现一致。 */ + @Override @SuppressWarnings("rawtypes") public List brpop(Integer timeout, Object... keys) { Jedis jedis = getJedis(); @@ -1022,6 +1068,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 使用客户端向 JbootRedis 服务器发送一个 PING ,如果服务器运作正常的话,会返回一个 PONG 。 * 通常用于测试与服务器的连接是否仍然生效,或者用于测量延迟值。 */ + @Override public String ping() { Jedis jedis = getJedis(); try { @@ -1036,6 +1083,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 假如 key 不存在,则创建一个只包含 member 元素作成员的集合。 * 当 key 不是集合类型时,返回一个错误。 */ + @Override public Long sadd(Object key, Object... members) { Jedis jedis = getJedis(); try { @@ -1048,6 +1096,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 返回集合 key 的基数(集合中元素的数量)。 */ + @Override public Long scard(Object key) { Jedis jedis = getJedis(); try { @@ -1061,6 +1110,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 移除并返回集合中的一个随机元素。 * 如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用 SRANDMEMBER 命令。 */ + @Override @SuppressWarnings("unchecked") public T spop(Object key) { Jedis jedis = getJedis(); @@ -1075,6 +1125,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回集合 key 中的所有成员。 * 不存在的 key 被视为空集合。 */ + @Override @SuppressWarnings("rawtypes") public Set smembers(Object key) { Jedis jedis = getJedis(); @@ -1091,6 +1142,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 判断 member 元素是否集合 key 的成员。 */ + @Override public boolean sismember(Object key, Object member) { Jedis jedis = getJedis(); try { @@ -1103,6 +1155,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 返回多个集合的交集,多个集合由 keys 指定 */ + @Override @SuppressWarnings("rawtypes") public Set sinter(Object... keys) { Jedis jedis = getJedis(); @@ -1119,6 +1172,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 返回集合中的一个随机元素。 */ + @Override @SuppressWarnings("unchecked") public T srandmember(Object key) { Jedis jedis = getJedis(); @@ -1137,6 +1191,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。 * 该操作和 SPOP 相似,但 SPOP 将随机元素从集合中移除并返回,而 SRANDMEMBER 则仅仅返回随机元素,而不对集合进行任何改动。 */ + @Override @SuppressWarnings("rawtypes") public List srandmember(Object key, int count) { Jedis jedis = getJedis(); @@ -1151,6 +1206,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。 */ + @Override public Long srem(Object key, Object... members) { Jedis jedis = getJedis(); try { @@ -1164,6 +1220,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回多个集合的并集,多个集合由 keys 指定 * 不存在的 key 被视为空集。 */ + @Override @SuppressWarnings("rawtypes") public Set sunion(Object... keys) { Jedis jedis = getJedis(); @@ -1181,6 +1238,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回一个集合的全部成员,该集合是所有给定集合之间的差集。 * 不存在的 key 被视为空集。 */ + @Override @SuppressWarnings("rawtypes") public Set sdiff(Object... keys) { Jedis jedis = getJedis(); @@ -1199,6 +1257,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值, * 并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。 */ + @Override public Long zadd(Object key, double score, Object member) { Jedis jedis = getJedis(); try { @@ -1208,12 +1267,14 @@ public class JbootJedisImpl extends JbootRedisBase { } } + @Override public Long zadd(Object key, Map scoreMembers) { Jedis jedis = getJedis(); try { - Map para = new HashMap(); - for (Entry e : scoreMembers.entrySet()) + Map para = new HashMap<>(); + for (Entry e : scoreMembers.entrySet()) { para.put(valueToBytes(e.getKey()), e.getValue()); // valueToBytes is important + } return jedis.zadd(keyToBytes(key), para); } finally { returnResource(jedis); @@ -1223,6 +1284,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 返回有序集 key 的基数。 */ + @Override public Long zcard(Object key) { Jedis jedis = getJedis(); try { @@ -1236,6 +1298,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。 * 关于参数 min 和 max 的详细使用方法,请参考 ZRANGEBYSCORE 命令。 */ + @Override public Long zcount(Object key, double min, double max) { Jedis jedis = getJedis(); try { @@ -1248,6 +1311,7 @@ public class JbootJedisImpl extends JbootRedisBase { /** * 为有序集 key 的成员 member 的 score 值加上增量 increment 。 */ + @Override public Double zincrby(Object key, double score, Object member) { Jedis jedis = getJedis(); try { @@ -1263,6 +1327,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 具有相同 score 值的成员按字典序(lexicographical order )来排列。 * 如果你需要成员按 score 值递减(从大到小)来排列,请使用 ZREVRANGE 命令。 */ + @Override @SuppressWarnings("rawtypes") public Set zrange(Object key, long start, long end) { Jedis jedis = getJedis(); @@ -1282,6 +1347,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 具有相同 score 值的成员按字典序的逆序(reverse lexicographical order)排列。 * 除了成员按 score 值递减的次序排列这一点外, ZREVRANGE 命令的其他方面和 ZRANGE 命令一样。 */ + @Override @SuppressWarnings("rawtypes") public Set zrevrange(Object key, long start, long end) { Jedis jedis = getJedis(); @@ -1299,6 +1365,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。 * 有序集成员按 score 值递增(从小到大)次序排列。 */ + @Override @SuppressWarnings("rawtypes") public Set zrangeByScore(Object key, double min, double max) { Jedis jedis = getJedis(); @@ -1317,6 +1384,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 排名以 0 为底,也就是说, score 值最小的成员排名为 0 。 * 使用 ZREVRANK 命令可以获得成员按 score 值递减(从大到小)排列的排名。 */ + @Override public Long zrank(Object key, Object member) { Jedis jedis = getJedis(); try { @@ -1331,6 +1399,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 排名以 0 为底,也就是说, score 值最大的成员排名为 0 。 * 使用 ZRANK 命令可以获得成员按 score 值递增(从小到大)排列的排名。 */ + @Override public Long zrevrank(Object key, Object member) { Jedis jedis = getJedis(); try { @@ -1344,6 +1413,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。 * 当 key 存在但不是有序集类型时,返回一个错误。 */ + @Override public Long zrem(Object key, Object... members) { Jedis jedis = getJedis(); try { @@ -1357,6 +1427,7 @@ public class JbootJedisImpl extends JbootRedisBase { * 返回有序集 key 中,成员 member 的 score 值。 * 如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。 */ + @Override public Double zscore(Object key, Object member) { Jedis jedis = getJedis(); try { @@ -1372,6 +1443,7 @@ public class JbootJedisImpl extends JbootRedisBase { * @param channel * @param message */ + @Override public void publish(String channel, String message) { Jedis jedis = getJedis(); try { @@ -1387,6 +1459,7 @@ public class JbootJedisImpl extends JbootRedisBase { * @param channel * @param message */ + @Override public void publish(byte[] channel, byte[] message) { Jedis jedis = getJedis(); try { @@ -1403,6 +1476,7 @@ public class JbootJedisImpl extends JbootRedisBase { * @param listener * @param channels */ + @Override public void subscribe(JedisPubSub listener, final String... channels) { /** * https://github.com/xetorthio/jedis/wiki/AdvancedUsage @@ -1422,11 +1496,7 @@ public class JbootJedisImpl extends JbootRedisBase { break; } catch (JedisConnectionException e) { LOG.error("Failed connect to redis, reconnect it.", e); - try { - Thread.sleep(1000); - } catch (InterruptedException ie) { - break; - } + QuietlyUtil.sleepQuietly(1000); } finally { returnResource(jedis); } @@ -1443,6 +1513,7 @@ public class JbootJedisImpl extends JbootRedisBase { * @param binaryListener * @param channels */ + @Override public void subscribe(BinaryJedisPubSub binaryListener, final byte[]... channels) { /** * https://github.com/xetorthio/jedis/wiki/AdvancedUsage @@ -1454,28 +1525,46 @@ public class JbootJedisImpl extends JbootRedisBase { @Override public void run() { //订阅线程断开连接,需要进行重连 - while (true) { - Jedis jedis = getJedis(); + while (!isClose()) { + Jedis jedis = null; try { + jedis = jedisPool.getResource(); // subscribe 方法是阻塞的,不用担心会走到returnResource,除非异常 jedis.subscribe(binaryListener, channels); LOG.warn("Disconnect to redis channel in subscribe binaryListener!"); break; } catch (Throwable e) { LOG.error("Failed connect to redis, reconnect it.", e); - try { - Thread.sleep(1000); - } catch (InterruptedException ie) { - break; - } + QuietlyUtil.sleepQuietly(1000); } finally { - returnResource(jedis); + if (jedis != null) { + returnResource(jedis); + } } } } }.start(); } + @Override + public RedisScanResult scan(String pattern, String cursor, int scanCount) { + ScanParams params = new ScanParams(); + params.match(pattern).count(scanCount); + try (Jedis jedis = getJedis()) { + ScanResult scanResult = jedis.scan(cursor, params); + return new RedisScanResult<>(scanResult.getCursor(), scanResult.getResult()); + } + } + + @Override + public Object eval(String script, int keyCount, String... params) { + Jedis jedis = getJedis(); + try { + return jedis.eval(script, keyCount, params); + } finally { + returnResource(jedis); + } + } public Jedis getJedis() { try { @@ -1486,11 +1575,12 @@ public class JbootJedisImpl extends JbootRedisBase { } } + public JedisPool getJedisPool() { + return jedisPool; + } + public void returnResource(Jedis jedis) { if (jedis != null) { - /** - * close 实际上是 returnResource,查看源码 - */ jedis.close(); } } diff --git a/src/main/java/io/jboot/support/redis/lettuce/JbootLettuceCodec.java b/src/main/java/io/jboot/support/redis/lettuce/JbootLettuceCodec.java index 90c2aa80d2e032866026cd8011c310648e73cd1d..9aefe8b947bb1efe89a5a8547dc852b8a31bac10 100644 --- a/src/main/java/io/jboot/support/redis/lettuce/JbootLettuceCodec.java +++ b/src/main/java/io/jboot/support/redis/lettuce/JbootLettuceCodec.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/redis/lettuce/JbootLettuceImpl.java b/src/main/java/io/jboot/support/redis/lettuce/JbootLettuceImpl.java index 84b528392f462c8f2fe022b470201df37bf9c724..260cc82ba01a485e8ac3fc727569972fae6899f8 100644 --- a/src/main/java/io/jboot/support/redis/lettuce/JbootLettuceImpl.java +++ b/src/main/java/io/jboot/support/redis/lettuce/JbootLettuceImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package io.jboot.support.redis.lettuce; import io.jboot.support.redis.JbootRedis; import io.jboot.support.redis.JbootRedisConfig; +import io.jboot.support.redis.RedisScanResult; import io.lettuce.core.RedisClient; import redis.clients.jedis.BinaryJedisPubSub; import redis.clients.jedis.JedisPubSub; @@ -475,6 +476,11 @@ public class JbootLettuceImpl implements JbootRedis { } + @Override + public RedisScanResult scan(String pattern, String cursor, int scanCount) { + return null; + } + @Override public byte[] keyToBytes(Object key) { return new byte[0]; @@ -519,4 +525,9 @@ public class JbootLettuceImpl implements JbootRedis { public List valueListFromBytesList(Collection data) { return null; } + + @Override + public Object eval(String script, int keyCount, String... params) { + return null; + } } diff --git a/src/main/java/io/jboot/support/redis/lettuce/LettuceException.java b/src/main/java/io/jboot/support/redis/lettuce/LettuceException.java index 742bce2ed5594a3e7c3e50390b1e59a3d5e66d77..fd0501a87c7c585022655c2eaaf95796e6eccb4e 100644 --- a/src/main/java/io/jboot/support/redis/lettuce/LettuceException.java +++ b/src/main/java/io/jboot/support/redis/lettuce/LettuceException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/redis/redisson/JbootRedissonImpl.java b/src/main/java/io/jboot/support/redis/redisson/JbootRedissonImpl.java index 434e47c302caf2acd20e0485c075cadb57d7f652..aa75966d3ed4d5536df2e6882896b95d18d258d5 100644 --- a/src/main/java/io/jboot/support/redis/redisson/JbootRedissonImpl.java +++ b/src/main/java/io/jboot/support/redis/redisson/JbootRedissonImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.jboot.support.redis.redisson; import io.jboot.support.redis.JbootRedis; +import io.jboot.support.redis.RedisScanResult; import redis.clients.jedis.BinaryJedisPubSub; import redis.clients.jedis.JedisPubSub; @@ -464,6 +465,11 @@ public class JbootRedissonImpl implements JbootRedis { } + @Override + public RedisScanResult scan(String pattern, String cursor, int scanCount) { + return null; + } + @Override public byte[] keyToBytes(Object key) { return new byte[0]; @@ -508,4 +514,9 @@ public class JbootRedissonImpl implements JbootRedis { public List valueListFromBytesList(Collection data) { return null; } + + @Override + public Object eval(String script, int keyCount, String... params) { + return null; + } } diff --git a/src/main/java/io/jboot/support/seata/JbootSeataManager.java b/src/main/java/io/jboot/support/seata/JbootSeataManager.java index 371a947f6b21e054315d312491771ba329de6db5..72527ac762c66dba740f413733513d30d8caf9dd 100644 --- a/src/main/java/io/jboot/support/seata/JbootSeataManager.java +++ b/src/main/java/io/jboot/support/seata/JbootSeataManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ public class JbootSeataManager { private boolean enable = false; private TransactionalTemplate transactionalTemplate; - private GlobalLockTemplate globalLockTemplate; + private GlobalLockTemplate globalLockTemplate; private SeataGlobalTransactionManager transactionManager; @@ -65,10 +65,11 @@ public class JbootSeataManager { config.getTxServiceGroup(), config.getMode() ); + transactionManager.init(); this.transactionalTemplate = new TransactionalTemplate(); - this.globalLockTemplate = new GlobalLockTemplate<>(); + this.globalLockTemplate = new GlobalLockTemplate(); this.enable = true; } @@ -86,22 +87,18 @@ public class JbootSeataManager { public FailureHandler getFailureHandler() { if (handler == null) { - synchronized (this) { + String failureHandlerClassOrSpiName = config.getFailureHandler(); + if (StrUtil.isBlank(failureHandlerClassOrSpiName)) { + handler = new DefaultFailureHandlerImpl(); + } else { + if (failureHandlerClassOrSpiName.contains(".")) { + handler = ClassUtil.newInstance(failureHandlerClassOrSpiName); + } + if (handler == null) { + handler = JbootSpiLoader.load(FailureHandler.class, failureHandlerClassOrSpiName); + } if (handler == null) { - String failureHandlerClassOrSpiName = config.getFailureHandler(); - if (StrUtil.isBlank(failureHandlerClassOrSpiName)) { - handler = new DefaultFailureHandlerImpl(); - } else { - if (failureHandlerClassOrSpiName.contains(".")) { - handler = ClassUtil.newInstance(failureHandlerClassOrSpiName); - } - if (handler == null) { - handler = JbootSpiLoader.load(FailureHandler.class, failureHandlerClassOrSpiName); - } - if (handler == null) { - handler = new DefaultFailureHandlerImpl(); - } - } + handler = new DefaultFailureHandlerImpl(); } } } @@ -117,11 +114,11 @@ public class JbootSeataManager { this.transactionalTemplate = transactionalTemplate; } - public GlobalLockTemplate getGlobalLockTemplate() { + public GlobalLockTemplate getGlobalLockTemplate() { return globalLockTemplate; } - public void setGlobalLockTemplate(GlobalLockTemplate globalLockTemplate) { + public void setGlobalLockTemplate(GlobalLockTemplate globalLockTemplate) { this.globalLockTemplate = globalLockTemplate; } diff --git a/src/main/java/io/jboot/support/seata/SeataConfig.java b/src/main/java/io/jboot/support/seata/SeataConfig.java index 3259e8196b5c73b8a677b3e76ba0275a10a200ea..d069f819ed314d63fd406ab0996fd14a29e2a133 100644 --- a/src/main/java/io/jboot/support/seata/SeataConfig.java +++ b/src/main/java/io/jboot/support/seata/SeataConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/seata/SeataGlobalTransactionManager.java b/src/main/java/io/jboot/support/seata/SeataGlobalTransactionManager.java index 7cea31776646fb88433570c80d616ba586f72582..2c203d8f3600161f85d210351cc36538b96148b8 100644 --- a/src/main/java/io/jboot/support/seata/SeataGlobalTransactionManager.java +++ b/src/main/java/io/jboot/support/seata/SeataGlobalTransactionManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,17 @@ package io.jboot.support.seata; import com.jfinal.log.Log; - import io.seata.common.util.StringUtils; import io.seata.config.ConfigurationFactory; -import io.seata.core.rpc.netty.RmRpcClient; -import io.seata.core.rpc.netty.ShutdownHook; -import io.seata.core.rpc.netty.TmRpcClient; +import io.seata.core.rpc.ShutdownHook; +import io.seata.core.rpc.netty.RmNettyRemotingClient; +import io.seata.core.rpc.netty.TmNettyRemotingClient; import io.seata.rm.RMClient; import io.seata.tm.TMClient; import io.seata.tm.api.DefaultFailureHandlerImpl; import io.seata.tm.api.FailureHandler; + public class SeataGlobalTransactionManager { @SuppressWarnings("unused") @@ -140,8 +140,8 @@ public class SeataGlobalTransactionManager { private void registerSpringShutdownHook() { ShutdownHook.removeRuntimeShutdownHook(); - ShutdownHook.getInstance().addDisposable(TmRpcClient.getInstance(applicationId, txServiceGroup)); - ShutdownHook.getInstance().addDisposable(RmRpcClient.getInstance(applicationId, txServiceGroup)); + ShutdownHook.getInstance().addDisposable(TmNettyRemotingClient.getInstance(applicationId, txServiceGroup)); + ShutdownHook.getInstance().addDisposable(RmNettyRemotingClient.getInstance(applicationId, txServiceGroup)); } public void destroy() { diff --git a/src/main/java/io/jboot/support/seata/annotation/SeataGlobalLock.java b/src/main/java/io/jboot/support/seata/annotation/SeataGlobalLock.java index f20bd49b5344bd4bc2af4d0f3210f9f6aefaadfe..dea5672a5dece7009c71161f2cceaeac0fdd2c3a 100644 --- a/src/main/java/io/jboot/support/seata/annotation/SeataGlobalLock.java +++ b/src/main/java/io/jboot/support/seata/annotation/SeataGlobalLock.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,4 +34,9 @@ import java.lang.annotation.Target; @Target(ElementType.METHOD) @Inherited public @interface SeataGlobalLock { + + int lockRetryInterval() default 0; + + int lockRetryTimes() default -1; + } diff --git a/src/main/java/io/jboot/support/seata/annotation/SeataGlobalTransactional.java b/src/main/java/io/jboot/support/seata/annotation/SeataGlobalTransactional.java index 2db9467c1f6db7998dfffd27a648003c434baef1..14d2938fbb101405e619f12039bf70e3637467c0 100644 --- a/src/main/java/io/jboot/support/seata/annotation/SeataGlobalTransactional.java +++ b/src/main/java/io/jboot/support/seata/annotation/SeataGlobalTransactional.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ public @interface SeataGlobalTransactional { * * @return timeoutMills in MILLISECONDS. */ - int timeoutMills() default TransactionInfo.DEFAULT_TIME_OUT; + int timeoutMills() default 60000; /** * Given name of the global transaction instance. diff --git a/src/main/java/io/jboot/support/seata/filter/TransactionPropagationFilter.java b/src/main/java/io/jboot/support/seata/filter/TransactionPropagationFilter.java index 36853a3949b29b3d1dd9213c76a975f7956662d4..1ae5ef297d7663b047dc8d922acd0c96cc97c217 100644 --- a/src/main/java/io/jboot/support/seata/filter/TransactionPropagationFilter.java +++ b/src/main/java/io/jboot/support/seata/filter/TransactionPropagationFilter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalInterceptorBuilder.java b/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..3d5f8a7b6732c5d6dfc3699155e7283c5a04ccbf --- /dev/null +++ b/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalInterceptorBuilder.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.seata.interceptor; + +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.support.seata.annotation.SeataGlobalLock; +import io.jboot.support.seata.annotation.SeataGlobalTransactional; + +import java.lang.reflect.Method; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +@AutoLoad +public class SeataGlobalInterceptorBuilder implements InterceptorBuilder { + + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + + if (Util.hasAnnotation(method, SeataGlobalTransactional.class) + || Util.hasAnnotation(method, SeataGlobalLock.class)) { + interceptors.add(SeataGlobalTransactionalInterceptor.class, 0); + } + } + + +} diff --git a/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalTransactionHandler.java b/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalTransactionHandler.java index 54083530f16c462d3a9908d135dc4e1d6c17467d..0452515ca80161bec7648296df329f2f721f6771 100644 --- a/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalTransactionHandler.java +++ b/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalTransactionHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,6 +91,9 @@ public class SeataGlobalTransactionHandler { throw e.getCause(); case RollbackFailure: failureHandler.onRollbackFailure(e.getTransaction(), e.getCause()); + throw e.getCause(); + case RollbackRetrying: + failureHandler.onRollbackRetrying(e.getTransaction(), e.getOriginalException()); throw e.getCause(); default: throw new ShouldNeverHappenException("Unknown TransactionalExecutor.Code: " + code); diff --git a/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalTransactionalInterceptor.java b/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalTransactionalInterceptor.java index 513f08b3d1bef0e8bd473483e2ed5b9cd3bcd726..e8a6c69e51b9b45342058f96184ea502c60e7446 100644 --- a/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalTransactionalInterceptor.java +++ b/src/main/java/io/jboot/support/seata/interceptor/SeataGlobalTransactionalInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,8 @@ import com.jfinal.aop.Invocation; import io.jboot.support.seata.JbootSeataManager; import io.jboot.support.seata.annotation.SeataGlobalLock; import io.jboot.support.seata.annotation.SeataGlobalTransactional; -import io.jboot.web.fixedinterceptor.FixedInterceptor; +import io.seata.core.model.GlobalLockConfig; +import io.seata.rm.GlobalLockExecutor; import java.lang.reflect.Method; @@ -31,7 +32,7 @@ import java.lang.reflect.Method; * io/seata/spring/annotation/GlobalTransactionalInterceptor.java * */ -public class SeataGlobalTransactionalInterceptor implements Interceptor, FixedInterceptor { +public class SeataGlobalTransactionalInterceptor implements Interceptor { public SeataGlobalTransactionalInterceptor() { } @@ -49,7 +50,7 @@ public class SeataGlobalTransactionalInterceptor implements Interceptor, FixedIn if (globalTrxAnno != null) { handleGlobalTransaction(inv, globalTrxAnno); } else if (globalLockAnno != null) { - handleGlobalLock(inv); + handleGlobalLock(inv,globalLockAnno); } else { inv.invoke(); } @@ -60,17 +61,20 @@ public class SeataGlobalTransactionalInterceptor implements Interceptor, FixedIn } - private void handleGlobalLock(final Invocation inv) throws Exception { - JbootSeataManager.me().getGlobalLockTemplate().execute(() -> { - try { + private void handleGlobalLock(final Invocation inv, final SeataGlobalLock globalLockAnno) throws Throwable { + JbootSeataManager.me().getGlobalLockTemplate().execute(new GlobalLockExecutor() { + @Override + public Object execute() throws Throwable { inv.invoke(); return inv.getReturnValue(); - } catch (Throwable e) { - if (e instanceof Exception) { - throw (Exception)e; - } else { - throw new RuntimeException(e); - } + } + + @Override + public GlobalLockConfig getGlobalLockConfig() { + GlobalLockConfig config = new GlobalLockConfig(); + config.setLockRetryInterval(globalLockAnno.lockRetryInterval()); + config.setLockRetryTimes(globalLockAnno.lockRetryTimes()); + return config; } }); } diff --git a/src/main/java/io/jboot/support/seata/tcc/ActionInterceptorHandler.java b/src/main/java/io/jboot/support/seata/tcc/ActionInterceptorHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..12cfe3c131b7d7ebca533f123101040efa638027 --- /dev/null +++ b/src/main/java/io/jboot/support/seata/tcc/ActionInterceptorHandler.java @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.seata.tcc; + +import com.alibaba.fastjson.JSON; +import com.jfinal.aop.Invocation; +import com.jfinal.log.Log; +import io.seata.common.Constants; +import io.seata.common.exception.FrameworkException; +import io.seata.common.util.NetUtil; +import io.seata.common.util.ReflectionUtil; +import io.seata.core.model.BranchType; +import io.seata.rm.DefaultResourceManager; +import io.seata.rm.tcc.TCCResource; +import io.seata.rm.tcc.api.BusinessActionContext; +import io.seata.rm.tcc.api.BusinessActionContextParameter; +import io.seata.rm.tcc.api.BusinessActionContextUtil; +import io.seata.rm.tcc.api.TwoPhaseBusinessAction; +import io.seata.rm.tcc.interceptor.ActionContextUtil; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.HashMap; +import java.util.Map; + +/** + * Handler the TCC Participant Aspect : Setting Context, Creating Branch Record + * 参考:https://github.com/seata/seata/blob/master/tcc/src/main/java/io/seata/rm/tcc/interceptor/ActionInterceptorHandler.java + * + * @author zhangsen/菜农 commit: https://gitee.com/fuhai/jboot/commit/55564bfd9e6eebfc39263291d89592cd16f77498 + */ +public class ActionInterceptorHandler { + private static final Log LOGGER = Log.getLog(TccActionInterceptor.class); + + /** + * Handler the TCC Aspect + * + * @param method the method + * @param arguments the arguments + * @param businessAction the business action + * @return map map + */ + public void proceed(Method method, Object[] arguments, String xid, TwoPhaseBusinessAction businessAction, + Invocation invocation) { + + //TCC name + String actionName = businessAction.name(); + BusinessActionContext actionContext = new BusinessActionContext(); + actionContext.setXid(xid); + //set action name + actionContext.setActionName(actionName); + Class[] types = method.getParameterTypes(); + Parameter[] parameters = invocation.getMethod().getParameters(); + int argIndex = 0; + for (Class cls : types) { + if (cls.getName().equals(BusinessActionContext.class.getName())) { + arguments[argIndex] = actionContext; + break; + } + argIndex++; + } + //Creating Branch Record + String branchId = doTccActionLogStore(method,parameters, arguments, businessAction, actionContext); + try { + registryResource(method, invocation.getTarget(),types, businessAction); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + actionContext.setBranchId(branchId); + // save the previous action context + BusinessActionContext previousActionContext = BusinessActionContextUtil.getContext(); + try { + //share actionContext implicitly + BusinessActionContextUtil.setContext(actionContext); + if (businessAction.useTCCFence()) { + // Use TCC Fence, and return the business result + TCCFenceHandler.prepareFence(xid, Long.valueOf(branchId), actionName); + } + invocation.invoke(); + } finally { + try { + //to report business action context finally if the actionContext.getUpdated() is true + BusinessActionContextUtil.reportContext(actionContext); + } finally { + if (previousActionContext != null) { + // recovery the previous action context + BusinessActionContextUtil.setContext(previousActionContext); + } else { + // clear the action context + BusinessActionContextUtil.clear(); + } + } + } + } + + /** + * Creating Branch Record + * + * @param method the method + * @param arguments the arguments + * @param businessAction the business action + * @param actionContext the action context + * @return the string + */ + protected String doTccActionLogStore(Method method, Parameter[] parameters , Object[] arguments, TwoPhaseBusinessAction businessAction, + BusinessActionContext actionContext) { + String actionName = actionContext.getActionName(); + String xid = actionContext.getXid(); + // + Map context = fetchActionRequestContext(method, arguments,parameters); + context.put(Constants.ACTION_START_TIME, System.currentTimeMillis()); + + //init business context + initBusinessContext(context, method, businessAction); + //Init running environment context + initFrameworkContext(context); + actionContext.setActionContext(context); + + //init applicationData + Map applicationContext = new HashMap<>(4); + applicationContext.put(Constants.TCC_ACTION_CONTEXT, context); + String applicationContextStr = JSON.toJSONString(applicationContext); + try { + //registry branch record + Long branchId = DefaultResourceManager.get().branchRegister(BranchType.TCC, actionName, null, xid, + applicationContextStr, null); + return String.valueOf(branchId); + } catch (Throwable t) { + String msg = String.format("TCC branch Register error, xid: %s", xid); + LOGGER.error(msg, t); + throw new FrameworkException(t, msg); + } + } + + /** + * Init running environment context + * + * @param context the context + */ + protected void initFrameworkContext(Map context) { + try { + context.put(Constants.HOST_NAME, NetUtil.getLocalIp()); + } catch (Throwable t) { + LOGGER.warn("getLocalIP error", t); + } + } + + /** + * Init business context + * + * @param context the context + * @param method the method + * @param businessAction the business action + */ + protected void initBusinessContext(Map context, Method method, + TwoPhaseBusinessAction businessAction) { + if (method != null) { + //the phase one method name + context.put(Constants.PREPARE_METHOD, method.getName()); + } + if (businessAction != null) { + //the phase two method name + context.put(Constants.COMMIT_METHOD, businessAction.commitMethod()); + context.put(Constants.ROLLBACK_METHOD, businessAction.rollbackMethod()); + context.put(Constants.ACTION_NAME, businessAction.name()); + context.put(Constants.USE_TCC_FENCE, businessAction.useTCCFence()); + } + } + + /** + * Extracting context data from parameters, add them to the context + * + * @param method the method + * @param arguments the arguments + * @return map map + */ + protected Map fetchActionRequestContext(Method method, Object[] arguments,Parameter[] parameters) { + Map context = new HashMap<>(8); + int x = 0; + for (Parameter p : parameters) { + if (!p.isNamePresent()) { + // 必须通过添加 -parameters 进行编译,才可以获取 Parameter 的编译前的名字 + throw new RuntimeException(" Maven or IDE config is error. see https://jfinal.com/doc/3-3 "); + } + if (!"io.seata.rm.tcc.api.BusinessActionContext".equals(p.getType().getName())) { + context.put(p.getName(), arguments[x]); + } + x++; + } + return context; + } + + public void registryResource(Method m, Object interfaceClass, Class[] arguments, TwoPhaseBusinessAction businessAction) throws NoSuchMethodException { + if (businessAction != null) { + TCCResource tccResource = new TCCResource(); + tccResource.setActionName(businessAction.name()); + tccResource.setTargetBean(interfaceClass); + tccResource.setPrepareMethod(m); + tccResource.setCommitMethodName(businessAction.commitMethod()); + tccResource.setCommitMethod(ReflectionUtil + .getMethod(interfaceClass.getClass(), businessAction.commitMethod(), + new Class[] {BusinessActionContext.class})); + tccResource.setRollbackMethodName(businessAction.rollbackMethod()); + tccResource.setRollbackMethod(ReflectionUtil + .getMethod(interfaceClass.getClass(), businessAction.rollbackMethod(), + new Class[] {BusinessActionContext.class})); + // set argsClasses + tccResource.setCommitArgsClasses(businessAction.commitArgsClasses()); + tccResource.setRollbackArgsClasses(businessAction.rollbackArgsClasses()); + // set phase two method's keys + tccResource.setPhaseTwoCommitKeys(this.getTwoPhaseArgs(tccResource.getCommitMethod(), + businessAction.commitArgsClasses())); + tccResource.setPhaseTwoRollbackKeys(this.getTwoPhaseArgs(tccResource.getRollbackMethod(), + businessAction.rollbackArgsClasses())); + //registry tcc resource + DefaultResourceManager.get().registerResource(tccResource); + } + } + + protected String[] getTwoPhaseArgs(Method method, Class[] argsClasses) { + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + String[] keys = new String[parameterAnnotations.length]; + /* + * get parameter's key + * if method's parameter list is like + * (BusinessActionContext, @BusinessActionContextParameter("a") A a, @BusinessActionContextParameter("b") B b) + * the keys will be [null, a, b] + */ + for (int i = 0; i < parameterAnnotations.length; i++) { + for (int j = 0; j < parameterAnnotations[i].length; j++) { + if (parameterAnnotations[i][j] instanceof BusinessActionContextParameter) { + BusinessActionContextParameter param = (BusinessActionContextParameter)parameterAnnotations[i][j]; + String key = ActionContextUtil.getParamNameFromAnnotation(param); + keys[i] = key; + break; + } + } + if (keys[i] == null && !(argsClasses[i].equals(BusinessActionContext.class))) { + throw new IllegalArgumentException("non-BusinessActionContext parameter should use annotation " + + "BusinessActionContextParameter"); + } + } + return keys; + } +} diff --git a/src/main/java/io/jboot/support/seata/tcc/JbootTCCResourceManager.java b/src/main/java/io/jboot/support/seata/tcc/JbootTCCResourceManager.java new file mode 100644 index 0000000000000000000000000000000000000000..c7bd105bb5e63ba0ae4c3c03db4e34b514d9375a --- /dev/null +++ b/src/main/java/io/jboot/support/seata/tcc/JbootTCCResourceManager.java @@ -0,0 +1,242 @@ +package io.jboot.support.seata.tcc; + +import com.alibaba.fastjson.JSON; +import io.seata.common.Constants; +import io.seata.common.exception.ShouldNeverHappenException; +import io.seata.common.exception.SkipCallbackWrapperException; +import io.seata.common.util.StringUtils; +import io.seata.core.exception.TransactionException; +import io.seata.core.model.BranchStatus; +import io.seata.core.model.BranchType; +import io.seata.core.model.Resource; +import io.seata.rm.AbstractResourceManager; +import io.seata.rm.tcc.TCCResource; +import io.seata.rm.tcc.TwoPhaseResult; +import io.seata.rm.tcc.api.BusinessActionContext; + +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author zhangxn + */ +public class JbootTCCResourceManager extends AbstractResourceManager { + + /** + * TCC resource cache + */ + private Map tccResourceCache = new ConcurrentHashMap<>(); + + /** + * Instantiates a new Tcc resource manager. + */ + public JbootTCCResourceManager() { + // not do anything + } + + /** + * registry TCC resource + * + * @param resource The resource to be managed. + */ + @Override + public void registerResource(Resource resource) { + TCCResource tccResource = (TCCResource)resource; + tccResourceCache.put(tccResource.getResourceId(), tccResource); + super.registerResource(tccResource); + } + + @Override + public Map getManagedResources() { + return tccResourceCache; + } + + /** + * TCC branch commit + * + * @param branchType + * @param xid Transaction id. + * @param branchId Branch id. + * @param resourceId Resource id. + * @param applicationData Application data bind with this branch. + * @return BranchStatus + * @throws TransactionException TransactionException + */ + @Override + public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId, + String applicationData) throws TransactionException { + TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId); + if (tccResource == null) { + throw new ShouldNeverHappenException(String.format("TCC resource is not exist, resourceId: %s", resourceId)); + } + Object targetTCCBean = tccResource.getTargetBean(); + Method commitMethod = tccResource.getCommitMethod(); + if (targetTCCBean == null || commitMethod == null) { + throw new ShouldNeverHappenException(String.format("TCC resource is not available, resourceId: %s", resourceId)); + } + try { + //BusinessActionContext + BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId, + applicationData); + Object[] args = this.getTwoPhaseCommitArgs(tccResource, businessActionContext); + Object ret; + boolean result; + // add idempotent and anti hanging + if (Boolean.TRUE.equals(businessActionContext.getActionContext(Constants.USE_TCC_FENCE))) { + try { + result = TCCFenceHandler.commitFence(commitMethod, targetTCCBean, xid, branchId, args); + } catch (SkipCallbackWrapperException | UndeclaredThrowableException e) { + throw e.getCause(); + } + } else { + ret = commitMethod.invoke(targetTCCBean, null); + if (ret != null) { + if (ret instanceof TwoPhaseResult) { + result = ((TwoPhaseResult)ret).isSuccess(); + } else { + result = (boolean)ret; + } + } else { + result = true; + } + } + LOGGER.info("TCC resource commit result : {}, xid: {}, branchId: {}, resourceId: {}", result, xid, branchId, resourceId); + return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable; + } catch (Throwable t) { + String msg = String.format("commit TCC resource error, resourceId: %s, xid: %s.", resourceId, xid); + LOGGER.error(msg, t); + return BranchStatus.PhaseTwo_CommitFailed_Retryable; + } + } + + /** + * TCC branch rollback + * + * @param branchType the branch type + * @param xid Transaction id. + * @param branchId Branch id. + * @param resourceId Resource id. + * @param applicationData Application data bind with this branch. + * @return BranchStatus + * @throws TransactionException TransactionException + */ + @Override + public BranchStatus branchRollback(BranchType branchType, String xid, long branchId, String resourceId, + String applicationData) throws TransactionException { + TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId); + if (tccResource == null) { + throw new ShouldNeverHappenException(String.format("TCC resource is not exist, resourceId: %s", resourceId)); + } + Object targetTCCBean = tccResource.getTargetBean(); + Method rollbackMethod = tccResource.getRollbackMethod(); + if (targetTCCBean == null || rollbackMethod == null) { + throw new ShouldNeverHappenException(String.format("TCC resource is not available, resourceId: %s", resourceId)); + } + try { + //BusinessActionContext + BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId, + applicationData); + Object[] args = this.getTwoPhaseRollbackArgs(tccResource, businessActionContext); + + Object ret; + boolean result; + // add idempotent and anti hanging + if (Boolean.TRUE.equals(businessActionContext.getActionContext(Constants.USE_TCC_FENCE))) { + try { + result = TCCFenceHandler.rollbackFence(rollbackMethod, targetTCCBean, xid, branchId, + args, tccResource.getActionName()); + } catch (SkipCallbackWrapperException | UndeclaredThrowableException e) { + throw e.getCause(); + } + } else { + ret = rollbackMethod.invoke(targetTCCBean,args); + if (ret != null) { + if (ret instanceof TwoPhaseResult) { + result = ((TwoPhaseResult)ret).isSuccess(); + } else { + result = (boolean)ret; + } + } else { + result = true; + } + } + LOGGER.info("TCC resource rollback result : {}, xid: {}, branchId: {}, resourceId: {}", result, xid, branchId, resourceId); + return result ? BranchStatus.PhaseTwo_Rollbacked : BranchStatus.PhaseTwo_RollbackFailed_Retryable; + } catch (Throwable t) { + String msg = String.format("rollback TCC resource error, resourceId: %s, xid: %s.", resourceId, xid); + LOGGER.error(msg, t); + return BranchStatus.PhaseTwo_RollbackFailed_Retryable; + } + } + + /** + * transfer tcc applicationData to BusinessActionContext + * + * @param xid the xid + * @param branchId the branch id + * @param resourceId the resource id + * @param applicationData the application data + * @return business action context + */ + protected BusinessActionContext getBusinessActionContext(String xid, long branchId, String resourceId, + String applicationData) { + Map actionContextMap = null; + if (StringUtils.isNotBlank(applicationData)) { + Map tccContext = JSON.parseObject(applicationData, Map.class); + actionContextMap = (Map)tccContext.get(Constants.TCC_ACTION_CONTEXT); + } + if (actionContextMap == null) { + actionContextMap = new HashMap<>(2); + } + + //instance the action context + BusinessActionContext businessActionContext = new BusinessActionContext( + xid, String.valueOf(branchId), actionContextMap); + businessActionContext.setActionName(resourceId); + return businessActionContext; + } + + /** + * get phase two commit method's args + * @param tccResource tccResource + * @param businessActionContext businessActionContext + * @return args + */ + private Object[] getTwoPhaseCommitArgs(TCCResource tccResource, BusinessActionContext businessActionContext) { + String[] keys = tccResource.getPhaseTwoCommitKeys(); + Class[] argsCommitClasses = tccResource.getCommitArgsClasses(); + return this.getTwoPhaseMethodParams(keys, argsCommitClasses, businessActionContext); + } + + /** + * get phase two rollback method's args + * @param tccResource tccResource + * @param businessActionContext businessActionContext + * @return args + */ + private Object[] getTwoPhaseRollbackArgs(TCCResource tccResource, BusinessActionContext businessActionContext) { + String[] keys = tccResource.getPhaseTwoRollbackKeys(); + Class[] argsRollbackClasses = tccResource.getRollbackArgsClasses(); + return this.getTwoPhaseMethodParams(keys, argsRollbackClasses, businessActionContext); + } + + private Object[] getTwoPhaseMethodParams(String[] keys, Class[] argsClasses, BusinessActionContext businessActionContext) { + Object[] args = new Object[argsClasses.length]; + for (int i = 0; i < argsClasses.length; i++) { + if (argsClasses[i].equals(BusinessActionContext.class)) { + args[i] = businessActionContext; + } else { + args[i] = businessActionContext.getActionContext(keys[i], argsClasses[i]); + } + } + return args; + } + + @Override + public BranchType getBranchType() { + return BranchType.TCC; + } +} diff --git a/src/main/java/io/jboot/support/seata/tcc/TCCFenceHandler.java b/src/main/java/io/jboot/support/seata/tcc/TCCFenceHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..2e4dad76a03d1bc92e131fc3dd5585dad72d7a8a --- /dev/null +++ b/src/main/java/io/jboot/support/seata/tcc/TCCFenceHandler.java @@ -0,0 +1,325 @@ +package io.jboot.support.seata.tcc; + +/** + * @author zhangxn + * @date 2022/5/30 21:32 + */ + +import com.jfinal.plugin.activerecord.Db; +import com.jfinal.plugin.activerecord.DbPro; +import com.jfinal.plugin.activerecord.IAtom; +import io.jboot.db.datasource.DataSourceBuilder; +import io.jboot.db.datasource.DataSourceConfig; +import io.jboot.db.datasource.DataSourceConfigManager; +import io.jboot.utils.StrUtil; +import io.seata.common.exception.FrameworkErrorCode; +import io.seata.common.thread.NamedThreadFactory; +import io.seata.rm.tcc.TwoPhaseResult; +import io.seata.rm.tcc.constant.TCCFenceConstant; +import io.seata.rm.tcc.exception.TCCFenceException; +import io.seata.rm.tcc.store.TCCFenceDO; +import io.seata.rm.tcc.store.TCCFenceStore; +import io.seata.rm.tcc.store.db.TCCFenceStoreDataBaseDAO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * TCC Fence Handler(idempotent, non_rollback, suspend) + * + * @author kaka2code + */ +public class TCCFenceHandler { + + private TCCFenceHandler() { + throw new IllegalStateException("Utility class"); + } + + private static final Logger LOGGER = LoggerFactory.getLogger(io.seata.rm.tcc.TCCFenceHandler.class); + + private static final TCCFenceStore TCC_FENCE_DAO = TCCFenceStoreDataBaseDAO.getInstance(); + + private static DataSource dataSource; + + + private static final int MAX_THREAD_CLEAN = 1; + + private static final int MAX_QUEUE_SIZE = 500; + + private static final LinkedBlockingQueue LOG_QUEUE = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE); + + private static FenceLogCleanRunnable fenceLogCleanRunnable; + + private static ExecutorService logCleanExecutor; + + static { + try { + initLogCleanExecutor(); + } catch (Exception e) { + LOGGER.error("init fence log clean executor error", e); + } + } + + /** + * tcc prepare method enhanced + * + * @param xid the global transaction id + * @param branchId the branch transaction id + * @param actionName the action name + * @return the boolean + */ + public static Object prepareFence(String xid, Long branchId, String actionName) { + DataSourceConfig dataSourceConfig = DataSourceConfigManager.me().getMainDatasourceConfig(); + DataSource dataSource = new DataSourceBuilder(dataSourceConfig).build(); + IAtom runnable = () -> { + Connection connection = dataSource.getConnection(); + boolean result = insertTCCFenceLog(connection, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED); + LOGGER.info("TCC fence prepare result: {}. xid: {}, branchId: {}", result, xid, branchId); + if (!result) { + throw new TCCFenceException(String.format("Insert tcc fence record error, prepare fence failed. xid= %s, branchId= %s", xid, branchId), + FrameworkErrorCode.InsertRecordError); + } + return result; + }; + DbPro dbPro = StrUtil.isBlank(dataSourceConfig.getName()) ? Db.use() : Db.use(dataSourceConfig.getName()); + return dbPro.tx(runnable); + } + + /** + * tcc commit method enhanced + * + * @param commitMethod commit method + * @param targetTCCBean target tcc bean + * @param xid the global transaction id + * @param branchId the branch transaction id + * @param args commit method's parameters + * @return the boolean + */ + public static boolean commitFence(Method commitMethod, Object targetTCCBean, + String xid, Long branchId, Object[] args) { + + DataSourceConfig dataSourceConfig = DataSourceConfigManager.me().getMainDatasourceConfig(); + DataSource dataSource = new DataSourceBuilder(dataSourceConfig).build(); + IAtom runnable = () -> { + Connection connection = dataSource.getConnection(); + TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(connection, xid, branchId); + if (tccFenceDO == null){ + throw new TCCFenceException(String.format("Insert tcc fence record error, rollback fence method failed. xid= %s, branchId= %s", xid, branchId), + FrameworkErrorCode.InsertRecordError); + } + if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) { + LOGGER.info("Branch transaction has already committed before. idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus()); + return true; + } + if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus()); + } + return false; + } + try { + return updateStatusAndInvokeTargetMethod(connection, commitMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_COMMITTED, args); + } catch (Exception ex) { + throw new TCCFenceException(ex.getCause()); + } + }; + DbPro dbPro = StrUtil.isBlank(dataSourceConfig.getName()) ? Db.use() : Db.use(dataSourceConfig.getName()); + return dbPro.tx(runnable); + } + + /** + * tcc rollback method enhanced + * + * @param rollbackMethod rollback method + * @param targetTCCBean target tcc bean + * @param xid the global transaction id + * @param branchId the branch transaction id + * @param args rollback method's parameters + * @param actionName the action name + * @return the boolean + */ + public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean, + String xid, Long branchId, Object[] args, String actionName) { + DataSourceConfig dataSourceConfig = DataSourceConfigManager.me().getMainDatasourceConfig(); + DataSource dataSource = new DataSourceBuilder(dataSourceConfig).build(); + IAtom runnable = () -> { + try { + Connection connection = dataSource.getConnection(); + TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(connection, xid, branchId); + if (tccFenceDO == null){ + boolean result = insertTCCFenceLog(connection, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED); + LOGGER.info("Insert tcc fence record result: {}. xid: {}, branchId: {}", result, xid, branchId); + if (!result) { + throw new TCCFenceException(String.format("Insert tcc fence record error, rollback fence method failed. xid= %s, branchId= %s", xid, branchId), + FrameworkErrorCode.InsertRecordError); + } + return true; + } else { + if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) { + LOGGER.info("Branch transaction had already rollbacked before, idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus()); + return true; + } + if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) { + if (LOGGER.isWarnEnabled()) { + LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus()); + } + return false; + } + } + return updateStatusAndInvokeTargetMethod(connection, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, args); + } catch (Throwable ex) { + throw new TCCFenceException(ex.getCause()); + } + }; + DbPro dbPro = StrUtil.isBlank(dataSourceConfig.getName()) ? Db.use() : Db.use(dataSourceConfig.getName()); + return dbPro.tx(Connection.TRANSACTION_READ_UNCOMMITTED, runnable); + } + + /** + * Insert TCC fence log + * + * @param conn the db connection + * @param xid the xid + * @param branchId the branchId + * @param status the status + * @return the boolean + */ + private static boolean insertTCCFenceLog(Connection conn, String xid, Long branchId, String actionName, Integer status) { + TCCFenceDO tccFenceDO = new TCCFenceDO(); + tccFenceDO.setXid(xid); + tccFenceDO.setBranchId(branchId); + tccFenceDO.setActionName(actionName); + tccFenceDO.setStatus(status); + return TCC_FENCE_DAO.insertTCCFenceDO(conn, tccFenceDO); + } + + /** + * Update TCC Fence status and invoke target method + * + * @param method target method + * @param targetTCCBean target bean + * @param xid the global transaction id + * @param branchId the branch transaction id + * @param status the tcc fence status + * @return the boolean + */ + private static boolean updateStatusAndInvokeTargetMethod(Connection conn, Method method, Object targetTCCBean, + String xid, Long branchId, int status, Object[] args) throws Exception { + boolean result = TCC_FENCE_DAO.updateTCCFenceDO(conn, xid, branchId, status, TCCFenceConstant.STATUS_TRIED); + if (result) { + // invoke two phase method + Object ret = method.invoke(targetTCCBean, args); + if (null != ret) { + if (ret instanceof TwoPhaseResult) { + result = ((TwoPhaseResult) ret).isSuccess(); + } else { + result = (boolean) ret; + } + } + } + return result; + } + + private static void initLogCleanExecutor() { + logCleanExecutor = new ThreadPoolExecutor(MAX_THREAD_CLEAN, MAX_THREAD_CLEAN, Integer.MAX_VALUE, + TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), + new NamedThreadFactory("fenceLogCleanThread", MAX_THREAD_CLEAN, true) + ); + fenceLogCleanRunnable = new FenceLogCleanRunnable(); + logCleanExecutor.submit(fenceLogCleanRunnable); + } + + /** + * Delete TCC Fence + * + * @param xid the global transaction id + * @param branchId the branch transaction id + * @return the boolean + */ + public static boolean deleteFence(String xid, Long branchId) { + + DataSourceConfig dataSourceConfig = DataSourceConfigManager.me().getMainDatasourceConfig(); + DataSource dataSource = new DataSourceBuilder(dataSourceConfig).build(); + IAtom runnable = () -> { + try { + Connection connection = dataSource.getConnection(); + boolean ret = TCC_FENCE_DAO.deleteTCCFenceDO(connection, xid, branchId); + return ret; + } catch (Throwable ex) { + return false; + } + }; + DbPro dbPro = StrUtil.isBlank(dataSourceConfig.getName()) ? Db.use() : Db.use(dataSourceConfig.getName()); + return dbPro.tx(Connection.TRANSACTION_READ_UNCOMMITTED, runnable); + } + + private static void addToLogCleanQueue(final String xid, final long branchId) { + FenceLogIdentity logIdentity = new FenceLogIdentity(); + logIdentity.setXid(xid); + logIdentity.setBranchId(branchId); + try { + LOG_QUEUE.add(logIdentity); + } catch (Exception e) { + LOGGER.warn("Insert tcc fence record into queue for async delete error,xid:{},branchId:{}", xid, branchId, e); + } + } + + /** + * clean fence log that has the final status runnable. + * + * @see TCCFenceConstant + */ + private static class FenceLogCleanRunnable implements Runnable { + @Override + public void run() { + while (true) { + try { + FenceLogIdentity logIdentity = LOG_QUEUE.take(); + boolean ret = deleteFence(logIdentity.getXid(), logIdentity.getBranchId()); + if (!ret) { + LOGGER.error("delete fence log failed, xid: {}, branchId: {}", logIdentity.getXid(), logIdentity.getBranchId()); + } + } catch (InterruptedException e) { + LOGGER.error("take fence log from queue for clean be interrupted", e); + } catch (Exception e) { + LOGGER.error("exception occur when clean fence log", e); + } + } + } + } + + private static class FenceLogIdentity { + /** + * the global transaction id + */ + private String xid; + + /** + * the branch transaction id + */ + private Long branchId; + + public String getXid() { + return xid; + } + + public Long getBranchId() { + return branchId; + } + + public void setXid(String xid) { + this.xid = xid; + } + + public void setBranchId(Long branchId) { + this.branchId = branchId; + } + } +} diff --git a/src/main/java/io/jboot/support/seata/tcc/TccActionInterceptor.java b/src/main/java/io/jboot/support/seata/tcc/TccActionInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..799930ce1cfd4ed9cea4b87f2e51019db2b8b604 --- /dev/null +++ b/src/main/java/io/jboot/support/seata/tcc/TccActionInterceptor.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.seata.tcc; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import io.jboot.support.seata.JbootSeataManager; +import io.seata.common.util.StringUtils; +import io.seata.core.context.RootContext; +import io.seata.core.model.BranchType; +import io.seata.rm.tcc.api.TwoPhaseBusinessAction; + +import java.lang.reflect.Method; + + +/** + * TCC Interceptor + *

+ * 参考: https://github.com/seata/seata/blob/develop/spring/src/main/java/io/seata/spring/tcc/TccActionInterceptor.java + * + * @author zhangsen + */ +public class TccActionInterceptor implements Interceptor { + + + private ActionInterceptorHandler actionInterceptorHandler = new ActionInterceptorHandler(); + + + @Override + public void intercept(Invocation inv) { + + if (!JbootSeataManager.me().isEnable()) { + inv.invoke(); + return; + } + + if (!RootContext.inGlobalTransaction()) { + // not in transaction + inv.invoke(); + return; + } + + Method method = inv.getMethod(); + TwoPhaseBusinessAction businessAction = method.getAnnotation(TwoPhaseBusinessAction.class); + //try method + if (businessAction != null) { + //save the xid + String xid = RootContext.getXID(); + //clear the context + BranchType previousBranchType = RootContext.getBranchType(); + if (BranchType.TCC != previousBranchType) { + RootContext.bindBranchType(BranchType.TCC); + } + try { + Object[] methodArgs = inv.getArgs(); + //Handler the TCC Aspect + actionInterceptorHandler.proceed(method, methodArgs, xid, businessAction, inv); + } finally { + //if not TCC, unbind branchType + if (BranchType.TCC != previousBranchType) { + RootContext.unbindBranchType(); + } + } + } else { + inv.invoke(); + } + } + +} diff --git a/src/main/java/io/jboot/support/seata/tcc/TccInterceptorBuilder.java b/src/main/java/io/jboot/support/seata/tcc/TccInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..f4c8ed7f62e012d5528126a1417033d5647d1c9c --- /dev/null +++ b/src/main/java/io/jboot/support/seata/tcc/TccInterceptorBuilder.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.seata.tcc; + +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.utils.ClassUtil; +import io.seata.rm.tcc.api.TwoPhaseBusinessAction; + +import java.lang.reflect.Method; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +@AutoLoad +public class TccInterceptorBuilder implements InterceptorBuilder { + + private static Boolean hasSeataTccDependency = ClassUtil.hasClass("io.seata.rm.tcc.api.TwoPhaseBusinessAction"); + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (hasSeataTccDependency && Util.hasAnnotation(method, TwoPhaseBusinessAction.class)) { + interceptors.add(new TccActionInterceptor()); + } + } + + +} diff --git a/src/main/java/io/jboot/support/sentinel/AbstractSentinelInterceptor.java b/src/main/java/io/jboot/support/sentinel/AbstractSentinelInterceptor.java index f54d4e17e4a85f20a30bf8b1073d747f5b224204..89602309f1c45d8f56ab8fe60e8c84fb812a5c6d 100644 --- a/src/main/java/io/jboot/support/sentinel/AbstractSentinelInterceptor.java +++ b/src/main/java/io/jboot/support/sentinel/AbstractSentinelInterceptor.java @@ -21,9 +21,7 @@ import com.alibaba.csp.sentinel.log.RecordLog; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.util.MethodUtil; import com.alibaba.csp.sentinel.util.StringUtil; -import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; -import io.jboot.web.fixedinterceptor.FixedInterceptor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -35,7 +33,7 @@ import java.util.Arrays; * * @author Eric Zhao */ -public abstract class AbstractSentinelInterceptor implements Interceptor, FixedInterceptor { +public abstract class AbstractSentinelInterceptor { protected void traceException(Throwable ex) { Tracer.trace(ex); @@ -116,8 +114,7 @@ public abstract class AbstractSentinelInterceptor implements Interceptor, FixedI return handleDefaultFallback(inv, defaultFallback, fallbackClass, ex); } - protected Object handleDefaultFallback(Invocation inv, String defaultFallback, - Class[] fallbackClass, Throwable ex) throws Throwable { + protected Object handleDefaultFallback(Invocation inv, String defaultFallback, Class[] fallbackClass, Throwable ex) throws Throwable { // Execute the default fallback function if configured. Method fallbackMethod = extractDefaultFallbackMethod(inv, defaultFallback, fallbackClass); if (fallbackMethod != null) { diff --git a/src/main/java/io/jboot/support/sentinel/JbootSentinelBuilder.java b/src/main/java/io/jboot/support/sentinel/JbootSentinelBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..c18dea23230d152b40c114e2a97def147c27938a --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/JbootSentinelBuilder.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel; + +import com.alibaba.csp.sentinel.datasource.FileWritableDataSource; +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.datasource.WritableDataSource; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import com.jfinal.kit.PathKit; +import io.jboot.core.spi.JbootSpiLoader; +import io.jboot.support.sentinel.datasource.*; +import io.jboot.utils.StrUtil; + +import java.io.File; +import java.util.List; + +public class JbootSentinelBuilder { + + public void init() { + SentinelConfig config = SentinelConfig.get(); + + // 初始化 sentinel 数据源, + // 当配置数据源的时候,sentinel 控制面板的配置将会更新的时候,无法写入到数据源的,需要去实现主动写入,这是 Sentinel 的一个坑 + // todo 晚点实现主动 Sentinel 控制台写入到数据源 + if (StrUtil.isNotBlank(config.getDatasource())) { + SentinelDatasourceFactory factory = getDatasourceFactory(config); + ReadableDataSource rds = factory.createDataSource(); + FlowRuleManager.register2Property(rds.getProperty()); + } + + // 当未配置数据源的情况下,使用文件数据源 + // 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中. + // 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中. + // 文档:https://github.com/alibaba/Sentinel/wiki/%E5%9C%A8%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8-Sentinel + else { + + String rulePath = config.getRuleFile(); + File ruleFile = rulePath.startsWith("/") ? new File(rulePath) : new File(PathKit.getWebRootPath(), rulePath); + + ReadableDataSource rds = new FileDataSource<>(ruleFile, this::decodeJson); + FlowRuleManager.register2Property(rds.getProperty()); + + WritableDataSource> wds = new FileWritableDataSource<>(ruleFile, this::encodeJson); + WritableDataSourceRegistry.registerFlowDataSource(wds); + } + + } + + private String encodeJson(T t) { + return JSON.toJSONString(t); + } + + private List decodeJson(String source) { + return JSON.parseObject(source, new TypeReference>() { + }); + } + + + private SentinelDatasourceFactory getDatasourceFactory(SentinelConfig config) { + String datasource = config.getDatasource(); + switch (datasource) { + case SentinelConfig.DATASOURCE_APOLLO: + return new ApolloDatasourceFactory(); + case SentinelConfig.DATASOURCE_NACOS: + return new NacosDatasourceFactory(); + case SentinelConfig.DATASOURCE_ZOOKEEPER: + return new ZookeeperDatasourceFactory(); + case SentinelConfig.DATASOURCE_REDIS: + return new RedisDatasourceFactory(); + default: + SentinelDatasourceFactory dataSourceFactory = JbootSpiLoader.load(SentinelDatasourceFactory.class, datasource); + if (dataSourceFactory == null) { + throw new NullPointerException("Can not load SentinelDatasourceFactory spi for name: " + datasource); + } + return dataSourceFactory; + } + } + + +} diff --git a/src/main/java/io/jboot/support/sentinel/JbootSentinelManager.java b/src/main/java/io/jboot/support/sentinel/JbootSentinelManager.java new file mode 100644 index 0000000000000000000000000000000000000000..984ffe31a709c9ac928e63fda9055b38a8e88d5e --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/JbootSentinelManager.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel; + + +public class JbootSentinelManager { + + private static JbootSentinelManager manager = new JbootSentinelManager(); + + private JbootSentinelManager() { + } + + public static JbootSentinelManager me() { + return manager; + } + + private SentinelConfig config = SentinelConfig.get(); + + + public void init() { + if (!config.isEnable()) { + return; + } + + new JbootSentinelBuilder().init(); + } + + +} diff --git a/src/main/java/io/jboot/support/sentinel/ResourceMetadataRegistry.java b/src/main/java/io/jboot/support/sentinel/ResourceMetadataRegistry.java index 6c7d6bcd7cd065fe4e8f1b3641ad15605016406d..7452bafaa11d6e05e75520f5390763e8044ea08c 100644 --- a/src/main/java/io/jboot/support/sentinel/ResourceMetadataRegistry.java +++ b/src/main/java/io/jboot/support/sentinel/ResourceMetadataRegistry.java @@ -66,7 +66,7 @@ final class ResourceMetadataRegistry { } private static String getKey(Class clazz, String name) { - return String.format("%s:%s", clazz.getCanonicalName(), name); + return clazz.getCanonicalName() + ":" + name; } /** diff --git a/src/main/java/io/jboot/support/sentinel/SentinelConfig.java b/src/main/java/io/jboot/support/sentinel/SentinelConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..817f10003ade7b539670dfa12378400a5094d74f --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/SentinelConfig.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel; + +import io.jboot.Jboot; +import io.jboot.app.config.annotation.ConfigModel; + +import java.util.Map; + +@ConfigModel(prefix = "jboot.sentinel") +public class SentinelConfig { + + public static final String DATASOURCE_REDIS = "redis"; + public static final String DATASOURCE_ZOOKEEPER = "zookeeper"; + public static final String DATASOURCE_NACOS = "nacos"; + public static final String DATASOURCE_APOLLO = "apollo"; + + // 是否启用 + private boolean enable = false; + + private String datasource; + + // 是否对 http 请求启用限流,启用后还需要去 sentinel 后台配置 + private boolean reqeustEnable = true; + + private String reqeustTargetPrefix; + + // 如果 http 被限流后跳转的页面 + private String requestBlockPage; + + // 如果 http 被限流后渲染的 json 数据,requestBlockPage 配置优先于此项 + private Map requestBlockJsonMap; + + private String ruleFile = "sentinel-rule.json"; + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + + public String getDatasource() { + return datasource; + } + + public void setDatasource(String datasource) { + this.datasource = datasource; + } + + public boolean isReqeustEnable() { + return reqeustEnable; + } + + public void setReqeustEnable(boolean reqeustEnable) { + this.reqeustEnable = reqeustEnable; + } + + public String getReqeustTargetPrefix() { + return reqeustTargetPrefix; + } + + public void setReqeustTargetPrefix(String reqeustTargetPrefix) { + this.reqeustTargetPrefix = reqeustTargetPrefix; + } + + public String getRequestBlockPage() { + return requestBlockPage; + } + + public void setRequestBlockPage(String requestBlockPage) { + this.requestBlockPage = requestBlockPage; + } + + public Map getRequestBlockJsonMap() { + return requestBlockJsonMap; + } + + public void setRequestBlockJsonMap(Map requestBlockJsonMap) { + this.requestBlockJsonMap = requestBlockJsonMap; + } + + public String getRuleFile() { + return ruleFile; + } + + public void setRuleFile(String ruleFile) { + this.ruleFile = ruleFile; + } + + private static SentinelConfig sentinelConfig; + + public static SentinelConfig get() { + if (sentinelConfig == null){ + sentinelConfig = Jboot.config(SentinelConfig.class); + } + return sentinelConfig; + } +} diff --git a/src/main/java/io/jboot/support/sentinel/SentinelHandler.java b/src/main/java/io/jboot/support/sentinel/SentinelHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..776801c5b5f7426fb053e59e4fafa3d80e135c07 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/SentinelHandler.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel; + +import com.alibaba.csp.sentinel.*; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.jfinal.handler.Handler; +import io.jboot.utils.StrUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +public class SentinelHandler extends Handler { + + private static final String EMPTY_ORIGIN = ""; + + private final String[] targetPrefix; + + public SentinelHandler() { + targetPrefix = parseTargetPrefix(); + } + + private String[] parseTargetPrefix() { + String reqeustTargetPrefix = SentinelConfig.get().getReqeustTargetPrefix(); + if (StrUtil.isBlank(reqeustTargetPrefix)) { + return new String[0]; + } else { + return StrUtil.splitToSet(reqeustTargetPrefix, ",").toArray(new String[0]); + } + } + + @Override + public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { + + // 不对静态资源进行流量管理 + if (target.contains(".")) { + next.handle(target, request, response, isHandled); + return; + } + + //配置了前缀的情况 + if (targetPrefix.length > 0) { + boolean matchTarget = false; + for (String prefx : targetPrefix) { + if (target.startsWith(prefx)) { + matchTarget = true; + break; + } + } + + if (!matchTarget) { + next.handle(target, request, response, isHandled); + return; + } + } + + + Entry urlEntry = null; + try { + String targetResource = SentinelUtil.buildResource(request); + + if (StrUtil.isNotBlank(targetResource)) { + ContextUtil.enter(targetResource, getOrigin(request)); + urlEntry = SphU.entry(targetResource, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + } + + next.handle(target, request, response, isHandled); + } catch (BlockException e) { + SentinelUtil.blockRequest(request, response); + isHandled[0] = true; + } catch (Exception e2) { + Tracer.traceEntry(e2, urlEntry); + throw e2; + } finally { + if (urlEntry != null) { + urlEntry.exit(); + } + ContextUtil.exit(); + } + + } + + protected String getOrigin(HttpServletRequest request) { + return EMPTY_ORIGIN; + } + + +} diff --git a/src/main/java/io/jboot/support/sentinel/SentinelInterceptor.java b/src/main/java/io/jboot/support/sentinel/SentinelInterceptor.java index a853be35fb8f229622f018280f60ed34826c5b4e..f7c1cb21e67292ff218a291ff9d515955f82b9fd 100644 --- a/src/main/java/io/jboot/support/sentinel/SentinelInterceptor.java +++ b/src/main/java/io/jboot/support/sentinel/SentinelInterceptor.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -20,13 +20,14 @@ import com.alibaba.csp.sentinel.EntryType; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; /** * @author michael yang (fuhai999@gmail.com) * @Date: 2020/1/7 */ -public class SentinelInterceptor extends AbstractSentinelInterceptor { +public class SentinelInterceptor extends AbstractSentinelInterceptor implements Interceptor { @Override public void intercept(Invocation inv) { @@ -37,15 +38,12 @@ public class SentinelInterceptor extends AbstractSentinelInterceptor { return; } - String resourceName = getResourceName(annotation.value(), inv.getMethod()); EntryType entryType = annotation.entryType(); int resourceType = annotation.resourceType(); Entry entry = null; try { entry = SphU.entry(resourceName, resourceType, entryType, inv.getArgs()); -// Object result = pjp.proceed(); -// return result; inv.invoke(); } catch (BlockException ex) { try { diff --git a/src/main/java/io/jboot/support/sentinel/SentinelInterceptorBuilder.java b/src/main/java/io/jboot/support/sentinel/SentinelInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..26abf95c88d93d7081ecae96931000ba5265fa3e --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/SentinelInterceptorBuilder.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel; + +import com.alibaba.csp.sentinel.annotation.SentinelResource; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.utils.ClassUtil; + +import java.lang.reflect.Method; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +@AutoLoad +public class SentinelInterceptorBuilder implements InterceptorBuilder { + + private static Boolean hasSentinelDependency = ClassUtil.hasClass("com.alibaba.csp.sentinel.Sph"); + private static Boolean isEnable = SentinelConfig.get().isEnable(); + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (hasSentinelDependency && isEnable && Util.hasAnnotation(method, SentinelResource.class)) { + interceptors.add(SentinelInterceptor.class); + } + } + + +} diff --git a/src/main/java/io/jboot/support/sentinel/SentinelManager.java b/src/main/java/io/jboot/support/sentinel/SentinelManager.java deleted file mode 100644 index e58a044af1d416f0957438f4c64bdf4d4a766b36..0000000000000000000000000000000000000000 --- a/src/main/java/io/jboot/support/sentinel/SentinelManager.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2016-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.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.gnu.org/licenses/lgpl-3.0.txt - *

- * 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 io.jboot.support.sentinel; - -import com.alibaba.csp.sentinel.util.AppNameUtil; -import io.jboot.app.JbootApplicationConfig; -import io.jboot.app.config.JbootConfigManager; - -import java.lang.reflect.Field; - -/** - * @author michael yang (fuhai999@gmail.com) - * @Date: 2020/1/7 - */ -public class SentinelManager { - - private SentinelManager(){} - - private static SentinelManager me = new SentinelManager(); - - public static SentinelManager me(){ - return me; - } - - public void init(){ - - try { - JbootApplicationConfig appConfig = JbootConfigManager.me().get(JbootApplicationConfig.class); - Field field = AppNameUtil.class.getDeclaredField("appName"); - field.setAccessible(true); - field.set(null,appConfig.getName()); - } catch (Exception e) { - e.printStackTrace(); - } - - } -} diff --git a/src/main/java/io/jboot/support/sentinel/SentinelUtil.java b/src/main/java/io/jboot/support/sentinel/SentinelUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..6b85d7e14f370bb700be020fda8743dc19af948f --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/SentinelUtil.java @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel; + +import com.alibaba.csp.sentinel.util.StringUtil; +import com.jfinal.kit.JsonKit; +import com.jfinal.kit.LogKit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/22 + */ +public class SentinelUtil { + + private static final String PATH_SPLIT = "/"; + + public static String buildResource(HttpServletRequest request) { + String pathInfo = getResourcePath(request); + if (!pathInfo.startsWith(PATH_SPLIT)) { + pathInfo = PATH_SPLIT + pathInfo; + } + + if (PATH_SPLIT.equals(pathInfo)) { + return pathInfo; + } + + // Note: pathInfo should be converted to camelCase style. + int lastSlashIndex = pathInfo.lastIndexOf("/"); + + if (lastSlashIndex >= 0) { + pathInfo = pathInfo.substring(0, lastSlashIndex) + "/" + + StringUtil.trim(pathInfo.substring(lastSlashIndex + 1)); + } else { + pathInfo = PATH_SPLIT + StringUtil.trim(pathInfo); + } + + return pathInfo; + } + + + private static String getResourcePath(HttpServletRequest request) { + String pathInfo = normalizeAbsolutePath(request.getPathInfo(), false); + String servletPath = normalizeAbsolutePath(request.getServletPath(), pathInfo.length() != 0); + + return servletPath + pathInfo; + } + + + private static String normalizeAbsolutePath(String path, boolean removeTrailingSlash) throws IllegalStateException { + return normalizePath(path, true, false, removeTrailingSlash); + } + + + private static String normalizePath(String path, boolean forceAbsolute, boolean forceRelative, + boolean removeTrailingSlash) throws IllegalStateException { + char[] pathChars = StringUtil.trimToEmpty(path).toCharArray(); + int length = pathChars.length; + + // Check path and slash. + boolean startsWithSlash = false; + boolean endsWithSlash = false; + + if (length > 0) { + char firstChar = pathChars[0]; + char lastChar = pathChars[length - 1]; + + startsWithSlash = firstChar == PATH_SPLIT.charAt(0) || firstChar == '\\'; + endsWithSlash = lastChar == PATH_SPLIT.charAt(0) || lastChar == '\\'; + } + + StringBuilder buf = new StringBuilder(length); + boolean isAbsolutePath = forceAbsolute || !forceRelative && startsWithSlash; + int index = startsWithSlash ? 0 : -1; + int level = 0; + + if (isAbsolutePath) { + buf.append(PATH_SPLIT); + } + + while (index < length) { + index = indexOfSlash(pathChars, index + 1, false); + + if (index == length) { + break; + } + + int nextSlashIndex = indexOfSlash(pathChars, index, true); + + String element = new String(pathChars, index, nextSlashIndex - index); + index = nextSlashIndex; + + // Ignore "." + if (".".equals(element)) { + continue; + } + + // Backtrack ".." + if ("..".equals(element)) { + if (level == 0) { + if (isAbsolutePath) { + throw new IllegalStateException(path); + } else { + buf.append("..").append(PATH_SPLIT); + } + } else { + buf.setLength(pathChars[--level]); + } + + continue; + } + + pathChars[level++] = (char) buf.length(); + buf.append(element).append(PATH_SPLIT); + } + + // remove the last "/" + if (buf.length() > 0) { + if (!endsWithSlash || removeTrailingSlash) { + buf.setLength(buf.length() - 1); + } + } + + return buf.toString(); + } + + + private static int indexOfSlash(char[] chars, int beginIndex, boolean slash) { + int i = beginIndex; + + for (; i < chars.length; i++) { + char ch = chars[i]; + + if (slash) { + if (ch == PATH_SPLIT.charAt(0) || ch == '\\') { + break; // if a slash + } + } else { + if (ch != PATH_SPLIT.charAt(0) && ch != '\\') { + break; // if not a slash + } + } + } + + return i; + } + + + + protected static final String contentType = "application/json; charset=utf-8"; + + public static void writeDefaultBlockedJson(HttpServletResponse resp, Map map) throws IOException { + resp.setStatus(200); + resp.setContentType(contentType); + PrintWriter out = resp.getWriter(); + out.print(JsonKit.toJson(map)); + } + + + public static void writeDefaultBlockedPage(HttpServletResponse resp) throws IOException { + resp.setStatus(200); + PrintWriter out = resp.getWriter(); + out.print("Blocked by Sentinel (flow limiting) in Jboot"); + } + + + public static void blockRequest(HttpServletRequest request, HttpServletResponse response) { + StringBuffer url = request.getRequestURL(); + + if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) { + url.append("?").append(request.getQueryString()); + } + + SentinelConfig config = SentinelConfig.get(); + + try { + if (StringUtil.isNotBlank(config.getRequestBlockPage())) { + String redirectUrl = config.getRequestBlockPage() + "?http_referer=" + url.toString(); + response.sendRedirect(redirectUrl); + } else if (config.getRequestBlockJsonMap() != null && !config.getRequestBlockJsonMap().isEmpty()) { + writeDefaultBlockedJson(response, config.getRequestBlockJsonMap()); + } else { + writeDefaultBlockedPage(response); + } + } catch (IOException ex) { + LogKit.error(ex.toString(), ex); + } + } + +} diff --git a/src/main/java/io/jboot/support/sentinel/datasource/ApolloDatasourceConfig.java b/src/main/java/io/jboot/support/sentinel/datasource/ApolloDatasourceConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..33263e70485b4841c0dd90e1600225a3ff43ad46 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/ApolloDatasourceConfig.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.StrUtil; + +@ConfigModel(prefix = "jboot.sentinel.datasource.apollo") +public class ApolloDatasourceConfig { + + private String serverAddress; // 例如: http://localhost:8080 + private String appId; + private String namespaceName; // application + private String ruleKey = "jboot-sentinel-rule"; + private String defaultFlowRules = "[]"; + + public String getServerAddress() { + return serverAddress; + } + + public void setServerAddress(String serverAddress) { + this.serverAddress = serverAddress; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getNamespaceName() { + return namespaceName; + } + + public void setNamespaceName(String namespaceName) { + this.namespaceName = namespaceName; + } + + public String getRuleKey() { + return ruleKey; + } + + public void setRuleKey(String ruleKey) { + this.ruleKey = ruleKey; + } + + public String getDefaultFlowRules() { + return defaultFlowRules; + } + + public void setDefaultFlowRules(String defaultFlowRules) { + this.defaultFlowRules = defaultFlowRules; + } + + public void assertConfigOk() { + if (StrUtil.isAnyBlank(namespaceName, ruleKey)) { + throw new JbootIllegalConfigException("jboot.sentinel.datasource.apollo not config well in jboot.properties"); + } + } +} diff --git a/src/main/java/io/jboot/support/sentinel/datasource/ApolloDatasourceFactory.java b/src/main/java/io/jboot/support/sentinel/datasource/ApolloDatasourceFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..68ca77fdc19c99a2eecc0c15f44786ec4a61f0e5 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/ApolloDatasourceFactory.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.datasource.apollo.ApolloDataSource; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import io.jboot.Jboot; + +import java.util.List; + +public class ApolloDatasourceFactory implements SentinelDatasourceFactory { + + + @Override + public ReadableDataSource createDataSource() { + ApolloDatasourceConfig adc = Jboot.config(ApolloDatasourceConfig.class); + adc.assertConfigOk(); + + String appId = System.getProperty("app.id", adc.getAppId()); + String serverAddr = System.getProperty("apollo.meta", adc.getServerAddress()); + + System.setProperty("app.id", appId); + System.setProperty("apollo.meta", serverAddr); + + return new ApolloDataSource<>(adc.getNamespaceName(), adc.getRuleKey(), adc.getDefaultFlowRules(), + source -> JSON.parseObject(source, new TypeReference>() { + })); + } +} diff --git a/src/main/java/io/jboot/support/sentinel/datasource/FileDataSource.java b/src/main/java/io/jboot/support/sentinel/datasource/FileDataSource.java new file mode 100644 index 0000000000000000000000000000000000000000..3139e78416966d68b7703b9e61d1189c07666028 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/FileDataSource.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import com.alibaba.csp.sentinel.datasource.AbstractDataSource; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.jfinal.kit.LogKit; +import io.jboot.utils.FileUtil; +import io.jboot.utils.QuietlyUtil; + +import java.io.File; + +public class FileDataSource extends AbstractDataSource { + + private File file; + + private boolean fileExists; + private long fileLastModified = -1; + private long fileLength = -1; + private boolean isClosed = false; + + public FileDataSource(File file, Converter parser) { + super(parser); + this.file = file; + this.fileExists = file.exists(); + + if (this.fileExists) { + this.fileLastModified = file.lastModified(); + this.fileLength = file.length(); + } + + updateProperties(); + + new Thread(() -> { + while (!isClosed) { + try { + doReadAndUpdateProperties(); + } catch (Exception ex) { + LogKit.error(ex.toString(), ex); + } + QuietlyUtil.sleepQuietly(5000); + } + }, "jboot-sentinel-file-reader").start(); + } + + + private void doReadAndUpdateProperties() { + boolean fileExists = file.exists(); + long fileLastModified = fileExists ? file.lastModified() : -1; + long fileLength = fileExists ? file.length() : -1; + + if (this.fileExists != fileExists + || this.fileLength != fileLength + || this.fileLastModified != fileLastModified) { + + updateProperties(); + + this.fileExists = fileExists; + this.fileLastModified = fileLastModified; + this.fileLength = fileLength; + } + } + + + private void updateProperties() { + String content = file.exists() ? FileUtil.readString(file) : ""; + getProperty().updateValue(parser.convert(content)); + } + + + @Override + public String readSource() throws Exception { + return FileUtil.readString(file); + } + + + @Override + public void close() throws Exception { + isClosed = true; + } +} diff --git a/src/main/java/io/jboot/support/sentinel/datasource/NacosDatasourceConfig.java b/src/main/java/io/jboot/support/sentinel/datasource/NacosDatasourceConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..6a51f3c6e0137682146ab92d5253a0bd258a5d65 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/NacosDatasourceConfig.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.StrUtil; + +@ConfigModel(prefix = "jboot.sentinel.datasource.nacos") +public class NacosDatasourceConfig { + + private String serverAddress; // 例如: http://localhost:8080 + private String groupId; + private String dataId; + + public String getServerAddress() { + return serverAddress; + } + + public void setServerAddress(String serverAddress) { + this.serverAddress = serverAddress; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } + + public void assertConfigOk() { + if (StrUtil.isAnyBlank(serverAddress, groupId, dataId)) { + throw new JbootIllegalConfigException("jboot.sentinel.datasource.nacos not config well in jboot.properties"); + } + } +} diff --git a/src/main/java/io/jboot/support/sentinel/datasource/NacosDatasourceFactory.java b/src/main/java/io/jboot/support/sentinel/datasource/NacosDatasourceFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..08862b08d445520f244facdf2792290a0f46b1b5 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/NacosDatasourceFactory.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import io.jboot.Jboot; + +import java.util.List; + +public class NacosDatasourceFactory implements SentinelDatasourceFactory { + + @Override + public ReadableDataSource createDataSource() { + NacosDatasourceConfig nds = Jboot.config(NacosDatasourceConfig.class); + nds.assertConfigOk(); + + return new NacosDataSource<>(nds.getServerAddress(), nds.getGroupId(), nds.getDataId(), + source -> JSON.parseObject(source, new TypeReference>() { + })); + } +} diff --git a/src/main/java/io/jboot/support/sentinel/datasource/RedisDatasourceConfig.java b/src/main/java/io/jboot/support/sentinel/datasource/RedisDatasourceConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..6475822d883166776632654d4079ebab19a606b0 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/RedisDatasourceConfig.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.StrUtil; + +@ConfigModel(prefix = "jboot.sentinel.datasource.redis") +public class RedisDatasourceConfig { + + private String host; + private int port = 6379; + private String password; + private int database = 0; + private String ruleKey = "jboot-sentinel-rule"; + private String channel = "jboot-sentinel-channel"; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getDatabase() { + return database; + } + + public void setDatabase(int database) { + this.database = database; + } + + public String getRuleKey() { + return ruleKey; + } + + public void setRuleKey(String ruleKey) { + this.ruleKey = ruleKey; + } + + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public void assertConfigOk() { + if (StrUtil.isAnyBlank(host)) { + throw new JbootIllegalConfigException("jboot.sentinel.datasource.redis not config well in jboot.properties"); + } + } +} diff --git a/src/main/java/io/jboot/support/sentinel/datasource/RedisDatasourceFactory.java b/src/main/java/io/jboot/support/sentinel/datasource/RedisDatasourceFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..42a7fde49b85e35f0651a5cb456416800b8a7f54 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/RedisDatasourceFactory.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.datasource.redis.RedisDataSource; +import com.alibaba.csp.sentinel.datasource.redis.config.RedisConnectionConfig; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import io.jboot.Jboot; +import io.jboot.utils.StrUtil; + +import java.util.List; + +public class RedisDatasourceFactory implements SentinelDatasourceFactory { + + @Override + public ReadableDataSource createDataSource() { + RedisDatasourceConfig rdc = Jboot.config(RedisDatasourceConfig.class); + rdc.assertConfigOk(); + + RedisConnectionConfig.Builder builder = RedisConnectionConfig.builder() + .withHost(rdc.getHost()) + .withPort(rdc.getPort()) + .withDatabase(rdc.getDatabase()); + + if (StrUtil.isNotBlank(rdc.getPassword())) { + builder.withPassword(rdc.getPassword()); + } + + return new RedisDataSource<>(builder.build(), rdc.getRuleKey(), rdc.getChannel(), + source -> JSON.parseObject(source, new TypeReference>() { + })); + + } +} diff --git a/src/main/java/io/jboot/support/sentinel/datasource/SentinelDatasourceFactory.java b/src/main/java/io/jboot/support/sentinel/datasource/SentinelDatasourceFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..19319e4326e993ea842aea93b48cb2f50bfe5cb8 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/SentinelDatasourceFactory.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; + +public interface SentinelDatasourceFactory { + + ReadableDataSource createDataSource(); +} diff --git a/src/main/java/io/jboot/support/sentinel/datasource/ZookeeperDatasourceConfig.java b/src/main/java/io/jboot/support/sentinel/datasource/ZookeeperDatasourceConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..fee206b67ed6d4a3d28ac2bf36bffde041cf0ff5 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/ZookeeperDatasourceConfig.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import io.jboot.app.config.annotation.ConfigModel; +import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.utils.StrUtil; + +@ConfigModel(prefix = "jboot.sentinel.datasource.zookeeper") +public class ZookeeperDatasourceConfig { + + private String serverAddress; // 例如: 127.0.0.1:2181 + private String path="/jboot/sentinel/rule/default"; // 例如 /Sentinel-Demo/SYSTEM-CODE-DEMO-FLOW + + public String getServerAddress() { + return serverAddress; + } + + public void setServerAddress(String serverAddress) { + this.serverAddress = serverAddress; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public void assertConfigOk() { + if (StrUtil.isAnyBlank(serverAddress)) { + throw new JbootIllegalConfigException("jboot.sentinel.datasource.zookeeper not config well in jboot.properties"); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/support/sentinel/datasource/ZookeeperDatasourceFactory.java b/src/main/java/io/jboot/support/sentinel/datasource/ZookeeperDatasourceFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..1691f5ff7fe77347d4b64a97ca5177d678f4a3b8 --- /dev/null +++ b/src/main/java/io/jboot/support/sentinel/datasource/ZookeeperDatasourceFactory.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.sentinel.datasource; + +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.datasource.zookeeper.ZookeeperDataSource; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import io.jboot.Jboot; + +import java.util.List; + +public class ZookeeperDatasourceFactory implements SentinelDatasourceFactory { + + @Override + public ReadableDataSource createDataSource() { + ZookeeperDatasourceConfig zdc = Jboot.config(ZookeeperDatasourceConfig.class); + zdc.assertConfigOk(); + + return new ZookeeperDataSource<>(zdc.getServerAddress(), zdc.getPath(), + source -> JSON.parseObject(source, new TypeReference>() { + })); + } +} diff --git a/src/main/java/io/jboot/support/shiro/JbootShiroConfig.java b/src/main/java/io/jboot/support/shiro/JbootShiroConfig.java index 866735dd130fc71b89e09159d6d8ab8979cfa085..7d37043b18e7ac013ef2a3495b8fd05474bf9dd4 100644 --- a/src/main/java/io/jboot/support/shiro/JbootShiroConfig.java +++ b/src/main/java/io/jboot/support/shiro/JbootShiroConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ public class JbootShiroConfig { private String urlMapping = "/*"; private String invokeListener; + private String filter = "io.jboot.support.shiro.JbootShiroFilter"; public String getLoginUrl() { return loginUrl; @@ -79,6 +80,14 @@ public class JbootShiroConfig { public boolean isConfigOK() { return ini != null; } + + public String getFilter() { + return filter; + } + + public void setFilter(String filter) { + this.filter = filter; + } } diff --git a/src/main/java/io/jboot/support/shiro/JbootShiroFilter.java b/src/main/java/io/jboot/support/shiro/JbootShiroFilter.java index 5bb7d74fe43681d2de18de6cf9486212fe3be526..c21225859caee2af9c57786bc9b7dfd1d6be604c 100644 --- a/src/main/java/io/jboot/support/shiro/JbootShiroFilter.java +++ b/src/main/java/io/jboot/support/shiro/JbootShiroFilter.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ import java.io.IOException; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.shiro */ public class JbootShiroFilter extends ShiroFilter { diff --git a/src/main/java/io/jboot/support/shiro/JbootShiroInterceptor.java b/src/main/java/io/jboot/support/shiro/JbootShiroInterceptor.java index 8bd51b60929be446a242d54cc8abff6938361108..ebc6c9f8921e91f17cf25297a67ca12bf28883b6 100644 --- a/src/main/java/io/jboot/support/shiro/JbootShiroInterceptor.java +++ b/src/main/java/io/jboot/support/shiro/JbootShiroInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,29 +15,34 @@ */ package io.jboot.support.shiro; +import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; -import io.jboot.Jboot; import io.jboot.support.shiro.processer.AuthorizeResult; -import io.jboot.web.fixedinterceptor.FixedInterceptor; -import io.jboot.web.fixedinterceptor.FixedInvocation; /** * Shiro 拦截器 */ -public class JbootShiroInterceptor implements FixedInterceptor { - - private static JbootShiroConfig config = Jboot.config(JbootShiroConfig.class); +public class JbootShiroInterceptor implements Interceptor { @Override public void intercept(Invocation inv) { - if (!config.isConfigOK()) { - inv.invoke(); - return; + // 优先执行 onInvokeBefore,得到 AuthorizeResult + // 如果 AuthorizeResult 不为 null,则说明用户自定义了其认证方式,比如 jwt、oss 等,此时直接返回给 onInvokeAfter + // 如果 AuthorizeResult 为 null,则由系统去执行(主要是去判断 Shiro 注解,然后通过对应的 Processor 去执行 ) + + JbootShiroManager manager = JbootShiroManager.me(); + + AuthorizeResult result = manager.getInvokeListener().onInvokeBefore(inv); + + if (result == null) { + result = manager.invoke(inv); + } + + if (result == null) { + result = AuthorizeResult.ok(); } - JbootShiroManager.me().getInvokeListener().onInvokeBefore(inv); - AuthorizeResult result = JbootShiroManager.me().invoke(inv.getActionKey()); - JbootShiroManager.me().getInvokeListener().onInvokeAfter(inv, result == null ? AuthorizeResult.ok() : result); + manager.getInvokeListener().onInvokeAfter(inv, result); } } diff --git a/src/main/java/io/jboot/support/shiro/JbootShiroInvokeListener.java b/src/main/java/io/jboot/support/shiro/JbootShiroInvokeListener.java index e116a0afc784626e1a8f46ebf79ca071e8d571ef..349863b5665104ebdc3df4e0a3b6ab7a4fd4477f 100644 --- a/src/main/java/io/jboot/support/shiro/JbootShiroInvokeListener.java +++ b/src/main/java/io/jboot/support/shiro/JbootShiroInvokeListener.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,9 @@ import com.jfinal.core.Controller; import io.jboot.Jboot; import io.jboot.support.shiro.processer.AuthorizeResult; import io.jboot.utils.StrUtil; +import org.apache.shiro.web.util.WebUtils; + +import javax.servlet.http.HttpServletRequest; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) @@ -33,7 +36,7 @@ public interface JbootShiroInvokeListener { * * @param inv */ - public void onInvokeBefore(Invocation inv); + AuthorizeResult onInvokeBefore(Invocation inv); /** * 通过这个方法,可以用来自定义shiro 处理结果 和 错误逻辑 @@ -41,18 +44,19 @@ public interface JbootShiroInvokeListener { * @param inv * @param result */ - public void onInvokeAfter(Invocation inv, AuthorizeResult result); + void onInvokeAfter(Invocation inv, AuthorizeResult result); - public static final JbootShiroInvokeListener DEFAULT = new JbootShiroInvokeListener() { + JbootShiroInvokeListener DEFAULT = new JbootShiroInvokeListener() { private JbootShiroConfig config = Jboot.config(JbootShiroConfig.class); @Override - public void onInvokeBefore(Invocation inv) { + public AuthorizeResult onInvokeBefore(Invocation inv) { //do nothing + return null; } @Override @@ -81,6 +85,9 @@ public interface JbootShiroInvokeListener { controller.renderError(401); return; } + HttpServletRequest request = controller.getRequest(); + //保存被拦截的请求 Shiro将在登录成功后跳转到原请求 + WebUtils.saveRequest(request); controller.redirect(config.getLoginUrl()); } diff --git a/src/main/java/io/jboot/support/shiro/JbootShiroManager.java b/src/main/java/io/jboot/support/shiro/JbootShiroManager.java index ce6f1e6f82df9e79d8579d35c1e06dbbdab8f5b0..be4124bd44c086ed855e8d79aea7ef723e46ad97 100644 --- a/src/main/java/io/jboot/support/shiro/JbootShiroManager.java +++ b/src/main/java/io/jboot/support/shiro/JbootShiroManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,30 +15,34 @@ */ package io.jboot.support.shiro; +import com.jfinal.aop.Invocation; import com.jfinal.config.Routes; import com.jfinal.core.Controller; +import com.jfinal.template.expr.ast.MethodKeyBuilder; import io.jboot.Jboot; -import io.jboot.support.shiro.processer.*; import io.jboot.exception.JbootIllegalConfigException; +import io.jboot.support.shiro.processer.*; import io.jboot.utils.ArrayUtil; import io.jboot.utils.ClassUtil; import io.jboot.utils.StrUtil; -import io.jboot.web.utils.ControllerUtil; import org.apache.shiro.authz.annotation.*; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.List; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * shiro 管理器. */ public class JbootShiroManager { - private static JbootShiroManager me = new JbootShiroManager(); + private final static JbootShiroManager me = new JbootShiroManager(); - private JbootShiroConfig jbootShiroConfig = Jboot.config(JbootShiroConfig.class); + private final JbootShiroConfig jbootShiroConfig = Jboot.config(JbootShiroConfig.class); + + private final ShiroRequiresAuthenticationProcesser requiresAuthenticationProcessor = new ShiroRequiresAuthenticationProcesser(); + private final ShiroRequiresUserProcesser requiresUserProcessor = new ShiroRequiresUserProcesser(); + private final ShiroRequiresGuestProcesser requiresGuestProcessor = new ShiroRequiresGuestProcesser(); private JbootShiroManager() { } @@ -48,79 +52,57 @@ public class JbootShiroManager { return me; } - private ConcurrentHashMap invokers = new ConcurrentHashMap<>(); - - private ShiroRequiresAuthenticationProcesser requiresAuthenticationProcesser = new ShiroRequiresAuthenticationProcesser(); - private ShiroRequiresUserProcesser requiresUserProcesser = new ShiroRequiresUserProcesser(); - private ShiroRequiresGuestProcesser requiresGuestProcesser = new ShiroRequiresGuestProcesser(); + private ConcurrentHashMap invokers = new ConcurrentHashMap<>(); public void init(List routes) { - if (!jbootShiroConfig.isConfigOK()) { - return; - } - initInvokers(routes); + // do nothing } /** - * 初始化 invokers 变量 + * 根据类和方法上的注解生成shiro的注解处理器 + * + * @return 返回是否有shiro处理器,ShiroInterceptorBuilder 根据这一结果来决定是否对方法进行拦截 */ - private void initInvokers(List routes) { - Set excludedMethodName = ControllerUtil.buildExcludedMethodName(); - - for (Routes.Route route : routes) { - Class controllerClass = route.getControllerClass(); - - String controllerKey = route.getControllerKey(); - - Annotation[] controllerAnnotations = controllerClass.getAnnotations(); - - Method[] methods = controllerClass.getMethods(); - for (Method method : methods) { - if (excludedMethodName.contains(method.getName())) { - continue; - } - - if (method.getAnnotation(ShiroClear.class) != null) { - continue; - } - - - Annotation[] methodAnnotations = method.getAnnotations(); - Annotation[] allAnnotations = ArrayUtil.concat(controllerAnnotations, methodAnnotations); - - - String actionKey = ControllerUtil.createActionKey(controllerClass, method, controllerKey); - ShiroAuthorizeProcesserInvoker invoker = new ShiroAuthorizeProcesserInvoker(); - - - for (Annotation annotation : allAnnotations) { - if (annotation.annotationType() == RequiresPermissions.class) { - ShiroRequiresPermissionsProcesser processer = new ShiroRequiresPermissionsProcesser((RequiresPermissions) annotation); - invoker.addProcesser(processer); - } else if (annotation.annotationType() == RequiresRoles.class) { - ShiroRequiresRolesProcesser processer = new ShiroRequiresRolesProcesser((RequiresRoles) annotation); - invoker.addProcesser(processer); - } else if (annotation.annotationType() == RequiresUser.class) { - invoker.addProcesser(requiresUserProcesser); - } else if (annotation.annotationType() == RequiresAuthentication.class) { - invoker.addProcesser(requiresAuthenticationProcesser); - } else if (annotation.annotationType() == RequiresGuest.class) { - invoker.addProcesser(requiresGuestProcesser); - } - } + public boolean buildShiroInvoker(Class clazz, Method method) { + if (Controller.class.isAssignableFrom(clazz) && + JbootShiroUtil.getControllerExcludedMethodName().contains(method.getName())) { + // 忽略 JbootController 中的方法 + return false; + } - if (invoker.getProcessers() != null && invoker.getProcessers().size() > 0) { - invokers.put(actionKey, invoker); - } + if (method.getAnnotation(ShiroClear.class) != null) { + return false; + } + Annotation[] allAnnotations = ArrayUtil.concat(clazz.getAnnotations(), method.getAnnotations()); + ShiroAuthorizeProcesserInvoker invoker = new ShiroAuthorizeProcesserInvoker(); + for (Annotation annotation : allAnnotations) { + if (annotation.annotationType() == RequiresPermissions.class) { + ShiroRequiresPermissionsProcesser processor = new ShiroRequiresPermissionsProcesser((RequiresPermissions) annotation); + invoker.addProcesser(processor); + } else if (annotation.annotationType() == RequiresRoles.class) { + ShiroRequiresRolesProcesser processor = new ShiroRequiresRolesProcesser((RequiresRoles) annotation); + invoker.addProcesser(processor); + } else if (annotation.annotationType() == RequiresUser.class) { + invoker.addProcesser(requiresUserProcessor); + } else if (annotation.annotationType() == RequiresAuthentication.class) { + invoker.addProcesser(requiresAuthenticationProcessor); + } else if (annotation.annotationType() == RequiresGuest.class) { + invoker.addProcesser(requiresGuestProcessor); } } - } + if (invoker.getProcessers() != null && invoker.getProcessers().size() > 0) { + invokers.put(getMethodKey(method), invoker); + return true; + } - public AuthorizeResult invoke(String actionKey) { - ShiroAuthorizeProcesserInvoker invoker = invokers.get(actionKey); + return false; + } + + public AuthorizeResult invoke(Invocation invocation) { + ShiroAuthorizeProcesserInvoker invoker = invokers.get(getMethodKey(invocation.getMethod())); if (invoker == null) { return AuthorizeResult.ok(); } @@ -128,6 +110,14 @@ public class JbootShiroManager { return invoker.invoke(); } + + private static MethodKeyBuilder keyBuilder = new MethodKeyBuilder.FastMethodKeyBuilder(); + + public static Long getMethodKey(Method method) { + return keyBuilder.getMethodKey(method.getDeclaringClass(), method.getName(), method.getParameterTypes()); + } + + private JbootShiroInvokeListener invokeListener; public JbootShiroInvokeListener getInvokeListener() { diff --git a/src/main/java/io/jboot/web/utils/ControllerUtil.java b/src/main/java/io/jboot/support/shiro/JbootShiroUtil.java similarity index 75% rename from src/main/java/io/jboot/web/utils/ControllerUtil.java rename to src/main/java/io/jboot/support/shiro/JbootShiroUtil.java index 4551cc433b50878c1c1c55d6152bd7cd30fdedd2..eba9bcb3e991b76d7d2dd5827b714d76417d43ab 100644 --- a/src/main/java/io/jboot/web/utils/ControllerUtil.java +++ b/src/main/java/io/jboot/support/shiro/JbootShiroUtil.java @@ -1,7 +1,8 @@ -package io.jboot.web.utils; +package io.jboot.support.shiro; import com.jfinal.core.ActionKey; import com.jfinal.core.Controller; +import io.jboot.web.controller.JbootController; import java.lang.reflect.Method; import java.util.HashSet; @@ -10,9 +11,8 @@ import java.util.Set; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.utils */ -public class ControllerUtil { +public class JbootShiroUtil { private static final String SLASH = "/"; @@ -46,13 +46,16 @@ public class ControllerUtil { return actionKey; } + private static final Set excludedMethodName = new HashSet(); - public static Set buildExcludedMethodName() { - Set excludedMethodName = new HashSet(); - Method[] methods = Controller.class.getMethods(); - for (Method m : methods) { - excludedMethodName.add(m.getName()); + public static Set getControllerExcludedMethodName() { + if (excludedMethodName.isEmpty()) { + Method[] methods = JbootController.class.getMethods(); + for (Method m : methods) { + excludedMethodName.add(m.getName()); + } } + return excludedMethodName; } diff --git a/src/main/java/io/jboot/support/shiro/JbootShiroWebEnvironment.java b/src/main/java/io/jboot/support/shiro/JbootShiroWebEnvironment.java new file mode 100644 index 0000000000000000000000000000000000000000..c0a599ccf0ad2ed172e9a96ebe532a328efec4dc --- /dev/null +++ b/src/main/java/io/jboot/support/shiro/JbootShiroWebEnvironment.java @@ -0,0 +1,18 @@ +package io.jboot.support.shiro; + +import io.jboot.Jboot; +import org.apache.shiro.config.IniFactorySupport; +import org.apache.shiro.web.env.IniWebEnvironment; + +public class JbootShiroWebEnvironment extends IniWebEnvironment { + @Override + protected String[] getDefaultConfigLocations() { + //读取jboot配置文件中的jboot.shiro.ini配置项 + String iniFileName = "classpath:" + Jboot.configValue("jboot.shiro.ini", "shiro.ini"); + return new String[]{ + iniFileName, + DEFAULT_WEB_INI_RESOURCE_PATH, + IniFactorySupport.DEFAULT_INI_RESOURCE_PATH + }; + } +} diff --git a/src/main/java/io/jboot/support/shiro/ShiroAuthorizeProcesserInvoker.java b/src/main/java/io/jboot/support/shiro/ShiroAuthorizeProcesserInvoker.java index 0f782d5a5c3d1fda3e4440d9579b8b21f8debc01..665761a302d5b415bffb9faf9a7815358bc4a18a 100644 --- a/src/main/java/io/jboot/support/shiro/ShiroAuthorizeProcesserInvoker.java +++ b/src/main/java/io/jboot/support/shiro/ShiroAuthorizeProcesserInvoker.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import java.util.ArrayList; import java.util.List; /** - * Shiro 认证处理器 执行者 + * Shiro 认证处理器 执行者,每个 actionKey 都有一个独立的 ShiroAuthorizeProcesserInvoker *

* 它是对 IShiroAuthorizeProcesser 的几个集合处理 */ diff --git a/src/main/java/io/jboot/support/shiro/ShiroInterceptorBuilder.java b/src/main/java/io/jboot/support/shiro/ShiroInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..0644a3fd4218756b9d7ed47f06935de8b75137c2 --- /dev/null +++ b/src/main/java/io/jboot/support/shiro/ShiroInterceptorBuilder.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.support.shiro; + +import io.jboot.Jboot; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; + +import java.lang.reflect.Method; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +@AutoLoad +public class ShiroInterceptorBuilder implements InterceptorBuilder { + + private static final JbootShiroConfig config = Jboot.config(JbootShiroConfig.class); + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + + if (config.isConfigOK() && + Util.isController(targetClass) // 暂时只对 controller 层的方法进行拦截 + ) { + boolean needIntercept = JbootShiroManager.me().buildShiroInvoker(targetClass, method); + if (needIntercept) { + interceptors.add(JbootShiroInterceptor.class); + } + } + } + + +} diff --git a/src/main/java/io/jboot/support/shiro/cache/JbootShiroCache.java b/src/main/java/io/jboot/support/shiro/cache/JbootShiroCache.java index e7809d740bf8545a48de200edf5a65f48c5bbe07..613dc4efaafed35c5ae395f0ee10545cfda75ad6 100755 --- a/src/main/java/io/jboot/support/shiro/cache/JbootShiroCache.java +++ b/src/main/java/io/jboot/support/shiro/cache/JbootShiroCache.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/cache/JbootShiroCacheManager.java b/src/main/java/io/jboot/support/shiro/cache/JbootShiroCacheManager.java index b40c7871408d0b7e6ac1b7459546e3733d617af6..a32bff4730ea40447c285341bf15e7a5519c3573 100755 --- a/src/main/java/io/jboot/support/shiro/cache/JbootShiroCacheManager.java +++ b/src/main/java/io/jboot/support/shiro/cache/JbootShiroCacheManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/JbootShiroDirectiveBase.java b/src/main/java/io/jboot/support/shiro/directives/JbootShiroDirectiveBase.java index aca62b5e29d020e634280fb986e176b404ee0560..d21fef68a9e25a644d6ee36259767eec5bb74c0e 100644 --- a/src/main/java/io/jboot/support/shiro/directives/JbootShiroDirectiveBase.java +++ b/src/main/java/io/jboot/support/shiro/directives/JbootShiroDirectiveBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroAuthenticatedDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroAuthenticatedDirective.java index 2ee964847845458e922b1a28f8dea6aec03a030a..291d4f695ff393c37419da3f2730863a0aeee39e 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroAuthenticatedDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroAuthenticatedDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroGuestDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroGuestDirective.java index ba5eb11fbc3ab36a9c3eab83dda569aa5e293463..dc464cdf95896c3af74a449dc3f168a1e34f42f9 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroGuestDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroGuestDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroHasAllPermissionDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroHasAllPermissionDirective.java index 11a1b617997b8f6605a011580a2e0359d7843bba..f2c19fb871ec820168cc2ecde7d35749d68aba04 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroHasAllPermissionDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroHasAllPermissionDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroHasAllRolesDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroHasAllRolesDirective.java index 5ec63abe0573be2402000d6d046094a7da586e37..2040b225b57e2b38dc89423bfab75bd1b4bed220 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroHasAllRolesDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroHasAllRolesDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ public class ShiroHasAllRolesDirective extends JbootShiroDirectiveBase { @Override public void onRender(Env env, Scope scope, Writer writer) { if (getSubject() != null && ArrayUtil.isNotEmpty(exprList.getExprArray())) { - List roles = new ArrayList(); + List roles = new ArrayList<>(); for (Expr expr : exprList.getExprArray()) { roles.add(expr.eval(scope).toString()); } diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroHasAnyPermissionDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroHasAnyPermissionDirective.java index 041c6bc49dcb44660d37c3194629b6fc43af717a..020fd2c28f5439df2c7d4dddf5fda57186d07edd 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroHasAnyPermissionDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroHasAnyPermissionDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroHasAnyRolesDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroHasAnyRolesDirective.java index bef6a413dff26d73bcdba983d08765580d9ecbbc..f2a9c460ae0450ae77226679cc78463101631970 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroHasAnyRolesDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroHasAnyRolesDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroHasPermissionDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroHasPermissionDirective.java index aa7dc9e8841df0fe586740210036b496e2dff804..047c97f9892e464acbfed22afea9b904179f3e53 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroHasPermissionDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroHasPermissionDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroHasRoleDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroHasRoleDirective.java index 2296c3e918d42aa0d79497b9c186727bcb4ef470..aca55bc36969a50a174af8804e3ccb2a29d68eaf 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroHasRoleDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroHasRoleDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroNoAuthenticatedDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroNoAuthenticatedDirective.java index f5e715fa315664f616d95f49faa852cb9ca51132..8e07f3733a2b21d594293872fe7dce7b6f037536 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroNoAuthenticatedDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroNoAuthenticatedDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroNotHasPermissionDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroNotHasPermissionDirective.java index 84e0738b9a5b4242c5f0394f56b4b038571279e6..7b5dac033f876f214bf85aae271d5fc5fc5b235e 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroNotHasPermissionDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroNotHasPermissionDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroNotHasRoleDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroNotHasRoleDirective.java index 6fcec6b240d20235b68d9985878eaa084378a31e..7509e13a2bd2ca9153e4e7d5b74780049ca73b5c 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroNotHasRoleDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroNotHasRoleDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/directives/ShiroPrincipalDirective.java b/src/main/java/io/jboot/support/shiro/directives/ShiroPrincipalDirective.java index 052f1ab5299946e1eb0ee7feb2782bb7ba4717d2..8ee59b2c6dea6a73ca817092dc889565e45409cd 100644 --- a/src/main/java/io/jboot/support/shiro/directives/ShiroPrincipalDirective.java +++ b/src/main/java/io/jboot/support/shiro/directives/ShiroPrincipalDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/processer/AuthorizeResult.java b/src/main/java/io/jboot/support/shiro/processer/AuthorizeResult.java index 6a5fd68824656ee28b70cf8c5fbd607433ef988f..adcd662a01fa18c7c809bcb1a9028959c6f667c9 100644 --- a/src/main/java/io/jboot/support/shiro/processer/AuthorizeResult.java +++ b/src/main/java/io/jboot/support/shiro/processer/AuthorizeResult.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/processer/IShiroAuthorizeProcesser.java b/src/main/java/io/jboot/support/shiro/processer/IShiroAuthorizeProcesser.java index 0f045c98ff8603a5e556249b4f72945c398b4d28..a22951afd8297f3e892c119b8e8c3ad8549fd811 100644 --- a/src/main/java/io/jboot/support/shiro/processer/IShiroAuthorizeProcesser.java +++ b/src/main/java/io/jboot/support/shiro/processer/IShiroAuthorizeProcesser.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,9 @@ package io.jboot.support.shiro.processer; /** * Shiro 的认证处理器 - * 用于对每个controller 的 每个方法进行认证 + * 用于对每个 controller 的 每个方法进行认证 + * + * 每个 shiro 注解,都有一个对于的 Processer,比如 注解 @RequiresGuest 的处理器为 ShiroRequiresGuestProcesser.java */ public interface IShiroAuthorizeProcesser { diff --git a/src/main/java/io/jboot/support/shiro/processer/ShiroClear.java b/src/main/java/io/jboot/support/shiro/processer/ShiroClear.java index b468ff02d03167bc8bbaaad3b17122b6a68e57ff..4bfa8cf5a14b12294cc316ca3d5c7381beb160ec 100644 --- a/src/main/java/io/jboot/support/shiro/processer/ShiroClear.java +++ b/src/main/java/io/jboot/support/shiro/processer/ShiroClear.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresAuthenticationProcesser.java b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresAuthenticationProcesser.java index 5a4709ff75d23da2c8f1a3e4121f6c6865c4937d..99304ca6795e0bec62f3259a49f6ab56a2a5977e 100644 --- a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresAuthenticationProcesser.java +++ b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresAuthenticationProcesser.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresGuestProcesser.java b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresGuestProcesser.java index 7c57c69afb2c41ba5a3d9d55ff1423dda7b90cfc..994cd6798c110f934986b9fe9d5d1bb88f6c56f2 100644 --- a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresGuestProcesser.java +++ b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresGuestProcesser.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresPermissionsProcesser.java b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresPermissionsProcesser.java index c5fbdf85f1de8f2aef4f8e53ce48a08787745ea5..1496213b06163c3874078f8f7aadb39f66a69852 100644 --- a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresPermissionsProcesser.java +++ b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresPermissionsProcesser.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresRolesProcesser.java b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresRolesProcesser.java index e13e3b1ee9d7b319fba86b18133ea77786a5b87c..9b394df0f353d5e50d02f6cabeffd67d27c586d5 100644 --- a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresRolesProcesser.java +++ b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresRolesProcesser.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresUserProcesser.java b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresUserProcesser.java index 203f12ca765399ebf32a2a543df931df850d975c..1174604c1a941099c34675003231f5b95efe9142 100644 --- a/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresUserProcesser.java +++ b/src/main/java/io/jboot/support/shiro/processer/ShiroRequiresUserProcesser.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/swagger/ControllerReaderExtension.java b/src/main/java/io/jboot/support/swagger/ControllerReaderExtension.java index 38c1e3eff76584d7448dc5135578ceec10d8a286..55fca00261082661f9ef7698e6d1a33ce1c4f91d 100644 --- a/src/main/java/io/jboot/support/swagger/ControllerReaderExtension.java +++ b/src/main/java/io/jboot/support/swagger/ControllerReaderExtension.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/swagger/JbootSwaggerConfig.java b/src/main/java/io/jboot/support/swagger/JbootSwaggerConfig.java index 287267bcec2f09676dcc3962e81021e7a4e9f9fa..82c564d2398f25cfc751dd49a754ac4d7e6b6575 100644 --- a/src/main/java/io/jboot/support/swagger/JbootSwaggerConfig.java +++ b/src/main/java/io/jboot/support/swagger/JbootSwaggerConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/swagger/JbootSwaggerController.java b/src/main/java/io/jboot/support/swagger/JbootSwaggerController.java index a3faff7c3ea0100cff0fce239e8be956d6c3b6c3..25054461072a336d86e82e8e3eb0bc609666fb9e 100644 --- a/src/main/java/io/jboot/support/swagger/JbootSwaggerController.java +++ b/src/main/java/io/jboot/support/swagger/JbootSwaggerController.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,16 +18,15 @@ package io.jboot.support.swagger; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializeConfig; import com.google.common.collect.Maps; +import com.jfinal.ext.cors.EnableCORS; import io.jboot.Jboot; import io.jboot.web.controller.JbootController; -import io.jboot.web.cors.EnableCORS; import io.swagger.models.Swagger; import io.swagger.models.properties.RefProperty; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.swagger */ public class JbootSwaggerController extends JbootController { @@ -46,7 +45,7 @@ public class JbootSwaggerController extends JbootController { } String basePath = getRequest().getRequestURL().toString(); - String jsonUrl = basePath + "json"; + String jsonUrl = basePath.endsWith("/") ? basePath + "json" : basePath + "/json"; html = html.replace("http://petstore.swagger.io/v2/swagger.json", jsonUrl); // 可能是 https ,看下载的 swagger 版本 diff --git a/src/main/java/io/jboot/support/swagger/JbootSwaggerManager.java b/src/main/java/io/jboot/support/swagger/JbootSwaggerManager.java index c26641ce7d505670c84ffff27ac23cb9c19c8a21..56e2f9518b9a86fe7b4631ae8c3722b4dd6abed8 100644 --- a/src/main/java/io/jboot/support/swagger/JbootSwaggerManager.java +++ b/src/main/java/io/jboot/support/swagger/JbootSwaggerManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ import static io.swagger.models.Scheme.HTTPS; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.swagger *

* 相关文档: https://www.gitbook.com/book/huangwenchao/swagger/details */ diff --git a/src/main/java/io/jboot/support/swagger/ParamType.java b/src/main/java/io/jboot/support/swagger/ParamType.java index a26a385bc51a12e965fbda6252169be9912e33a8..9969d2b2002f9ae04d7950b5920f5e003fc42fa9 100644 --- a/src/main/java/io/jboot/support/swagger/ParamType.java +++ b/src/main/java/io/jboot/support/swagger/ParamType.java @@ -3,7 +3,6 @@ package io.jboot.support.swagger; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.component.swagger */ public class ParamType { diff --git a/src/main/java/io/jboot/support/swagger/Reader.java b/src/main/java/io/jboot/support/swagger/Reader.java index 636a322f246c19ecc4f357b2a542040acb3c10cb..caeaea04cf899d67fe31e38679af737ae72ca527 100644 --- a/src/main/java/io/jboot/support/swagger/Reader.java +++ b/src/main/java/io/jboot/support/swagger/Reader.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package io.jboot.support.swagger; +import com.jfinal.core.ActionKey; import com.jfinal.core.Controller; import io.jboot.web.controller.JbootControllerManager; import io.swagger.models.Operation; @@ -72,7 +73,17 @@ public class Reader { String methodPath = "index".equals(method.getName()) ? "" : "/" + method.getName(); String operationPath = JbootControllerManager.me().getPathByController((Class) context.getCls()) + methodPath; - + //如果有ActionKey注解的URL路径,则使用该路径而不是方法名 + ActionKey actionKeyAnnotation = ReflectionUtils.getAnnotation(method, ActionKey.class); + if (actionKeyAnnotation != null && !actionKeyAnnotation.value().isEmpty()) { + if (actionKeyAnnotation.value().startsWith("./")) { + String actionName = actionKeyAnnotation.value().substring(2); + String pathByController = JbootControllerManager.me().getPathByController((Class) context.getCls()); + operationPath = pathByController.endsWith("/") ? (pathByController + actionName) : (pathByController + "/" + actionName); + } else { + operationPath = actionKeyAnnotation.value(); + } + } String httpMethod = extension.getHttpMethod(context, method); if (operationPath == null || httpMethod == null) { diff --git a/src/main/java/io/jboot/support/swagger/ReaderContext.java b/src/main/java/io/jboot/support/swagger/ReaderContext.java index 81469236c032da8d6a08dd8067bfc464b5543cc5..b2b988692664cb2f951db36fbc96e3f8f5cb9bde 100644 --- a/src/main/java/io/jboot/support/swagger/ReaderContext.java +++ b/src/main/java/io/jboot/support/swagger/ReaderContext.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/swagger/RefPropertySerializer.java b/src/main/java/io/jboot/support/swagger/RefPropertySerializer.java index df2b3254105354d8d0188f0d01979017312627ba..b132b98d270d7778606552e828ed65b28e11e453 100644 --- a/src/main/java/io/jboot/support/swagger/RefPropertySerializer.java +++ b/src/main/java/io/jboot/support/swagger/RefPropertySerializer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/support/swagger/SwaggerPath.java b/src/main/java/io/jboot/support/swagger/SwaggerPath.java index 1b380aa1b4b8ae2884fa02bb79d37d8b7e7542c6..eae5a0f749bd719775b909e67a55483ecfcfdce0 100644 --- a/src/main/java/io/jboot/support/swagger/SwaggerPath.java +++ b/src/main/java/io/jboot/support/swagger/SwaggerPath.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import java.util.Map; * @version V1.0 * @Title: 自定义 Swagger Path * @Description: 目的是为了 防止 fastjson 生成 opreations 和 operationMap 的json生成 - * @Package io.jboot.component.swagger */ public class SwaggerPath extends Path { diff --git a/src/main/java/io/jboot/test/CPI.java b/src/main/java/io/jboot/test/CPI.java new file mode 100644 index 0000000000000000000000000000000000000000..847d0e82366db9cd8bcb0625ff88f710c89bf7f8 --- /dev/null +++ b/src/main/java/io/jboot/test/CPI.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import io.jboot.test.web.MockHttpServletRequest; +import io.jboot.test.web.MockHttpServletResponse; + +public class CPI { + + public static void startApp(Class testClass) { + MockApp.getInstance().start(testClass); + } + + public static void stopApp() { + MockApp.getInstance().stop(); + } + + public static void mockRequest(MockHttpServletRequest request, MockHttpServletResponse response) { + MockApp.mockRequest(request, response); + } + + public static void setTestInstance(Object testInstants) { + MockApp.getInstance().setTestInstance(testInstants); + } + +} diff --git a/src/main/java/io/jboot/test/MockApp.java b/src/main/java/io/jboot/test/MockApp.java new file mode 100644 index 0000000000000000000000000000000000000000..20a784ac603fd3de284f5fb6039e341fe5c5e9c9 --- /dev/null +++ b/src/main/java/io/jboot/test/MockApp.java @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import com.jfinal.config.JFinalConfig; +import com.jfinal.core.JFinalFilter; +import com.jfinal.kit.LogKit; +import com.jfinal.kit.PathKit; +import io.jboot.aop.JbootAopFactory; +import io.jboot.aop.cglib.JbootCglibProxyFactory; +import io.jboot.aop.javassist.JbootJavassistProxyFactory; +import io.jboot.app.PathKitExt; +import io.jboot.app.config.JbootConfigManager; +import io.jboot.test.web.MockFilterChain; +import io.jboot.test.web.MockFilterConfig; +import io.jboot.test.web.MockJFinalFilter; +import io.jboot.utils.ClassScanner; +import io.jboot.utils.FileUtil; +import io.jboot.utils.ReflectUtil; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +class MockApp { + + public static final String DEFAULT_WEB_ROOT_PATH = "../classes/webapp"; + public static final String DEFAULT_CLASS_PATH = "../classes"; + + private static final MockApp app = new MockApp(); + + private JFinalConfig config; + private final JFinalFilter filter; + + private Object testInstance; + + private boolean isInit = false; + + + private MockApp() { + filter = new MockJFinalFilter(); + } + + public static MockApp getInstance() { + return app; + } + + static void mockRequest(ServletRequest req, ServletResponse res) { + try { + app.filter.doFilter(req, res, new MockFilterChain()); + } catch (IOException | ServletException e) { + e.printStackTrace(); + } + } + + public void setTestInstance(Object testInstance) { + this.testInstance = testInstance; + } + + public Object getTestInstance() { + return testInstance; + } + + void start(Class testClass) { + if (!isInit) { + init(testClass); + isInit = true; + } + } + + private void init(Class testClass) { + try { + TestConfig testConfig = testClass.getAnnotation(TestConfig.class); + + if (testConfig != null) { + JbootConfigManager.parseArgs(testConfig.launchArgs()); + JbootConfigManager.me().setDevMode(testConfig.devMode()); + ClassScanner.setPrintScannerInfoEnable(testConfig.printScannerInfo()); + } else { + ClassScanner.setPrintScannerInfoEnable(false); + } + + doInitJFinalPathKit(testConfig); + + + boolean autoMockInterface = testConfig != null && testConfig.autoMockInterface(); + + //support cglib and javassist + JbootCglibProxyFactory.setMethodInterceptor(aClass -> new MockMethodInterceptor(autoMockInterface)); + JbootJavassistProxyFactory.setMethodInterceptor(aClass -> new MockMethodInterceptor(autoMockInterface)); + + List mockMethodInfos = getMockMethodInfoList(testClass); + if (!mockMethodInfos.isEmpty()) { + mockMethodInfos.forEach(MockMethodInterceptor::addMethodInfo); + } + + List mockClassInfos = getMockClassInfoList(testClass); + if (mockClassInfos != null && !mockClassInfos.isEmpty()) { + mockClassInfos.forEach(mockClassInfo -> { + JbootAopFactory.me().addMapping(mockClassInfo.getTargetClass(), mockClassInfo.getMockClass()); + MockMethodInterceptor.addMockClass(mockClassInfo.getMockClass()); + }); + } + + filter.init(new MockFilterConfig()); + config = ReflectUtil.getFieldValue(filter, "jfinalConfig"); + } catch (ServletException e) { + LogKit.error(e.toString(), e); + } + } + + private List getMockClassInfoList(Class testClass) { + List scanedClasses = ClassScanner.scanClassByAnnotation(MockClass.class, true); + + LinkedList mockClasses = new LinkedList<>(scanedClasses); + Class[] declardClasses = testClass.getDeclaredClasses(); + if (declardClasses.length > 0) { + for (Class declardClass : declardClasses) { + if (declardClass.getAnnotation(MockClass.class) != null) { + mockClasses.remove(declardClass); + mockClasses.addLast(declardClass); + } + } + } + + if (mockClasses.isEmpty()) { + return null; + } + + List classInfoList = new ArrayList<>(); + for (Class mockClass : mockClasses) { +// MockClass annotation = mockClass.getAnnotation(MockClass.class); +// Class[] targetClasses = annotation.value(); +// if (targetClasses.length == 0) { +// targetClasses = mockClass.getInterfaces(); +// } + Class[] targetClasses = mockClass.getInterfaces(); + if (targetClasses.length == 0) { + throw new IllegalStateException("@MockClass() in \"" + mockClass.getName() + "\" must implementation interface."); + } + + for (Class targetClass : targetClasses) { + classInfoList.add(new MockClassInfo(mockClass, targetClass)); + } + } + + return classInfoList; + } + + private List getMockMethodInfoList(Class testClass) { + List methodInfoList = new ArrayList<>(); + searchMockMethods(testClass, methodInfoList); + return methodInfoList; + } + + + private void searchMockMethods(Class searchClass, List tolist) { + Method[] methods = searchClass.getDeclaredMethods(); + for (Method method : methods) { + MockMethod mockMethod = method.getAnnotation(MockMethod.class); + if (mockMethod != null) { + tolist.add(new MockMethodInfo(searchClass, method, mockMethod)); + } + } + + Class superClass = searchClass.getSuperclass(); + if (superClass != Object.class && superClass != null) { + searchMockMethods(superClass, tolist); + } + } + + + void stop() { + if (config != null) { + config.onStop(); + } + } + + + private void doInitJFinalPathKit(TestConfig testConfig) { + try { + String configWebRootPath = testConfig != null ? testConfig.webRootPath() : DEFAULT_WEB_ROOT_PATH; + String configClassPath = testConfig != null ? testConfig.classPath() : DEFAULT_CLASS_PATH; + + //相对路径,是相对 /target/test-classes 进行判断的 + if (!FileUtil.isAbsolutePath(configWebRootPath)) { + configWebRootPath = new File(PathKitExt.getWebRootPath(), configWebRootPath).getCanonicalPath(); + } + //设置 webRootPath + PathKit.setWebRootPath(configWebRootPath); + + + if (!FileUtil.isAbsolutePath(configClassPath)) { + configClassPath = new File(PathKitExt.getRootClassPath(), configClassPath).getCanonicalPath(); + } + //设置 classPath + PathKit.setRootClassPath(configClassPath); + + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + +} diff --git a/src/main/java/io/jboot/test/MockClass.java b/src/main/java/io/jboot/test/MockClass.java new file mode 100644 index 0000000000000000000000000000000000000000..2b51b681801835a218345c201f2f70697844232d --- /dev/null +++ b/src/main/java/io/jboot/test/MockClass.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface MockClass { +// Class[] value() default {}; +} \ No newline at end of file diff --git a/src/main/java/io/jboot/test/MockClassInfo.java b/src/main/java/io/jboot/test/MockClassInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..8932ff97ded10f178cbe78d610ce4cf185366deb --- /dev/null +++ b/src/main/java/io/jboot/test/MockClassInfo.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +class MockClassInfo { + + private Class mockClass; + private Class targetClass; + + public MockClassInfo(Class mockClass, Class targetClass) { + this.mockClass = mockClass; + this.targetClass = targetClass; + } + + public Class getMockClass() { + return mockClass; + } + + public void setMockClass(Class mockClass) { + this.mockClass = mockClass; + } + + public Class getTargetClass() { + return targetClass; + } + + public void setTargetClass(Class targetClass) { + this.targetClass = targetClass; + } +} diff --git a/src/main/java/io/jboot/test/MockExceptions.java b/src/main/java/io/jboot/test/MockExceptions.java new file mode 100644 index 0000000000000000000000000000000000000000..42bf44eb6382feeacf65157c334935591134112d --- /dev/null +++ b/src/main/java/io/jboot/test/MockExceptions.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + + +public class MockExceptions { + + public static UnsupportedOperationException unsupported = new UnsupportedOperationException("Unsupported in mock."); +} diff --git a/src/main/java/io/jboot/test/MockMethod.java b/src/main/java/io/jboot/test/MockMethod.java new file mode 100644 index 0000000000000000000000000000000000000000..f31cca3ce305f9d1d95f9a5c666a826df2553321 --- /dev/null +++ b/src/main/java/io/jboot/test/MockMethod.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface MockMethod { + + Class targetClass(); + + String targetMethod() default ""; + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/test/MockMethodInfo.java b/src/main/java/io/jboot/test/MockMethodInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..4784d81a25725be7594ec05a9a69afcd362354bf --- /dev/null +++ b/src/main/java/io/jboot/test/MockMethodInfo.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import com.jfinal.kit.LogKit; +import io.jboot.utils.ReflectUtil; +import io.jboot.utils.StrUtil; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +class MockMethodInfo { + + private Class targetClass; + private Method targetMethod; + private boolean firstArgIsTarget; + + private Class mockClass; + private Method mockMethod; + + public MockMethodInfo(MockMethodInfo methodInfo,Class newTargetClass) { + this.targetClass = newTargetClass; + this.targetMethod = methodInfo.targetMethod; + this.firstArgIsTarget = methodInfo.firstArgIsTarget; + this.mockClass = methodInfo.mockClass; + this.mockMethod = methodInfo.mockMethod; + } + + + public MockMethodInfo(Class testClass, Method testClassMethod, MockMethod mockMethod) { + this.targetClass = mockMethod.targetClass(); + + String targetMethodName = StrUtil.isNotBlank(mockMethod.targetMethod()) ? mockMethod.targetMethod() : testClassMethod.getName(); + this.targetMethod = ReflectUtil.searchMethod(targetClass, m -> { + if (!m.getName().equals(targetMethodName)) { + return false; + } + + Class[] testClassMethodParaTypes = testClassMethod.getParameterTypes(); + + if (Arrays.equals(m.getParameterTypes(), testClassMethodParaTypes)) { + return true; + } + + if (testClassMethodParaTypes.length > 0 && testClassMethodParaTypes[0] == mockMethod.targetClass()) { + Class[] newClassMethodParaTypes = new Class[testClassMethodParaTypes.length - 1]; + System.arraycopy(testClassMethodParaTypes, 1, newClassMethodParaTypes, 0, newClassMethodParaTypes.length); + + if (Arrays.equals(m.getParameterTypes(), newClassMethodParaTypes)) { + this.firstArgIsTarget = true; + return true; + } + } + return false; + }); + + if (targetMethod == null) { + throw new IllegalStateException("Can not mock the method: \"" + targetMethodName + "\" in class: " + mockMethod.targetClass()); + } + + this.mockClass = testClass; + this.mockMethod = testClassMethod; + + } + + + public Class getTargetClass() { + return targetClass; + } + + public void setTargetClass(Class targetClass) { + this.targetClass = targetClass; + } + + public Method getTargetMethod() { + return targetMethod; + } + + public void setTargetMethod(Method targetMethod) { + this.targetMethod = targetMethod; + } + + public Class getMockClass() { + return mockClass; + } + + public void setMockClass(Class mockClass) { + this.mockClass = mockClass; + } + + public Method getMockMethod() { + return mockMethod; + } + + public void setMockMethod(Method mockMethod) { + this.mockMethod = mockMethod; + } + + public Object invokeMock(Object obj, Object... args) throws InvocationTargetException, IllegalAccessException { + if (firstArgIsTarget) { + Object[] newArgs = new Object[args.length + 1]; + newArgs[0] = obj; + System.arraycopy(args, 0, newArgs, 1, args.length); + args = newArgs; + } + + Object testInstance = MockApp.getInstance().getTestInstance(); + if (testInstance == null) { + testInstance = newInstance(this.mockClass); + } + + if (testInstance == null) { + return null; + } + + return mockMethod.invoke(testInstance, args); + } + + + private static T newInstance(Class clazz) { + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (Exception e) { + LogKit.logNothing(e); + } + return null; + } + +} diff --git a/src/main/java/io/jboot/test/MockMethodInterceptor.java b/src/main/java/io/jboot/test/MockMethodInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..64c3e4dad04a35ca80f9416687c4b202fe7c662d --- /dev/null +++ b/src/main/java/io/jboot/test/MockMethodInterceptor.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import com.jfinal.kit.LogKit; +import io.jboot.aop.InterceptorCache; +import io.jboot.aop.cglib.JbootCglibCallback; +import io.jboot.aop.javassist.JbootJavassistHandler; +import io.jboot.service.JbootServiceBase; +import io.jboot.utils.ClassUtil; +import javassist.util.proxy.MethodHandler; +import net.sf.cglib.proxy.MethodProxy; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class MockMethodInterceptor extends JbootCglibCallback implements MethodHandler { + + private static final Map METHOD_INFO_CACHE = new HashMap<>(); + + public static void addMethodInfo(MockMethodInfo value) { + InterceptorCache.MethodKey methodKey = InterceptorCache.getMethodKey(value.getTargetClass(), value.getTargetMethod()); + METHOD_INFO_CACHE.put(methodKey, value); + } + + public static void addMockClass(Class mockClass) { + Class[] interfaces = mockClass.getInterfaces(); + List mockMethodInfos = new ArrayList<>(); + for (Class inter : interfaces) { + for (MockMethodInfo mockMethodInfo : METHOD_INFO_CACHE.values()) { + //相同的代理对象 + if (mockMethodInfo.getTargetClass() == inter) { + mockMethodInfos.add(new MockMethodInfo(mockMethodInfo, mockClass)); + } + } + } + mockMethodInfos.forEach(MockMethodInterceptor::addMethodInfo); + } + + + private boolean autoMockInterface; + + public MockMethodInterceptor(boolean autoMockInterface) { + this.autoMockInterface = autoMockInterface; + } + + @Override + public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + + Class targetClass = ClassUtil.getUsefulClass(target.getClass()); + + //对于接口而且没有实现类的情况,target 是一个 Object 类 + if (targetClass == Object.class && method.getDeclaringClass() != Object.class) { + targetClass = method.getDeclaringClass(); + } + + InterceptorCache.MethodKey methodKey = InterceptorCache.getMethodKey(targetClass, method); + + if (METHOD_INFO_CACHE.containsKey(methodKey)) { + MockMethodInfo methodInfo = METHOD_INFO_CACHE.get(methodKey); + return methodInfo.invokeMock(target, args); + } + + if (autoMockInterface && Modifier.isInterface(targetClass.getModifiers())) { + if (!("toString".equals(method.getName()) && args.length == 0)) { + LogKit.warn("Return null for Mock Method: \"" + ClassUtil.buildMethodString(method) + "\", " + + "Because the class \"" + targetClass.getName() + "\" is an interface and has no any implementation classes."); + } + return null; + } + + try { + return super.intercept(target, method, args, methodProxy); + } catch (Exception ex) { + if ("initDao".equals(method.getName()) && JbootServiceBase.class == method.getDeclaringClass()) { + return null; + } else { + throw ex; + } + } + + } + + + private static final JbootJavassistHandler orginalHandler = new JbootJavassistHandler(); + + @Override + public Object invoke(Object target, Method thisMethod, Method method, Object[] args) throws Throwable { + Class targetClass = ClassUtil.getUsefulClass(target.getClass()); + + //对于接口而且没有实现类的情况,target 是一个 Object 类 + if (targetClass == Object.class && method.getDeclaringClass() != Object.class) { + targetClass = method.getDeclaringClass(); + } + + InterceptorCache.MethodKey methodKey = InterceptorCache.getMethodKey(targetClass, method); + + if (METHOD_INFO_CACHE.containsKey(methodKey)) { + MockMethodInfo methodInfo = METHOD_INFO_CACHE.get(methodKey); + return methodInfo.invokeMock(target, args); + } + + if (autoMockInterface && Modifier.isInterface(targetClass.getModifiers())) { + if (!("toString".equals(method.getName()) && args.length == 0)) { + LogKit.warn("Return null for Mock Method: \"" + ClassUtil.buildMethodString(method) + "\", " + + "Because the class \"" + targetClass.getName() + "\" is an interface and has no any implementation classes."); + } + return null; + } + + try { + return orginalHandler.invoke(target, thisMethod, method, args); + } catch (Exception ex) { + if ("initDao".equals(method.getName()) && JbootServiceBase.class == method.getDeclaringClass()) { + return null; + } else { + throw ex; + } + } + } +} diff --git a/src/main/java/io/jboot/test/MockMvc.java b/src/main/java/io/jboot/test/MockMvc.java new file mode 100644 index 0000000000000000000000000000000000000000..996546506640215e0eb4592e88935ceed13a54f8 --- /dev/null +++ b/src/main/java/io/jboot/test/MockMvc.java @@ -0,0 +1,324 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import io.jboot.components.http.HttpMimeTypes; +import io.jboot.test.web.MockHttpServletRequest; +import io.jboot.test.web.MockHttpServletResponse; +import io.jboot.test.web.MockServletInputStream; +import io.jboot.utils.StrUtil; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.Cookie; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.function.Consumer; + +public class MockMvc { + + protected boolean holdCookiesEnable = false; + protected Set holdCookies = new HashSet<>(); + + protected Consumer requestStartListener; + protected Consumer requestFinishedListener; + + public MockMvc() { + } + + public MockMvc(boolean holdCookiesEnable) { + this.holdCookiesEnable = holdCookiesEnable; + } + + public boolean isHoldCookiesEnable() { + return holdCookiesEnable; + } + + public void setHoldCookiesEnable(boolean holdCookiesEnable) { + this.holdCookiesEnable = holdCookiesEnable; + } + + public Set getHoldCookies() { + return holdCookies; + } + + public String getCookieValue(String name) { + for (Cookie holdCookie : holdCookies) { + if (holdCookie.getName().equals(name)) { + return holdCookie.getValue(); + } + } + return null; + } + + public void setHoldCookies(Set holdCookies) { + this.holdCookies = holdCookies; + } + + public Consumer getRequestStartListener() { + return requestStartListener; + } + + public void setRequestStartListener(Consumer requestStartListener) { + this.requestStartListener = requestStartListener; + } + + public Consumer getRequestFinishedListener() { + return requestFinishedListener; + } + + public void setRequestFinishedListener(Consumer requestFinishedListener) { + this.requestFinishedListener = requestFinishedListener; + } + + + public MockMvcResult get(String target) { + return get(target, null, null, null); + } + + + public MockMvcResult get(String target, Map paras) { + return get(target, paras, null, null); + } + + + public MockMvcResult get(String target, Map p, Set cookies) { + return get(target, p, null, cookies); + } + + + public MockMvcResult get(String target, Map p, Map headers) { + return get(target, p, headers, null); + } + + public MockMvcResult get(String target, Map p, Map headers, Set cookies) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + + final Map paras = p != null ? p : new HashMap<>(); + + + int indexOf = target.lastIndexOf("?"); + if (indexOf != -1) { + Map targetParas = StrUtil.queryStringToMap(target.substring(indexOf + 1)); + paras.putAll(targetParas); + target = target.substring(0, indexOf); + } + + request.setServletPath(target); + + if (headers != null) { + request.setHeaders(headers); + } + + paras.forEach(request::addParameter); + + if (cookies != null) { + request.setCookies(cookies); + } + + + return doMockRequest(request); + } + + public MockMvcResult post(String target) { + return post(target, null, null, null, null); + } + + public MockMvcResult post(String target, String postData) { + return post(target, null, null, null, postData); + } + + public MockMvcResult post(String target, Map paras) { + return post(target, paras, null, null, null); + } + + public MockMvcResult post(String target, Map paras, String postData) { + return post(target, paras, null, null, postData); + } + + public MockMvcResult post(String target, Map paras, Map headers) { + return post(target, paras, headers, null, null); + } + + public MockMvcResult post(String target, Map paras, Map headers, String postData) { + return post(target, paras, headers, null, postData); + } + + public MockMvcResult post(String target, Map paras, Map headers, Set cookies, String postData) { + MockServletInputStream inStream = null; + if (StrUtil.isNotBlank(postData)) { + inStream = new MockServletInputStream(postData); + } + return doPost(target, paras, headers, cookies, inStream); + } + + + public MockMvcResult upload(String target, Map paras, Map headers, Set cookies) { + + + String endFlag = "\r\n"; + String startFlag = "--"; + String boundary = "------" + StrUtil.uuid(); + + if (headers == null) { + headers = new HashMap<>(); + } + headers.put("Content-Type", "multipart/form-data; boundary=" + boundary); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if (paras != null) { + paras.forEach((key, value) -> { + if (value instanceof File) { + File file = (File) value; + writeString(baos, startFlag + boundary + endFlag); + writeString(baos, "Content-Disposition: form-data; name=\"" + key + "\"; filename=\"" + file.getName() + "\""); + writeString(baos, endFlag + "Content-Type: " + HttpMimeTypes.getMimeType(file.getName())); + writeString(baos, endFlag + endFlag); + writeFile(baos, file); + writeString(baos, endFlag); + } else { + writeString(baos, startFlag + boundary + endFlag); + writeString(baos, "Content-Disposition: form-data; name=\"" + key + "\""); + writeString(baos, endFlag + endFlag); + writeString(baos, String.valueOf(value)); + writeString(baos, endFlag); + } + }); + } + + writeString(baos, startFlag + boundary + startFlag + endFlag); + return doPost(target, paras, headers, cookies, new MockServletInputStream(baos.toByteArray())); + } + + private void writeFile(ByteArrayOutputStream dos, File file) { + FileInputStream fStream = null; + try { + fStream = new FileInputStream(file); + byte[] buffer = new byte[2028]; + for (int len = 0; (len = fStream.read(buffer)) > 0; ) { + dos.write(buffer, 0, len); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (fStream != null) { + try { + fStream.close(); + } catch (IOException e) { + } + } + } + + } + + private void writeString(OutputStream dos, String s) { + try { + dos.write(s.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + + public MockMvcResult doPost(String target, Map paras, Map headers, Set cookies, ServletInputStream inStream) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + + int indexOf = target.lastIndexOf("?"); + if (indexOf != -1) { + Map targetParas = StrUtil.queryStringToMap(target.substring(indexOf + 1)); + if (paras == null) { + paras = new HashMap<>(); + } + paras.putAll(targetParas); + target = target.substring(0, indexOf); + } + + request.setServletPath(target); + + if (headers != null) { + request.setHeaders(headers); + } + + if (paras != null) { + paras.forEach(request::addParameter); + } + + if (inStream != null) { + request.setInputStream(inStream); + } + + + if (cookies != null) { + request.setCookies(cookies); + } + + return doMockRequest(request); + } + + + public MockMvcResult doMockRequest(MockHttpServletRequest request) { + MockHttpServletResponse response = createResponse(); + try { + if (requestStartListener != null) { + requestStartListener.accept(request); + } + + Set cookies = new HashSet<>(); + + //开启 cookie 保持, 用户未设置自己的 cookie, 上次的 cookie 有值 + if (isHoldCookiesEnable()) { + cookies.addAll(holdCookies); + } + + for (Cookie cookie : request.getCookies()) { + doSetCookie(cookie, cookies); + } + + request.setCookies(cookies); + doSendRequest(request, response); + + } finally { + if (requestFinishedListener != null) { + requestFinishedListener.accept(response); + } + + if (isHoldCookiesEnable() && response.getCookies().size() > 0) { + response.getCookies().forEach(cookie -> doSetCookie(cookie, holdCookies)); + } + } + + return new MockMvcResult(response); + } + + public void doSetCookie(Cookie newCookie, Set toCookies) { + toCookies.removeIf(cookie -> Objects.equals(cookie.getName(), newCookie.getName())); + if (StrUtil.isNotEmpty(newCookie.getValue())) { + toCookies.add(newCookie); + } + } + + + public void doSendRequest(MockHttpServletRequest request, MockHttpServletResponse response) { + MockApp.mockRequest(request, response); + } + + public MockHttpServletResponse createResponse() { + return new MockHttpServletResponse(); + } + +} diff --git a/src/main/java/io/jboot/test/MockMvcAsserter.java b/src/main/java/io/jboot/test/MockMvcAsserter.java new file mode 100644 index 0000000000000000000000000000000000000000..d2421bfe04219afb935bb7d0ddc63733be33885d --- /dev/null +++ b/src/main/java/io/jboot/test/MockMvcAsserter.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +public interface MockMvcAsserter { + + void doAssert(MockMvcResult result); + +} diff --git a/src/main/java/io/jboot/test/MockMvcJsonAsserter.java b/src/main/java/io/jboot/test/MockMvcJsonAsserter.java new file mode 100644 index 0000000000000000000000000000000000000000..6aa66299ddf41043a85047d0d948754ea9d9d926 --- /dev/null +++ b/src/main/java/io/jboot/test/MockMvcJsonAsserter.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import com.alibaba.fastjson.JSONObject; + +public interface MockMvcJsonAsserter { + + void doAssert(JSONObject jsonObject); + +} diff --git a/src/main/java/io/jboot/test/MockMvcResult.java b/src/main/java/io/jboot/test/MockMvcResult.java new file mode 100644 index 0000000000000000000000000000000000000000..e7a1d51a98606bb5255108b8a24c4fc5dfae4628 --- /dev/null +++ b/src/main/java/io/jboot/test/MockMvcResult.java @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import io.jboot.test.web.MockHttpServletResponse; + +import javax.servlet.http.Cookie; +import java.util.*; + +public class MockMvcResult { + + final MockHttpServletResponse response; + private JSONObject jsonObject; + + public MockMvcResult(MockHttpServletResponse response) { + this.response = response; + } + + public String getContent() { + return response.getContentString(); + } + + public JSONObject getContentAsJSONObject() { + if (jsonObject == null) { + jsonObject = JSON.parseObject(getContent()); + } + return jsonObject; + } + + public String getContentType() { + return response.getContentType(); + } + + public int getStatus() { + return response.getStatus(); + } + + public String getHeader(String name) { + return response.getHeader(name); + } + + public Map getHeaders() { + Map headers = new HashMap<>(); + Collection headerNames = response.getHeaderNames(); + if (headerNames != null) { + for (String headerName : headerNames) { + headers.put(headerName, response.getHeader(headerName)); + } + } + return headers; + } + + public Set getCookies() { + return response.getCookies(); + } + + public String getCookie(String name) { + for (Cookie cookie : response.getCookies()) { + if (Objects.equals(name, cookie.getName())) { + return cookie.getValue(); + } + } + return null; + } + + + public MockHttpServletResponse getResponse() { + return response; + } + + public MockMvcResult printResult() { + System.out.println(this); + return this; + } + + public MockMvcResult printContent() { + System.out.println(getContent()); + return this; + } + + public MockMvcResult assertThat(MockMvcAsserter mockMvcAssert) { + mockMvcAssert.doAssert(this); + return this; + } + + public MockMvcResult assertJson(MockMvcJsonAsserter mockMvcAssert) { + mockMvcAssert.doAssert(this.getContentAsJSONObject()); + return this; + } + + + public MockMvcResult assertTrue(MockMvcTrueAsserter mockMvcTrueAsserter) { + return assertTrue(mockMvcTrueAsserter, "MockMvc result can not match the asserter."); + } + + + public MockMvcResult assertTrue(MockMvcTrueAsserter mockMvcTrueAsserter, String message) { + if (!mockMvcTrueAsserter.doAssert(this)) { + throw new AssertionError(message); + } + return this; + } + + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Response:\n"); + builder.append("Headers >>> ").append(getHeaders()).append("\n"); + builder.append("Status >>> ").append(getStatus()).append("\n"); + builder.append("Content >>> ").append(getContent()).append("\n"); + return builder.toString(); + } +} diff --git a/src/main/java/io/jboot/test/MockMvcTrueAsserter.java b/src/main/java/io/jboot/test/MockMvcTrueAsserter.java new file mode 100644 index 0000000000000000000000000000000000000000..0ea822f2ec836497bc2a2c6a6baaa1e73f097c12 --- /dev/null +++ b/src/main/java/io/jboot/test/MockMvcTrueAsserter.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +public interface MockMvcTrueAsserter { + + boolean doAssert(MockMvcResult result); + +} diff --git a/src/main/java/io/jboot/test/MockProxy.java b/src/main/java/io/jboot/test/MockProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..f5d9e35e9cc95e10b75ce019c8953f35a6163b5f --- /dev/null +++ b/src/main/java/io/jboot/test/MockProxy.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + + +import io.jboot.utils.ClassUtil; + +import java.lang.reflect.Proxy; + +public class MockProxy { + + public static T create(Class target) { + return (T) Proxy.newProxyInstance(target.getClassLoader(), new Class[]{target}, (proxy, method, args) -> { + throw new IllegalAccessException("Can not invoke this mock method: " + ClassUtil.buildMethodString(method)); + }); + } +} diff --git a/src/main/java/io/jboot/test/TestConfig.java b/src/main/java/io/jboot/test/TestConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..e720a18e8cb82cf9f6e326bd3ddaae83ec46a5a6 --- /dev/null +++ b/src/main/java/io/jboot/test/TestConfig.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface TestConfig { + + //webRootPath 配置 + String webRootPath() default MockApp.DEFAULT_WEB_ROOT_PATH; + + //class path 配置 + String classPath() default MockApp.DEFAULT_CLASS_PATH; + + //自动 Mock Interface 接口,有些接口没有实现类的,当执行其方法时,啥都不操作,若有返回值则返回 null + boolean autoMockInterface() default false; + + //配置 appMode + boolean devMode() default true; + + //配置启动是否打印 class 扫描信息 + boolean printScannerInfo() default false; + + //配置启动参数 + String[] launchArgs() default ""; + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/test/junit4/JbootRunner.java b/src/main/java/io/jboot/test/junit4/JbootRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..827ec80954c1cd17176f7580783c5316aff3c007 --- /dev/null +++ b/src/main/java/io/jboot/test/junit4/JbootRunner.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.junit4; + +import com.jfinal.aop.Aop; +import io.jboot.test.CPI; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; + +public class JbootRunner extends BlockJUnit4ClassRunner { + + public JbootRunner(Class klass) throws InitializationError { + super(klass); + } + + @Override + protected Object createTest() throws Exception { + Object ret = Aop.inject(super.createTest()); + CPI.setTestInstance(ret); + return ret; + } + + @Override + public void run(RunNotifier notifier) { + try { + CPI.startApp(getTestClass().getJavaClass()); + super.run(notifier); + } finally { + CPI.stopApp(); + } + } + + + @Override + protected Statement methodInvoker(FrameworkMethod method, Object test) { + return super.methodInvoker(method, test); + } + + +} diff --git a/src/main/java/io/jboot/test/junit5/JbootExtension.java b/src/main/java/io/jboot/test/junit5/JbootExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..d1441fad622deabd00099be0e5f943babdbeeefd --- /dev/null +++ b/src/main/java/io/jboot/test/junit5/JbootExtension.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.junit5; + +import com.jfinal.aop.Aop; +import io.jboot.test.CPI; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.util.Optional; + +public class JbootExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback { + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + Optional> classOptional = extensionContext.getTestClass(); + if (classOptional.isPresent()) { + CPI.startApp(classOptional.get()); + } + + } + + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + CPI.stopApp(); + } + + + @Override + public void beforeEach(ExtensionContext extensionContext) throws Exception { + Optional instantceOptional = extensionContext.getTestInstance(); + if (instantceOptional.isPresent()) { + Aop.inject(instantceOptional.get()); + CPI.setTestInstance(instantceOptional.get()); + } + } + + +} diff --git a/src/main/java/io/jboot/test/web/MockFilterChain.java b/src/main/java/io/jboot/test/web/MockFilterChain.java new file mode 100644 index 0000000000000000000000000000000000000000..55b30bd12c228bc290239ede20a159da2882c6a6 --- /dev/null +++ b/src/main/java/io/jboot/test/web/MockFilterChain.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.web; + + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; + +public class MockFilterChain implements FilterChain { + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException, ServletException { + //do nothing + } +} diff --git a/src/main/java/io/jboot/test/web/MockFilterConfig.java b/src/main/java/io/jboot/test/web/MockFilterConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..a9da3655ea1a17d5b05c9449da19ff970312323a --- /dev/null +++ b/src/main/java/io/jboot/test/web/MockFilterConfig.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.web; + +import io.jboot.core.JbootCoreConfig; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class MockFilterConfig implements FilterConfig { + + protected Map initParameters = new HashMap<>(); + protected ServletContext servletContext = MockServletContext.DEFAULT; + + public MockFilterConfig() { + initParameters.put("configClass", JbootCoreConfig.class.getName()); + } + + @Override + public String getFilterName() { + return "mockFilter"; + } + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + @Override + public String getInitParameter(String name) { + return initParameters.get(name); + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(initParameters.keySet()); + } +} diff --git a/src/main/java/io/jboot/test/web/MockHttpServletRequest.java b/src/main/java/io/jboot/test/web/MockHttpServletRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..10046ebdfebc187b427702f6886db1a3839153ff --- /dev/null +++ b/src/main/java/io/jboot/test/web/MockHttpServletRequest.java @@ -0,0 +1,733 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.web; + +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.jfinal.kit.LogKit; +import io.jboot.utils.StrUtil; + +import javax.servlet.*; +import javax.servlet.http.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.Principal; +import java.util.*; + +public class MockHttpServletRequest implements HttpServletRequest { + + protected String contextPath; + protected String method = "GET"; + protected String pathInfo; + protected String pathTranslated; + protected String queryString = ""; + protected String requestURI; + protected String servletPath; + protected String characterEncoding = "UTF-8"; + protected String protocol = "HTTP/1.1"; + + private String remoteAddr = "127.0.0.1"; + private String remoteHost = "localhost"; + private int remotePort = 80; + private String localName = "localhost"; + private String localAddr = "127.0.0.1"; + private int localPort = 80; + + protected String remoteUser; + protected String authType; + protected Principal userPrincipal; + + protected StringBuffer requestURL; + protected HttpSession session; + protected ServletInputStream inputStream; + + protected byte[] content; + + + protected ServletContext servletContext = MockServletContext.DEFAULT; + protected HttpServletResponse response; + + protected Map headers = new HashMap<>(); + protected Map attributeMap = new HashMap<>(); + protected Map parameters = new HashMap<>(); + protected Set cookies = new HashSet<>(); + protected LinkedList locales = new LinkedList<>(); + + protected boolean requestedSessionIdValid = true; + + protected boolean requestedSessionIdFromCookie = true; + + protected boolean requestedSessionIdFromURL = false; + + protected final Set userRoles = new HashSet<>(); + protected final Multimap parts = LinkedHashMultimap.create(); + + + public MockHttpServletRequest() { +// super(MockProxy.create(HttpServletRequest.class)); + } + + @Override + public String getContextPath() { + if (contextPath == null) { + contextPath = servletContext.getContextPath(); + } + return contextPath; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + + @Override + public String getHeader(String name) { + return headers.get(name.toLowerCase()); + } + + public void setHeaders(Map headers) { + if (headers != null) { + headers.forEach((s, s2) -> MockHttpServletRequest.this.headers.put(s.toLowerCase(), s2)); + } + } + + public void addHeader(String name, Object value) { + headers.put(name.toLowerCase(), value.toString()); + } + + @Override + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + + @Override + public String getPathInfo() { + return pathInfo; + } + + public void setPathInfo(String pathInfo) { + this.pathInfo = pathInfo; + } + + + @Override + public String getPathTranslated() { + return (this.pathInfo != null ? getRealPath(this.pathInfo) : null); + } + + + @Override + public String getQueryString() { + return queryString; + } + + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + + @Override + public String getRemoteUser() { + return remoteUser; + } + + + public void addUserRole(String role) { + this.userRoles.add(role); + } + + @Override + public boolean isUserInRole(String role) { + return userRoles.contains(role); + } + + public void setRemoteUser(String remoteUser) { + this.remoteUser = remoteUser; + } + + + @Override + public String getAuthType() { + return authType; + } + + public void setAuthType(String authType) { + this.authType = authType; + } + + @Override + public String getRequestURI() { + return requestURI; + } + + public void setRequestURI(String requestURI) { + this.requestURI = requestURI; + } + + + @Override + public StringBuffer getRequestURL() { + return requestURL; + } + + public void setRequestURL(StringBuffer requestURL) { + this.requestURL = requestURL; + } + + @Override + public String getRequestedSessionId() { + if (session != null) { + return session.getId(); + } + return null; + } + + + @Override + public String getServletPath() { + return servletPath; + } + + public void setServletPath(String servletPath) { + this.servletPath = servletPath; + if (requestURI == null) { + this.requestURI = getContextPath() + servletPath; + } + } + + + @Override + public HttpSession getSession() { + return getSession(true); + } + + + @Override + public HttpSession getSession(boolean create) { + if (session != null) { + return session; + } + + String sessionId = getCookieValue("jsessionId"); + if (sessionId != null) { + session = new MockHttpSession(sessionId, getServletContext()); + session.setMaxInactiveInterval(60 * 60); + } else if (create) { + sessionId = UUID.randomUUID().toString().replace("-", ""); + session = new MockHttpSession(sessionId, getServletContext()); + session.setMaxInactiveInterval(60 * 60); + setCookie("jsessionId", sessionId, -1); + } + return session; + } + + + @Override + public String changeSessionId() { + String sessionId = UUID.randomUUID().toString().replace("-", ""); + session = new MockHttpSession(sessionId, getServletContext()); + session.setMaxInactiveInterval(60 * 60); + setCookie("jsessionId", sessionId, -1); + return sessionId; + } + + /** + * Get cookie value by cookie name. + */ + private String getCookieValue(String name) { + Cookie cookie = getCookieObject(name); + return cookie != null ? cookie.getValue() : null; + } + + /** + * Get cookie object by cookie name. + */ + private Cookie getCookieObject(String name) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return cookie; + } + } + return null; + } + + /** + * @param name + * @param value + * @param maxAgeInSeconds + */ + private void setCookie(String name, String value, int maxAgeInSeconds) { + Cookie cookie = new Cookie(name, value); + cookie.setMaxAge(maxAgeInSeconds); + response.addCookie(cookie); + } + + + @Override + public Principal getUserPrincipal() { + return userPrincipal; + } + + public void setUserPrincipal(Principal userPrincipal) { + this.userPrincipal = userPrincipal; + } + + + @Override + public Object getAttribute(String key) { + return attributeMap.get(key); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(attributeMap.keySet()); + } + + + @Override + public String getCharacterEncoding() { + return characterEncoding; + } + + @Override + public int getContentLength() { + String cl = this.getHeader("content-length"); + if (cl != null) { + try { + return Integer.parseInt(cl); + } catch (NumberFormatException e) { + return 0; + } + } + + if (inputStream != null) { + try { + return inputStream.available(); + } catch (IOException e) { + return 0; + } + } + + return 0; + } + + @Override + public String getContentType() { + return this.getHeader("content-type"); + } + + + @Override + public ServletInputStream getInputStream() throws IOException { + if (inputStream == null) { + inputStream = new MockServletInputStream(""); + } + return inputStream; + } + + public void setInputStream(ServletInputStream ins) { + this.inputStream = ins; + } + + + public void setContent(byte[] content) { + this.content = content; + } + + + @Override + public long getContentLengthLong() { + return (this.content != null ? this.content.length : -1); + } + + + @Override + public String getParameter(String key) { + if (parameters.containsKey(key)) { + return parameters.get(key)[0]; + } + return null; + } + + public void addParameter(String key, Number num) { + addParameter(key, num.toString()); + } + + public void addParameter(String key, String value) { + addParameter(key, new String[]{value}); + } + + public void addParameter(String key, Object value) { + addParameter(key, new String[]{String.valueOf(value)}); + } + + + public void addParameter(String key, String[] values) { + parameters.put(key, values); + + if ("GET".equalsIgnoreCase(getMethod())) { + updateQueryString(); + } + } + + public void addQueryParameter(String key, Object value) { + + if ("GET".equalsIgnoreCase(getMethod())) { + parameters.put(key, new String[]{String.valueOf(value)}); + } + + Map queryStringMap = StrUtil.isNotBlank(queryString) ? StrUtil.queryStringToMap(this.queryString) : new HashMap(); + queryStringMap.put(key, value); + setQueryString(StrUtil.mapToQueryString(queryStringMap)); + } + + private void updateQueryString() { + StringBuilder sb = new StringBuilder(); + for (String key : parameters.keySet()) { + if (key == null || key.length() == 0) { + continue; + } + if (sb.length() > 0) { + sb.append("&"); + } + + sb.append(key.trim()).append("="); + String[] values = parameters.get(key); + if (values == null || values.length == 0) { + continue; + } + + if (values.length == 1) { + sb.append(StrUtil.urlEncode(values[0])); + } else { + for (int i = 0; i < values.length; i++) { + if (i == 0) { + sb.append(StrUtil.urlEncode(values[i])); + } else { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(key.trim()).append("=").append(StrUtil.urlEncode(values[i])); + } + } + } + } + + setQueryString(sb.toString()); + } + + @Override + public Map getParameterMap() { + return parameters; + } + + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(parameters.keySet()); + } + + @Override + public String[] getParameterValues(String name) { + return parameters.get(name); + } + + + @Override + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + @Override + public void removeAttribute(String key) { + attributeMap.remove(key); + } + + @Override + public void setAttribute(String key, Object value) { + attributeMap.put(key, value); + } + + @Override + public Cookie[] getCookies() { + return cookies.toArray(new Cookie[cookies.size()]); + } + + public void setCookies(Set cookies) { + this.cookies = cookies; + } + + @Override + public Enumeration getLocales() { + return Collections.enumeration(locales); + } + + public void setLocales(LinkedList locales) { + this.locales = locales; + } + + @Override + public void setCharacterEncoding(String characterEncoding) { + this.characterEncoding = characterEncoding; + } + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public String getServerName() { + return "localhost"; + } + + @Override + public int getServerPort() { + return 80; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + + public void setRemotePort(int remotePort) { + this.remotePort = remotePort; + } + + @Override + public int getRemotePort() { + return remotePort; + } + + public void setRemoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + @Override + public String getRemoteAddr() { + return remoteAddr; + } + + public void setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + } + + @Override + public String getRemoteHost() { + return remoteHost; + } + + @Override + public boolean isRequestedSessionIdFromURL() { + return requestedSessionIdFromURL; + } + + @Override + public boolean isRequestedSessionIdFromUrl() { + return requestedSessionIdFromURL; + } + + public void setRequestedSessionIdFromURL(boolean requestedSessionIdFromURL) { + this.requestedSessionIdFromURL = requestedSessionIdFromURL; + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + return false; + } + + @Override + public void login(String username, String password) throws ServletException { + LogKit.error("Unsupport login method!"); + } + + public void setRequestedSessionIdFromCookie(boolean requestedSessionIdFromCookie) { + this.requestedSessionIdFromCookie = requestedSessionIdFromCookie; + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + return requestedSessionIdFromCookie; + } + + public void setRequestedSessionIdValid(boolean requestedSessionIdValid) { + this.requestedSessionIdValid = requestedSessionIdValid; + } + + @Override + public boolean isRequestedSessionIdValid() { + return requestedSessionIdValid; + } + + @Override + public String getScheme() { + return "http"; + } + + @Override + public Locale getLocale() { + return this.locales.getFirst(); + } + + public void setLocalPort(int localPort) { + this.localPort = localPort; + } + + @Override + public int getLocalPort() { + return localPort; + } + + public void setLocalAddr(String localAddr) { + this.localAddr = localAddr; + } + + @Override + public String getLocalAddr() { + return localAddr; + } + + public void setLocalName(String localName) { + this.localName = localName; + } + + @Override + public String getLocalName() { + return localName; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return null; + } + + @Override + public String getRealPath(String path) { + return this.servletContext.getRealPath(path); + } + + @Override + public long getDateHeader(String name) { + return Long.valueOf(getHeader(name)); + } + + @Override + public DispatcherType getDispatcherType() { + return DispatcherType.REQUEST; + } + + @Override + public Enumeration getHeaders(String name) { + String header = getHeader(name); + String[] headers = header.split(";"); + return Collections.enumeration(Arrays.asList(headers)); + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + @Override + public int getIntHeader(String name) { + return Integer.valueOf(getHeader(name)); + } + + @Override + public void logout() throws ServletException { + this.userPrincipal = null; + this.remoteUser = null; + this.authType = null; + } + + public void addPart(Part part) { + this.parts.put(part.getName(), part); + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + final Collection parts = this.parts.get(name); + for (Part part : parts) { + return part; + } + return null; + } + + @Override + public Collection getParts() throws IOException, ServletException { + List result = new LinkedList<>(this.parts.values()); + return result; + } + + @Override + public T upgrade(Class handlerClass) throws IOException, ServletException { + LogKit.error("Unsupport upgrade method!"); + return null; + } + + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + public HttpServletResponse getResponse() { + return response; + } + + public void setResponse(HttpServletResponse response) { + this.response = response; + } +} diff --git a/src/main/java/io/jboot/test/web/MockHttpServletResponse.java b/src/main/java/io/jboot/test/web/MockHttpServletResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..fc237db157295bae3a78ab240c9877a4116f4cb2 --- /dev/null +++ b/src/main/java/io/jboot/test/web/MockHttpServletResponse.java @@ -0,0 +1,252 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.web; + +import com.jfinal.core.JFinal; +import io.jboot.test.MockProxy; + +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.*; +import java.util.*; + +public class MockHttpServletResponse extends HttpServletResponseWrapper { + + protected String contentString; + protected ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + protected PrintWriter writer; + protected Map headers = new HashMap<>(); + protected Set cookies = new HashSet<>(); + + protected int status = 200; + protected String statusMessage = "OK"; + protected Locale locale; + protected String contentType; + protected String characterEncoding = JFinal.me().getConstants().getEncoding(); + + + public MockHttpServletResponse() { + super(MockProxy.create(HttpServletResponse.class)); + } + + public MockHttpServletResponse(ByteArrayOutputStream stream) { + super(MockProxy.create(HttpServletResponse.class)); + this.stream = stream; + } + + public MockHttpServletResponse(HttpServletResponse response) { + super(response); + } + + public MockHttpServletResponse(HttpServletResponse response, ByteArrayOutputStream stream) { + super(response); + this.stream = stream; + } + + @Override + public void addCookie(Cookie cookie) { + cookies.add(cookie); + } + + public Set getCookies() { + return cookies; + } + + + @Override + public void addDateHeader(String key, long value) { + headers.put(key, "" + value); + } + + @Override + public void addHeader(String key, String value) { + headers.put(key, value); + } + + @Override + public void addIntHeader(String key, int value) { + headers.put(key, "" + value); + } + + @Override + public boolean containsHeader(String key) { + return headers.containsKey(key); + } + + @Override + public void sendError(int status) throws IOException { + this.setStatus(status); + } + + @Override + public void sendError(int status, String statusMessage) throws IOException { + this.setStatus(status, statusMessage); + } + + @Override + public void sendRedirect(String value) throws IOException { + if (status == 200) { + setStatus(302); + } + headers.put("Location", value); + } + + @Override + public void setDateHeader(String key, long value) { + headers.put(key, "" + value); + } + + @Override + public void setHeader(String key, String value) { + headers.put(key, value); + } + + @Override + public void setIntHeader(String key, int value) { + headers.put(key, "" + value); + } + + @Override + public void setStatus(int status) { + this.status = status; + } + + @Override + public void setStatus(int status, String statusMessage) { + this.status = status; + this.statusMessage = statusMessage; + } + + @Override + public void flushBuffer() throws IOException { + getWriter().flush(); + } + + @Override + public int getBufferSize() { + return stream.size(); + } + + @Override + public String getCharacterEncoding() { + return characterEncoding; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public Locale getLocale() { + return locale; + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + return new ServletOutputStream() { + + @Override + public void write(int arg0) throws IOException { + stream.write(arg0); + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + } + }; + } + + @Override + public PrintWriter getWriter() throws IOException { + if (writer == null) { + writer = new PrintWriter(new OutputStreamWriter(stream, characterEncoding)); + } + return writer; + } + + @Override + public void reset() { + stream.reset(); + } + + @Override + public void resetBuffer() { + stream.reset(); + } + + + @Override + public void setCharacterEncoding(String characterEncoding) { + this.characterEncoding = characterEncoding; + } + + @Override + public void setContentType(String contentType) { + this.contentType = contentType; + } + + @Override + public void setLocale(Locale locale) { + this.locale = locale; + } + + @Override + public int getStatus() { + return status; + } + + public String getStatusMessage() { + return statusMessage; + } + + public String getContentString() { + if (contentString == null) { + try { + getWriter().flush(); + contentString = stream.toString(characterEncoding); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return contentString; + } + + @Override + public String getHeader(String key) { + return headers.get(key); + } + + @Override + public Collection getHeaderNames() { + return headers.keySet(); + } + + @Override + public boolean isCommitted() { + return false; + } + +} diff --git a/src/main/java/io/jboot/test/web/MockHttpSession.java b/src/main/java/io/jboot/test/web/MockHttpSession.java new file mode 100644 index 0000000000000000000000000000000000000000..bbcdb297f3c8d097b42c2f4e7284fc234c5f11f9 --- /dev/null +++ b/src/main/java/io/jboot/test/web/MockHttpSession.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.web; + +import io.jboot.web.session.JbootHttpSession; + +import javax.servlet.ServletContext; +import java.util.HashMap; +import java.util.Map; + +public class MockHttpSession extends JbootHttpSession { + + protected static Map> storeCache = new HashMap<>(); + + public MockHttpSession(String id, ServletContext servletContext) { + super(id, servletContext, createSessionStore(id), null); + } + + private static Map createSessionStore(String sessionId) { + Map store = storeCache.get(sessionId); + if (store == null) { + store = new HashMap<>(); + storeCache.put(sessionId, store); + } + return store; + } + +} diff --git a/src/main/java/io/jboot/test/web/MockJFinalFilter.java b/src/main/java/io/jboot/test/web/MockJFinalFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..d4c0b79d7b8c3d11822bb17ca87873fb5e63c5a9 --- /dev/null +++ b/src/main/java/io/jboot/test/web/MockJFinalFilter.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.web; + +import com.jfinal.core.JFinalFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MockJFinalFilter extends JFinalFilter { + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest)req; + HttpServletResponse response = (HttpServletResponse)res; + request.setCharacterEncoding(encoding); + + String target = request.getRequestURI(); + if (contextPathLength != 0) { + target = target.substring(contextPathLength); + } + + boolean[] isHandled = {false}; + try { + handler.handle(target, request, response, isHandled); + } + catch (Exception e) { + if (log.isErrorEnabled()) { + String qs = request.getQueryString(); + log.error(qs == null ? target : target + "?" + qs, e); + } + throw new AssertionError(e.getMessage(),e); + } + + if (isHandled[0] == false) { + // 默认拒绝直接访问 jsp 文件,加固 tomcat、jetty 安全性 +// if (constants.getDenyAccessJsp() && isJsp(target)) { +// com.jfinal.kit.HandlerKit.renderError404(request, response, isHandled); +// return ; +// } + + chain.doFilter(request, response); + } + } +} diff --git a/src/main/java/io/jboot/test/web/MockServletContext.java b/src/main/java/io/jboot/test/web/MockServletContext.java new file mode 100644 index 0000000000000000000000000000000000000000..32f624259a982e7c2462ed5a1db09f2636df1863 --- /dev/null +++ b/src/main/java/io/jboot/test/web/MockServletContext.java @@ -0,0 +1,355 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.web; + +import com.jfinal.kit.PathKit; +import io.jboot.components.http.HttpMimeTypes; +import io.jboot.test.MockExceptions; + +import javax.servlet.*; +import javax.servlet.descriptor.JspConfigDescriptor; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; + +public class MockServletContext implements ServletContext { + public static MockServletContext DEFAULT = new MockServletContext(); + + protected Map initParameters = new HashMap<>(); + protected Map attributes = new HashMap<>(); + + protected String contextPath = ""; + protected String servletContextName = ""; + + @Override + public String getContextPath() { + return contextPath; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + @Override + public ServletContext getContext(String uripath) { + throw MockExceptions.unsupported; + } + + @Override + public int getMajorVersion() { + return 0; + } + + @Override + public int getMinorVersion() { + return 0; + } + + @Override + public int getEffectiveMajorVersion() { + return 0; + } + + @Override + public int getEffectiveMinorVersion() { + return 0; + } + + @Override + public String getMimeType(String file) { + return HttpMimeTypes.getMimeType(file); + } + + @Override + public Set getResourcePaths(String path) { + try { + HashSet hashSet = new HashSet<>(); + Enumeration enumeration = getClass().getClassLoader().getResources(path); + while (enumeration.hasMoreElements()) { + URL url = enumeration.nextElement(); + hashSet.add(url.toString()); + } + return hashSet; + } catch (IOException e) { + return null; + } + } + + @Override + public URL getResource(String path) throws MalformedURLException { + return getClass().getResource(path); + } + + @Override + public InputStream getResourceAsStream(String path) { + return getClass().getResourceAsStream(path); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + throw MockExceptions.unsupported; + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + throw MockExceptions.unsupported; + } + + @Override + public Servlet getServlet(String name) throws ServletException { + throw MockExceptions.unsupported; + } + + @Override + public Enumeration getServlets() { + throw MockExceptions.unsupported; + } + + @Override + public Enumeration getServletNames() { + throw MockExceptions.unsupported; + } + + @Override + public void log(String msg) { + System.out.println(msg); + } + + @Override + public void log(Exception exception, String msg) { + System.out.println(msg); + exception.printStackTrace(); + } + + @Override + public void log(String message, Throwable throwable) { + System.out.println(message); + throwable.printStackTrace(); + } + + + @Override + public String getRealPath(String path) { + return new File(PathKit.getWebRootPath(), path).getAbsolutePath(); + } + + @Override + public String getServerInfo() { + return "Jboot Mock Server 1.0"; + } + + @Override + public String getInitParameter(String name) { + return initParameters.get(name); + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(initParameters.keySet()); + } + + @Override + public boolean setInitParameter(String name, String value) { + return null != initParameters.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(attributes.keySet()); + } + + @Override + public void setAttribute(String name, Object object) { + attributes.put(name, object); + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + + @Override + public String getServletContextName() { + return servletContextName; + } + + public void setServletContextName(String servletContextName) { + this.servletContextName = servletContextName; + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, String className) { + throw MockExceptions.unsupported; + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) { + throw MockExceptions.unsupported; + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Class servletClass) { + throw MockExceptions.unsupported; + } + + @Override + public ServletRegistration.Dynamic addJspFile(String servletName, String jspFile) { + throw MockExceptions.unsupported; + } + + @Override + public T createServlet(Class clazz) throws ServletException { + throw MockExceptions.unsupported; + } + + @Override + public ServletRegistration getServletRegistration(String servletName) { + throw MockExceptions.unsupported; + } + + @Override + public Map getServletRegistrations() { + throw MockExceptions.unsupported; + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, String className) { + throw MockExceptions.unsupported; + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { + throw MockExceptions.unsupported; + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Class filterClass) { + throw MockExceptions.unsupported; + } + + @Override + public T createFilter(Class clazz) throws ServletException { + throw MockExceptions.unsupported; + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + throw MockExceptions.unsupported; + } + + @Override + public Map getFilterRegistrations() { + throw MockExceptions.unsupported; + } + + @Override + public SessionCookieConfig getSessionCookieConfig() { + return null; + } + + @Override + public void setSessionTrackingModes(Set sessionTrackingModes) { + + } + + @Override + public Set getDefaultSessionTrackingModes() { + return null; + } + + @Override + public Set getEffectiveSessionTrackingModes() { + return null; + } + + @Override + public void addListener(String className) { + + } + + @Override + public void addListener(T t) { + + } + + @Override + public void addListener(Class listenerClass) { + + } + + @Override + public T createListener(Class clazz) throws ServletException { + return null; + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return null; + } + + @Override + public ClassLoader getClassLoader() { + return null; + } + + @Override + public void declareRoles(String... roleNames) { + + } + + @Override + public String getVirtualServerName() { + return null; + } + + @Override + public int getSessionTimeout() { + return 0; + } + + @Override + public void setSessionTimeout(int sessionTimeout) { + + } + + @Override + public String getRequestCharacterEncoding() { + return null; + } + + @Override + public void setRequestCharacterEncoding(String encoding) { + + } + + @Override + public String getResponseCharacterEncoding() { + return null; + } + + @Override + public void setResponseCharacterEncoding(String encoding) { + + } +} diff --git a/src/main/java/io/jboot/test/web/MockServletInputStream.java b/src/main/java/io/jboot/test/web/MockServletInputStream.java new file mode 100644 index 0000000000000000000000000000000000000000..4330982eab6b4599f6ad219c136ea3be164f294c --- /dev/null +++ b/src/main/java/io/jboot/test/web/MockServletInputStream.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.web; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * 参考:https://stackoverflow.com/questions/30484388/inputstream-to-servletinputstream + */ +public class MockServletInputStream extends ServletInputStream { + + final byte[] myBytes; + + private int lastIndexRetrieved = -1; + private ReadListener readListener = null; + private int readLimit = -1; + private int markedPosition = -1; + + public MockServletInputStream(byte[] myBytes) { + this.myBytes = myBytes; + } + + public MockServletInputStream(String content) { + myBytes = content.getBytes(StandardCharsets.UTF_8); + } + + @Override + public boolean isFinished() { + return (lastIndexRetrieved == myBytes.length - 1); + } + + @Override + public boolean isReady() { + return isFinished(); + } + + @Override + public int available() throws IOException { + return (myBytes.length - lastIndexRetrieved - 1); + } + + @Override + public void close() throws IOException { + lastIndexRetrieved = myBytes.length - 1; + } + + @Override + public void setReadListener(ReadListener readListener) { + this.readListener = readListener; + if (!isFinished()) { + try { + readListener.onDataAvailable(); + } catch (IOException e) { + readListener.onError(e); + } + } else { + try { + readListener.onAllDataRead(); + } catch (IOException e) { + readListener.onError(e); + } + } + } + + + @Override + public boolean markSupported() { + return true; + } + + @Override + public synchronized void mark(int readLimit) { + this.readLimit = readLimit; + this.markedPosition = lastIndexRetrieved; + } + + @Override + public synchronized void reset() throws IOException { + if (markedPosition == -1) { + throw new IOException("No mark found"); + } else { + lastIndexRetrieved = markedPosition; + readLimit = -1; + } + } + + // Replacement of earlier read method to cope with readLimit + @Override + public int read() throws IOException { + int i; + if (!isFinished()) { + i = myBytes[lastIndexRetrieved + 1]; + lastIndexRetrieved++; + if (isFinished() && (readListener != null)) { + try { + readListener.onAllDataRead(); + } catch (IOException ex) { + readListener.onError(ex); + throw ex; + } + readLimit = -1; + } + if (readLimit != -1) { + if ((lastIndexRetrieved - markedPosition) > readLimit) { + // This part is actually not necessary in our implementation + // as we are not storing any data. However we need to respect + // the contract. + markedPosition = -1; + readLimit = -1; + } + } + return i; + } else { + return -1; + } + } +} diff --git a/src/main/java/io/jboot/utils/AnnotationUtil.java b/src/main/java/io/jboot/utils/AnnotationUtil.java index 7bdd9a089b24b7d26ba6e5ee86da093b8dd2e6c9..1ab48c8ae95e27d1ce6caa5c6e77f4e58abc11f5 100644 --- a/src/main/java/io/jboot/utils/AnnotationUtil.java +++ b/src/main/java/io/jboot/utils/AnnotationUtil.java @@ -1,90 +1,83 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jboot.utils; -import io.jboot.app.config.JbootConfigManager; +import io.jboot.app.config.JbootConfigKit; public class AnnotationUtil { - private static final String EXPR_PREFIX = "${"; - private static final String EXPR_SUFFIX = "}"; - - public static String get(String value) { - if (StrUtil.isBlank(value)) { - return null; - } else { - value = value.trim(); - } + return get(value, StrUtil.EMPTY); + } - if (value.startsWith(EXPR_PREFIX) && value.endsWith(EXPR_SUFFIX)) { - String key = value.substring(2, value.length() - 1); - return getConfigValueByKeyString(key); + public static String get(String value, String defaultValue) { + if (StrUtil.isNotBlank(value)) { + String ret = JbootConfigKit.parseValue(value.trim()); + if (StrUtil.isNotBlank(ret)) { + return ret; + } } - - return value; + return defaultValue; } + public static String[] get(String[] value) { + if (ArrayUtil.isNullOrEmpty(value)) { + return null; + } - public static String getConfigValueByKeyString(String key) { - int indexOf = key.indexOf(":"); - String defaultValue = null; - if (indexOf != -1) { - defaultValue = key.substring(indexOf + 1); - key = key.substring(0, indexOf); + String[] rets = new String[value.length]; + for (int i = 0; i < rets.length; i++) { + rets[i] = get(value[i]); } - String configValue = JbootConfigManager.me().getConfigValue(key.trim()); - String returnValue = StrUtil.obtainDefaultIfBlank(configValue, defaultValue); - return StrUtil.isBlank(returnValue) ? null : returnValue.trim(); + return rets; } + public static Integer getInt(String value) { String intValue = get(value); - if (intValue == null) return null; - return Integer.valueOf(intValue); + return intValue == null ? null : Integer.parseInt(intValue); } public static Integer getInt(String value, int defaultValue) { String intValue = get(value); - if (intValue == null) return defaultValue; - return Integer.valueOf(intValue); + return intValue == null ? defaultValue : Integer.parseInt(intValue); } public static Long getLong(String value) { String longValue = get(value); - if (longValue == null) return null; - return Long.valueOf(longValue); + return longValue == null ? null : Long.parseLong(longValue); } public static Long getLong(String value, long defaultValue) { String longValue = get(value); - if (longValue == null) return defaultValue; - return Long.valueOf(longValue); + return longValue == null ? defaultValue : Long.parseLong(longValue); } public static Boolean getBool(String value) { String boolValue = get(value); - if (boolValue == null) return null; - return Boolean.valueOf(boolValue); + return boolValue == null ? null : Boolean.parseBoolean(boolValue); } public static Boolean getBool(String value, boolean defaultValue) { String boolValue = get(value); - if (boolValue == null) return defaultValue; - return Boolean.valueOf(boolValue); + return boolValue == null ? defaultValue : Boolean.parseBoolean(boolValue); } - public static String[] get(String[] value) { - if (ArrayUtil.isNullOrEmpty(value)) { - return null; - } - - String[] rets = new String[value.length]; - for (int i = 0; i < rets.length; i++) { - rets[i] = get(value[i]); - } - return rets; - } } diff --git a/src/main/java/io/jboot/utils/AntPathMatcher.java b/src/main/java/io/jboot/utils/AntPathMatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..c82d0124ab68fa65a6158825b4d929e2765330c2 --- /dev/null +++ b/src/main/java/io/jboot/utils/AntPathMatcher.java @@ -0,0 +1,849 @@ +package io.jboot.utils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class AntPathMatcher { + + /** + * Default path separator: "/". + */ + public static final String DEFAULT_PATH_SEPARATOR = "/"; + + private static final int CACHE_TURNOFF_THRESHOLD = 65536; + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?}"); + + private static final char[] WILDCARD_CHARS = {'*', '?', '{'}; + + + private String pathSeparator; + + private PathSeparatorPatternCache pathSeparatorPatternCache; + + private boolean caseSensitive = true; + + private boolean trimTokens = false; + + + private volatile Boolean cachePatterns; + + private final Map tokenizedPatternCache = new ConcurrentHashMap<>(256); + + final Map stringMatcherCache = new ConcurrentHashMap<>(256); + + + /** + * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. + */ + public AntPathMatcher() { + this.pathSeparator = DEFAULT_PATH_SEPARATOR; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR); + } + + /** + * A convenient, alternative constructor to use with a custom path separator. + * + * @param pathSeparator the path separator to use, must not be {@code null}. + */ + public AntPathMatcher(String pathSeparator) { + this.pathSeparator = pathSeparator; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); + } + + + /** + * Set the path separator to use for pattern parsing. + *

Default is "/", as in Ant. + */ + public void setPathSeparator(String pathSeparator) { + this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); + } + + /** + * Specify whether to perform pattern matching in a case-sensitive fashion. + *

Default is {@code true}. Switch this to {@code false} for case-insensitive matching. * + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + /** + * Specify whether to trim tokenized paths and patterns. + *

Default is {@code false}. + */ + public void setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + } + + /** + * Specify whether to cache parsed pattern metadata for patterns passed + * into this matcher's {@link #match} method. A value of {@code true} + * activates an unlimited pattern cache; a value of {@code false} turns + * the pattern cache off completely. + *

Default is for the cache to be on, but with the variant to automatically + * turn it off when encountering too many patterns to cache at runtime + * (the threshold is 65536), assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + * + * @see #getStringMatcher(String) + */ + public void setCachePatterns(boolean cachePatterns) { + this.cachePatterns = cachePatterns; + } + + private void deactivatePatternCache() { + this.cachePatterns = false; + this.tokenizedPatternCache.clear(); + this.stringMatcherCache.clear(); + } + + + public boolean isPattern(String path) { + if (path == null) { + return false; + } + boolean uriVar = false; + for (int i = 0; i < path.length(); i++) { + char c = path.charAt(i); + if (c == '*' || c == '?') { + return true; + } + if (c == '{') { + uriVar = true; + continue; + } + if (c == '}' && uriVar) { + return true; + } + } + return false; + } + + + public boolean match(String pattern, String path) { + return doMatch(pattern, path, true, null); + } + + + public boolean matchStart(String pattern, String path) { + return doMatch(pattern, path, false, null); + } + + /** + * Actually match the given {@code path} against the given {@code pattern}. + * + * @param pattern the pattern to match against + * @param path the path to test + * @param fullMatch whether a full pattern match is required (else a pattern match + * as far as the given base path goes is sufficient) + * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't + */ + protected boolean doMatch(String pattern, String path, boolean fullMatch, + Map uriTemplateVariables) { + + if (path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pattDirs = tokenizePattern(pattern); + if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { + return false; + } + + String[] pathDirs = tokenizePath(path); + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxStart]; + if ("**".equals(pattDir)) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxEnd]; + if (pattDir.equals("**")) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = pattDirs[pattIdxStart + j + 1]; + String subStr = pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr, uriTemplateVariables)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + private boolean isPotentialMatch(String path, String[] pattDirs) { + if (!this.trimTokens) { + int pos = 0; + for (String pattDir : pattDirs) { + int skipped = skipSeparator(path, pos, this.pathSeparator); + pos += skipped; + skipped = skipSegment(path, pos, pattDir); + if (skipped < pattDir.length()) { + return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0)))); + } + pos += skipped; + } + } + return true; + } + + private int skipSegment(String path, int pos, String prefix) { + int skipped = 0; + for (int i = 0; i < prefix.length(); i++) { + char c = prefix.charAt(i); + if (isWildcardChar(c)) { + return skipped; + } + int currPos = pos + skipped; + if (currPos >= path.length()) { + return 0; + } + if (c == path.charAt(currPos)) { + skipped++; + } + } + return skipped; + } + + private int skipSeparator(String path, int pos, String separator) { + int skipped = 0; + while (path.startsWith(separator, pos + skipped)) { + skipped += separator.length(); + } + return skipped; + } + + private boolean isWildcardChar(char c) { + for (char candidate : WILDCARD_CHARS) { + if (c == candidate) { + return true; + } + } + return false; + } + + /** + * Tokenize the given path pattern into parts, based on this matcher's settings. + *

Performs caching based on {@link #setCachePatterns}, delegating to + * {@link #tokenizePath(String)} for the actual tokenization algorithm. + * + * @param pattern the pattern to tokenize + * @return the tokenized pattern parts + */ + protected String[] tokenizePattern(String pattern) { + String[] tokenized = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + tokenized = this.tokenizedPatternCache.get(pattern); + } + if (tokenized == null) { + tokenized = tokenizePath(pattern); + if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return tokenized; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.tokenizedPatternCache.put(pattern, tokenized); + } + } + return tokenized; + } + + /** + * Tokenize the given path into parts, based on this matcher's settings. + * + * @param path the path to tokenize + * @return the tokenized path parts + */ + protected String[] tokenizePath(String path) { + return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + } + + /** + * Test whether or not a string matches against a pattern. + * + * @param pattern the pattern to match against (never {@code null}) + * @param str the String which must be matched against the pattern (never {@code null}) + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise + */ + private boolean matchStrings(String pattern, String str, + Map uriTemplateVariables) { + + return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); + } + + /** + * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. + *

The default implementation checks this AntPathMatcher's internal cache + * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance + * if no cached copy is found. + *

When encountering too many patterns to cache at runtime (the threshold is 65536), + * it turns the default cache off, assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + *

This method may be overridden to implement a custom cache strategy. + * + * @param pattern the pattern to match against (never {@code null}) + * @return a corresponding AntPathStringMatcher (never {@code null}) + * @see #setCachePatterns + */ + protected AntPathStringMatcher getStringMatcher(String pattern) { + AntPathStringMatcher matcher = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + matcher = this.stringMatcherCache.get(pattern); + } + if (matcher == null) { + matcher = new AntPathStringMatcher(pattern, this.caseSensitive); + if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return matcher; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.stringMatcherCache.put(pattern, matcher); + } + } + return matcher; + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part.

For example:

    + *
  • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''
  • + *
  • '{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
  • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'
  • + *
  • '{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
  • '{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'
  • + *
  • '{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'
  • + *
  • '{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
  • + *
  • '{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
+ *

Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but + * does not enforce this. + */ + + public String extractPathWithinPattern(String pattern, String path) { + if (pattern == null){ + return ""; + } + String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); + String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + StringBuilder builder = new StringBuilder(); + boolean pathStarted = false; + + for (int segment = 0; segment < patternParts.length; segment++) { + String patternPart = patternParts[segment]; + if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { + for (; segment < pathParts.length; segment++) { + if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { + builder.append(this.pathSeparator); + } + builder.append(pathParts[segment]); + pathStarted = true; + } + } + } + + return builder.toString(); + } + + + public Map extractUriTemplateVariables(String pattern, String path) { + Map variables = new LinkedHashMap<>(); + boolean result = doMatch(pattern, path, true, variables); + if (!result) { + throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + } + return variables; + } + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of + * explicitness. + *

This {@code Comparator} will {@linkplain List#sort(Comparator) sort} + * a list so that more specific patterns (without URI templates or wild cards) come before + * generic patterns. So given a list with the following patterns, the returned comparator + * will sort this list so that the order will be as indicated. + *

    + *
  1. {@code /hotels/new}
  2. + *
  3. {@code /hotels/{hotel}}
  4. + *
  5. {@code /hotels/*}
  6. + *
+ *

The full path given as parameter is used to test for exact matches. So when the given path + * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. + * + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + + public Comparator getPatternComparator(String path) { + return new AntPatternComparator(path); + } + + + /** + * Tests whether or not a string matches against a pattern via a {@link Pattern}. + *

The pattern may contain special characters: '*' means zero or more characters; '?' means one and + * only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}. + */ + protected static class AntPathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "((?s).*)"; + + private final String rawPattern; + + private final boolean caseSensitive; + + private final boolean exactMatch; + + + private final Pattern pattern; + + private final List variableNames = new ArrayList<>(); + + public AntPathStringMatcher(String pattern) { + this(pattern, true); + } + + public AntPathStringMatcher(String pattern, boolean caseSensitive) { + this.rawPattern = pattern; + this.caseSensitive = caseSensitive; + StringBuilder patternBuilder = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(pattern, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } else if ("*".equals(match)) { + patternBuilder.append(".*"); + } else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(matcher.group(1)); + } else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + // No glob pattern was found, this is an exact String match + if (end == 0) { + this.exactMatch = true; + this.pattern = null; + } else { + this.exactMatch = false; + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = (this.caseSensitive ? Pattern.compile(patternBuilder.toString()) : + Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); + } + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + /** + * Main entry point. + * + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. + */ + public boolean matchStrings(String str, Map uriTemplateVariables) { + if (this.exactMatch) { + return this.caseSensitive ? this.rawPattern.equals(str) : this.rawPattern.equalsIgnoreCase(str); + } else if (this.pattern != null) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (uriTemplateVariables != null) { + if (this.variableNames.size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = matcher.group(i); + uriTemplateVariables.put(name, value); + } + } + return true; + } + } + return false; + } + + } + + + /** + * The default {@link Comparator} implementation returned by + * {@link #getPatternComparator(String)}. + *

In order, the most "generic" pattern is determined by the following: + *

    + *
  • if it's null or a capture all pattern (i.e. it is equal to "/**")
  • + *
  • if the other pattern is an actual match
  • + *
  • if it's a catch-all pattern (i.e. it ends with "**"
  • + *
  • if it's got more "*" than the other pattern
  • + *
  • if it's got more "{foo}" than the other pattern
  • + *
  • if it's shorter than the other pattern
  • + *
+ */ + protected static class AntPatternComparator implements Comparator { + + private final String path; + + public AntPatternComparator(String path) { + this.path = path; + } + + /** + * Compare two patterns to determine which should match first, i.e. which + * is the most specific regarding the current path. + * + * @return a negative integer, zero, or a positive integer as pattern1 is + * more specific, equally specific, or less specific than pattern2. + */ + + public int compare(String pattern1, String pattern2) { + PatternInfo info1 = new PatternInfo(pattern1); + PatternInfo info2 = new PatternInfo(pattern2); + + if (info1.isLeastSpecific() && info2.isLeastSpecific()) { + return 0; + } else if (info1.isLeastSpecific()) { + return 1; + } else if (info2.isLeastSpecific()) { + return -1; + } + + boolean pattern1EqualsPath = pattern1.equals(this.path); + boolean pattern2EqualsPath = pattern2.equals(this.path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } else if (pattern1EqualsPath) { + return -1; + } else if (pattern2EqualsPath) { + return 1; + } + + if (info1.isPrefixPattern() && info2.isPrefixPattern()) { + return info2.getLength() - info1.getLength(); + } else if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { + return 1; + } else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { + return -1; + } + + if (info1.getTotalCount() != info2.getTotalCount()) { + return info1.getTotalCount() - info2.getTotalCount(); + } + + if (info1.getLength() != info2.getLength()) { + return info2.getLength() - info1.getLength(); + } + + if (info1.getSingleWildcards() < info2.getSingleWildcards()) { + return -1; + } else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { + return 1; + } + + if (info1.getUriVars() < info2.getUriVars()) { + return -1; + } else if (info2.getUriVars() < info1.getUriVars()) { + return 1; + } + + return 0; + } + + + /** + * Value class that holds information about the pattern, e.g. number of + * occurrences of "*", "**", and "{" pattern elements. + */ + private static class PatternInfo { + + + private final String pattern; + + private int uriVars; + + private int singleWildcards; + + private int doubleWildcards; + + private boolean catchAllPattern; + + private boolean prefixPattern; + + + private Integer length; + + public PatternInfo(String pattern) { + this.pattern = pattern; + if (this.pattern != null) { + initCounters(); + this.catchAllPattern = this.pattern.equals("/**"); + this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); + } + if (this.uriVars == 0) { + this.length = (this.pattern != null ? this.pattern.length() : 0); + } + } + + protected void initCounters() { + int pos = 0; + if (this.pattern != null) { + while (pos < this.pattern.length()) { + if (this.pattern.charAt(pos) == '{') { + this.uriVars++; + pos++; + } else if (this.pattern.charAt(pos) == '*') { + if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { + this.doubleWildcards++; + pos += 2; + } else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { + this.singleWildcards++; + pos++; + } else { + pos++; + } + } else { + pos++; + } + } + } + } + + public int getUriVars() { + return this.uriVars; + } + + public int getSingleWildcards() { + return this.singleWildcards; + } + + public int getDoubleWildcards() { + return this.doubleWildcards; + } + + public boolean isLeastSpecific() { + return (this.pattern == null || this.catchAllPattern); + } + + public boolean isPrefixPattern() { + return this.prefixPattern; + } + + public int getTotalCount() { + return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); + } + + /** + * Returns the length of the given pattern, where template variables are considered to be 1 long. + */ + public int getLength() { + if (this.length == null) { + this.length = (this.pattern != null ? + VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length() : 0); + } + return this.length; + } + } + } + + + /** + * A simple cache for patterns that depend on the configured path separator. + */ + private static class PathSeparatorPatternCache { + + private final String endsOnWildCard; + + private final String endsOnDoubleWildCard; + + public PathSeparatorPatternCache(String pathSeparator) { + this.endsOnWildCard = pathSeparator + "*"; + this.endsOnDoubleWildCard = pathSeparator + "**"; + } + + public String getEndsOnWildCard() { + return this.endsOnWildCard; + } + + public String getEndsOnDoubleWildCard() { + return this.endsOnDoubleWildCard; + } + } + +} + +abstract class StringUtils { + + private static final String[] EMPTY_STRING_ARRAY = {}; + + private static final String FOLDER_SEPARATOR = "/"; + + private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + + private static final String TOP_PATH = ".."; + + private static final String CURRENT_PATH = "."; + + private static final char EXTENSION_SEPARATOR = '.'; + + + public static String[] tokenizeToStringArray(String str, String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + public static String[] tokenizeToStringArray( + String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + + if (str == null) { + return EMPTY_STRING_ARRAY; + } + + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList<>(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * Copy the given {@link Collection} into a {@code String} array. + *

The {@code Collection} must contain {@code String} elements only. + * + * @param collection the {@code Collection} to copy + * (potentially {@code null} or empty) + * @return the resulting {@code String} array + */ + public static String[] toStringArray(Collection collection) { + return ((collection != null && collection.size() != 0) ? collection.toArray(EMPTY_STRING_ARRAY) : EMPTY_STRING_ARRAY); + } + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/utils/ArrayUtil.java b/src/main/java/io/jboot/utils/ArrayUtil.java index c7343ea3a9dc8f568a0fc1bb28ef239d33193e86..c859f1c019ae71db4289ca85d0d4d4d1a6da9bf1 100644 --- a/src/main/java/io/jboot/utils/ArrayUtil.java +++ b/src/main/java/io/jboot/utils/ArrayUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,12 @@ */ package io.jboot.utils; -import java.util.Arrays; -import java.util.Collection; -import java.util.Map; +import java.util.*; public class ArrayUtil { - public static boolean isNotEmpty(Collection list) { + public static boolean isNotEmpty(Collection list) { return list != null && list.size() > 0; } @@ -34,7 +32,7 @@ public class ArrayUtil { return objects != null && objects.length > 0; } - public static boolean isNullOrEmpty(Collection list) { + public static boolean isNullOrEmpty(Collection list) { return list == null || list.size() == 0; } @@ -60,4 +58,44 @@ public class ArrayUtil { return result; } + public static boolean contains(T[] array, T element) { + if (array == null || array.length == 0) { + return false; + } + + for (T t : array) { + if (Objects.equals(t, element)) { + return true; + } + } + return false; + } + + public static boolean isSameElements(Collection c1, Collection c2) { + if (c1 == c2) { + return true; + } + + if ((c1 == null || c1.isEmpty()) && (c2 == null || c2.isEmpty())) { + return true; + } + + if (c1 != null && c2 != null && c1.size() == c2.size() && c1.containsAll(c2)) { + return true; + } + + return false; + } + + + public static String toString(Object[] strings, String delimiter) { + StringJoiner sb = new StringJoiner(delimiter); + if (strings != null) { + for (Object o : strings) { + sb.add(String.valueOf(o)); + } + } + return sb.toString(); + } + } diff --git a/src/main/java/io/jboot/utils/CacheUtil.java b/src/main/java/io/jboot/utils/CacheUtil.java index c3585ceb6b8bf5a2592b094d76d08378d1f7f99b..28737c3884e58d8a96892dda00582af590f31d6d 100644 --- a/src/main/java/io/jboot/utils/CacheUtil.java +++ b/src/main/java/io/jboot/utils/CacheUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,12 +25,20 @@ import java.util.List; /** * Usage: * 1、CacheUtil.get("cacheName","key") - * 2、CacheUtil.use("redis").get("cacheName","key") + * 2、CacheUtil.use("default").get("cacheName","key") */ public class CacheUtil { - public static JbootCache use(String type) { - return JbootCacheManager.me().getCache(type); + public static JbootCache use(String name) { + return JbootCacheManager.me().getCache(name); + } + + public static JbootCache setThreadCacheNamePrefix(String cacheNamePrefix) { + return JbootCacheManager.me().getCache().setThreadCacheNamePrefix(cacheNamePrefix); + } + + public static void clearThreadCacheNamePrefix() { + JbootCacheManager.me().getCache().clearThreadCacheNamePrefix(); } public static T get(String cacheName, Object key) { diff --git a/src/main/java/io/jboot/utils/ClassScanner.java b/src/main/java/io/jboot/utils/ClassScanner.java index cd4262452861c1d605f421ab51b1cc953016c6ae..b559164dc38d9ff6bd88340bb5483a2d117d4782 100644 --- a/src/main/java/io/jboot/utils/ClassScanner.java +++ b/src/main/java/io/jboot/utils/ClassScanner.java @@ -1,511 +1,907 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.utils; - -import io.jboot.app.config.JbootConfigManager; - -import java.io.File; -import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.Modifier; -import java.net.URL; -import java.net.URLClassLoader; -import java.net.URLDecoder; -import java.util.*; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.stream.Collectors; - -public class ClassScanner { - - private static final Set applicationClassCache = new HashSet<>(); - public static final Set includeJars = new HashSet<>(); - public static final Set excludeJars = new HashSet<>(); - - public static void addScanJarPrefix(String prefix) { - includeJars.add(prefix.trim()); - } - - static { - addScanJarPrefix("jboot"); - } - - public static void addUnscanJarPrefix(String prefix) { - excludeJars.add(prefix.trim()); - } - - static { - excludeJars.add("jfinal-"); - excludeJars.add("cos-20"); - excludeJars.add("cglib-"); - excludeJars.add("undertow-"); - excludeJars.add("xnio-"); - excludeJars.add("javax."); - excludeJars.add("hikaricp-"); - excludeJars.add("druid-"); - excludeJars.add("mysql-"); - excludeJars.add("db2jcc-"); - excludeJars.add("db2jcc4-"); - excludeJars.add("ojdbc"); - excludeJars.add("junit-"); - excludeJars.add("org.junit"); - excludeJars.add("hamcrest-"); - excludeJars.add("jboss-"); - excludeJars.add("motan-"); - excludeJars.add("commons-pool"); - excludeJars.add("commons-beanutils"); - excludeJars.add("commons-codec"); - excludeJars.add("commons-collections"); - excludeJars.add("commons-configuration"); - excludeJars.add("commons-lang"); - excludeJars.add("commons-logging"); - excludeJars.add("commons-io"); - excludeJars.add("commons-httpclient"); - excludeJars.add("commons-fileupload"); - excludeJars.add("commons-validator"); - excludeJars.add("commons-email"); - excludeJars.add("commons-text"); - excludeJars.add("commons-cli"); - excludeJars.add("commons-math"); - excludeJars.add("commons-jxpath"); - excludeJars.add("audience-"); - excludeJars.add("hessian-"); - excludeJars.add("metrics-"); - excludeJars.add("javapoet-"); - excludeJars.add("netty-"); - excludeJars.add("consul-"); - excludeJars.add("gson-"); - excludeJars.add("zookeeper-"); - excludeJars.add("slf4j-"); - excludeJars.add("fastjson-"); - excludeJars.add("guava-"); - excludeJars.add("failureaccess-"); - excludeJars.add("listenablefuture-"); - excludeJars.add("jsr305-"); - excludeJars.add("checker-qual-"); - excludeJars.add("error_prone_annotations-"); - excludeJars.add("j2objc-"); - excludeJars.add("animal-sniffer-"); - excludeJars.add("cron4j-"); - excludeJars.add("jedis-"); - excludeJars.add("lettuce-"); - excludeJars.add("reactor-"); - excludeJars.add("fst-"); - excludeJars.add("kryo-"); - excludeJars.add("jackson-"); - excludeJars.add("javassist-"); - excludeJars.add("objenesis-"); - excludeJars.add("reflectasm-"); - excludeJars.add("asm-"); - excludeJars.add("minlog-"); - excludeJars.add("jsoup-"); - excludeJars.add("ons-client-"); - excludeJars.add("amqp-client-"); - excludeJars.add("ehcache-"); - excludeJars.add("sharding-"); - excludeJars.add("snakeyaml-"); - excludeJars.add("groovy-"); - excludeJars.add("profiler-"); - excludeJars.add("joda-time-"); - excludeJars.add("shiro-"); - excludeJars.add("dubbo-"); - excludeJars.add("curator-"); - excludeJars.add("resteasy-"); - excludeJars.add("reactive-"); - excludeJars.add("validation-"); - excludeJars.add("httpclient-"); - excludeJars.add("httpcore-"); - excludeJars.add("httpmime-"); - excludeJars.add("jcip-"); - excludeJars.add("jcl-"); - excludeJars.add("microprofile-"); - excludeJars.add("org.osgi"); - excludeJars.add("zkclient-"); - excludeJars.add("jjwt-"); - excludeJars.add("okhttp-"); - excludeJars.add("okio-"); - excludeJars.add("zbus-"); - excludeJars.add("swagger-"); - excludeJars.add("j2cache-"); - excludeJars.add("caffeine-"); - excludeJars.add("jline-"); - excludeJars.add("qpid-"); - excludeJars.add("geronimo-"); - excludeJars.add("activation-"); - excludeJars.add("org.abego"); - excludeJars.add("antlr-"); - excludeJars.add("antlr4-"); - excludeJars.add("st4-"); - excludeJars.add("icu4j-"); - excludeJars.add("idea_rt"); - excludeJars.add("mrjtoolkit"); - excludeJars.add("logback-"); - excludeJars.add("log4j-"); - excludeJars.add("log4j2-"); - excludeJars.add("aliyun-java-sdk-"); - excludeJars.add("aliyun-sdk-"); - excludeJars.add("archaius-"); - excludeJars.add("aopalliance-"); - excludeJars.add("hdrhistogram-"); - excludeJars.add("jdom-"); - excludeJars.add("rxjava-"); - excludeJars.add("jersey-"); - excludeJars.add("stax-"); - excludeJars.add("stax2-"); - excludeJars.add("jettison-"); - excludeJars.add("commonmark-"); - excludeJars.add("jaxb-"); - excludeJars.add("json-20"); - excludeJars.add("jcseg-"); - excludeJars.add("lucene-"); - excludeJars.add("elasticsearch-"); - excludeJars.add("jopt-"); - excludeJars.add("httpasyncclient-"); - excludeJars.add("jna-"); - excludeJars.add("lang-mustache-client-"); - excludeJars.add("parent-join-client-"); - excludeJars.add("rank-eval-client-"); - excludeJars.add("aggs-matrix-stats-client-"); - excludeJars.add("t-digest-"); - excludeJars.add("compiler-"); - excludeJars.add("hppc-"); - excludeJars.add("libthrift-"); - excludeJars.add("seata-"); - excludeJars.add("eureka-"); - excludeJars.add("netflix-"); - excludeJars.add("nacos-"); - excludeJars.add("apollo-"); - excludeJars.add("guice-"); - excludeJars.add("servlet-"); - excludeJars.add("debugger-agent.jar"); - excludeJars.add("xpp3_min-"); - excludeJars.add("latency"); - excludeJars.add("micrometer-"); - excludeJars.add("xstream-"); - excludeJars.add("jsr311-"); - excludeJars.add("servo-"); - excludeJars.add("compactmap-"); - excludeJars.add("dexx-"); - excludeJars.add("xmlpull-"); - } - - static { - String scanJarPrefx = JbootConfigManager.me().getConfigValue("jboot.app.scanner.scanJarPrefix"); - if (scanJarPrefx != null) { - for (String prefix : scanJarPrefx.split(",")) { - addScanJarPrefix(prefix.trim()); - } - } - - String unScanJarPrefix = JbootConfigManager.me().getConfigValue("jboot.app.scanner.unScanJarPrefix"); - if (unScanJarPrefix != null) { - for (String prefix : unScanJarPrefix.split(",")) { - addUnscanJarPrefix(prefix.trim()); - } - } - } - - public static List> scanSubClass(Class pclazz) { - return scanSubClass(pclazz, false); - } - - - public static List> scanSubClass(Class pclazz, boolean isInstantiable) { - initIfNecessary(); - List> classes = new ArrayList<>(); - findChildClasses(classes, pclazz, isInstantiable); - return classes; - } - - public static List scanClass() { - return scanClass(false); - } - - public static List scanClass(boolean isInstantiable) { - - initIfNecessary(); - - if (!isInstantiable) { - return new ArrayList<>(applicationClassCache); - } - - return applicationClassCache.stream() - .filter(ClassScanner::isInstantiable) - .collect(Collectors.toList()); - - } - - public static void clearClassCache() { - applicationClassCache.clear(); - } - - - private static boolean isInstantiable(Class clazz) { - return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()); - } - - - public static List scanClassByAnnotation(Class annotationClass, boolean isInstantiable) { - initIfNecessary(); - - List list = new ArrayList<>(); - for (Class clazz : applicationClassCache) { - Annotation annotation = clazz.getAnnotation(annotationClass); - if (annotation == null) { - continue; - } - - if (isInstantiable && !isInstantiable(clazz)) { - continue; - } - - list.add(clazz); - } - - return list; - } - - private static void initIfNecessary() { - if (applicationClassCache.isEmpty()) { - initAppClasses(); - } - } - - - private static void findChildClasses(List> classes, Class parent, boolean isInstantiable) { - for (Class clazz : applicationClassCache) { - - if (!parent.isAssignableFrom(clazz)) { - continue; - } - - if (isInstantiable && !isInstantiable(clazz)) { - continue; - } - - classes.add(clazz); - } - } - - - private static void initAppClasses() { - - Set jarPaths = new HashSet<>(); - Set classPaths = new HashSet<>(); - - findClassPathsAndJars(jarPaths, classPaths, ClassScanner.class.getClassLoader()); - - String tomcatClassPath = null; - - for (String classPath : classPaths) { - //过滤tomcat自身的lib 以及 bin 下的jar - File tomcatApiJarFile = new File(classPath, "tomcat-api.jar"); - if (tomcatApiJarFile.exists()) { - tomcatClassPath = tomcatApiJarFile - .getParentFile() - .getParentFile().getAbsolutePath(); - continue; - } - - if (JbootConfigManager.me().isDevMode()) { - System.out.println("ClassScanner scan classpath : " + classPath); - } - - addClassesFromClassPath(classPath); - } - - for (String jarPath : jarPaths) { - - //过滤 tomcat 的 jar,但是不能过滤 webapps 目录下的 - if (tomcatClassPath != null - && jarPath.startsWith(tomcatClassPath) - && !jarPath.contains("webapps")) { - continue; - } - - if (!isIncludeJar(jarPath)) { - continue; - } - - if (JbootConfigManager.me().isDevMode()) { - System.out.println("ClassScanner scan jar : " + jarPath); - } - - addClassesFromJar(jarPath); - } - - - } - - private static void addClassesFromJar(String jarPath) { - JarFile jarFile = null; - try { - jarFile = new JarFile(jarPath); - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry jarEntry = entries.nextElement(); - String entryName = jarEntry.getName(); - if (!jarEntry.isDirectory() && entryName.endsWith(".class")) { - String className = entryName.replace("/", ".").substring(0, entryName.length() - 6); - addClass(classForName(className)); - } - } - } catch (IOException e1) { - } finally { - if (jarFile != null) { - try { - jarFile.close(); - } catch (IOException e) { - } - } - } - } - - - private static void addClassesFromClassPath(String classPath) { - - List classFileList = new ArrayList<>(); - scanClassFile(classFileList, classPath); - - for (File file : classFileList) { - - int start = classPath.length(); - int end = file.toString().length() - ".class".length(); - - String classFile = file.toString().substring(start + 1, end); - String className = classFile.replace(File.separator, "."); - - addClass(classForName(className)); - } - } - - private static void addClass(Class clazz) { - if (clazz != null) { - applicationClassCache.add(clazz); - } - } - - - private static void findClassPathsAndJars(Set jarPaths, Set classPaths, ClassLoader classLoader) { - try { - if (classLoader instanceof URLClassLoader) { - URLClassLoader urlClassLoader = (URLClassLoader) classLoader; - URL[] urLs = urlClassLoader.getURLs(); - for (URL url : urLs) { - String path = url.getPath(); - path = URLDecoder.decode(path, "UTF-8"); - - // path : /d:/xxx - if (path.startsWith("/") && path.indexOf(":") == 2) { - path = path.substring(1); - } - - if (!path.toLowerCase().endsWith(".jar")) { - classPaths.add(new File(path).getCanonicalPath()); - continue; - } - - jarPaths.add(path); - } - } - ClassLoader parent = classLoader.getParent(); - if (parent != null) { - findClassPathsAndJars(jarPaths, classPaths, parent); - } - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - private static boolean isIncludeJar(String path) { - - String jarName = new File(path).getName().toLowerCase(); - - for (String include : includeJars) { - if (jarName.startsWith(include)) { - return true; - } - } - - for (String exclude : excludeJars) { - if (jarName.startsWith(exclude)) { - return false; - } - } - - //from maven repository - if (path.contains("/.m2/repository") - || path.contains("\\.m2\\repository")) { - return false; - } - - //from jre lib - if (path.contains("/jre/lib") - || path.contains("\\jre\\lib")) { - return false; - } - - //from java home - if (getJavaHome() != null - && path.startsWith(getJavaHome())) { - return false; - } - - return true; - } - - - @SuppressWarnings("unchecked") - private static Class classForName(String className) { - try { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - return Class.forName(className, false, cl); - } catch (Throwable ex) { - //ignore - } - return null; - } - - - private static void scanClassFile(List fileList, String path) { - File[] files = new File(path).listFiles(); - if (null == files || files.length == 0) { - return; - } - for (File file : files) { - if (file.isDirectory()) { - scanClassFile(fileList, file.getAbsolutePath()); - } else if (file.getName().endsWith(".class")) { - fileList.add(file); - } - } - } - - - private static String javaHome; - - private static String getJavaHome() { - if (javaHome == null) { - try { - javaHome = new File(System.getProperty("java.home"), "..").getCanonicalPath(); - } catch (IOException e) { - e.printStackTrace(); - } - } - return javaHome; - } - -} +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import io.jboot.app.config.JbootConfigManager; + +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLDecoder; +import java.util.*; +import java.util.function.Predicate; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.stream.Collectors; + +public class ClassScanner { + + private static final Set> appClassesCache = new HashSet<>(); + + public static final Set scanJars = new HashSet<>(); + public static final Set excludeJars = new HashSet<>(); + + public static final Set scanClasses = new HashSet<>(); + public static final Set excludeClasses = new HashSet<>(); + + // dev模式打开扫描信息打印 + private static boolean printScannerInfoEnable = false; + + public static boolean isPrintScannerInfoEnable() { + return printScannerInfoEnable; + } + + public static void setPrintScannerInfoEnable(boolean printScannerInfoEnable) { + ClassScanner.printScannerInfoEnable = printScannerInfoEnable; + } + + + public static void addScanJarPrefix(String prefix) { + scanJars.add(prefix.toLowerCase().trim()); + } + + static { + scanJars.add("jboot"); + } + + + public static void addUnscanJarPrefix(String prefix) { + excludeJars.add(prefix.toLowerCase().trim()); + } + + static { + excludeJars.add("jfinal-"); + excludeJars.add("cos-"); + excludeJars.add("cglib-"); + excludeJars.add("undertow-"); + excludeJars.add("xnio-"); + excludeJars.add("javax."); + excludeJars.add("hikaricp-"); + excludeJars.add("druid-"); + excludeJars.add("mysql-"); + excludeJars.add("db2jcc-"); + excludeJars.add("db2jcc4-"); + excludeJars.add("ojdbc"); + excludeJars.add("junit-"); + excludeJars.add("junit5-"); + excludeJars.add("org.junit"); + excludeJars.add("hamcrest-"); + excludeJars.add("jboss-"); + excludeJars.add("motan-"); + excludeJars.add("commons-pool"); + excludeJars.add("commons-beanutils"); + excludeJars.add("commons-codec"); + excludeJars.add("commons-collections"); + excludeJars.add("commons-configuration"); + excludeJars.add("commons-lang"); + excludeJars.add("commons-logging"); + excludeJars.add("commons-io"); + excludeJars.add("commons-httpclient"); + excludeJars.add("commons-fileupload"); + excludeJars.add("commons-validator"); + excludeJars.add("commons-email"); + excludeJars.add("commons-text"); + excludeJars.add("commons-cli"); + excludeJars.add("commons-math"); + excludeJars.add("commons-jxpath"); + excludeJars.add("commons-compress"); + excludeJars.add("audience-"); + excludeJars.add("hessian-"); + excludeJars.add("metrics-"); + excludeJars.add("javapoet-"); + excludeJars.add("netty-"); + excludeJars.add("consul-"); + excludeJars.add("gson-"); + excludeJars.add("zookeeper-"); + excludeJars.add("slf4j-"); + excludeJars.add("fastjson-"); + excludeJars.add("guava-"); + excludeJars.add("failureaccess-"); + excludeJars.add("listenablefuture-"); + excludeJars.add("jsr305-"); + excludeJars.add("checker-qual-"); + excludeJars.add("error_prone_annotations-"); + excludeJars.add("j2objc-"); + excludeJars.add("animal-sniffer-"); + excludeJars.add("cron4j-"); + excludeJars.add("jedis-"); + excludeJars.add("lettuce-"); + excludeJars.add("reactor-"); + excludeJars.add("fst-"); + excludeJars.add("kryo-"); + excludeJars.add("jackson-"); + excludeJars.add("javassist-"); + excludeJars.add("objenesis-"); + excludeJars.add("reflectasm-"); + excludeJars.add("asm-"); + excludeJars.add("minlog-"); + excludeJars.add("jsoup-"); + excludeJars.add("ons-client-"); + excludeJars.add("amqp-client-"); + excludeJars.add("ehcache-"); + excludeJars.add("sharding-"); + excludeJars.add("snakeyaml-"); + excludeJars.add("groovy-"); + excludeJars.add("profiler-"); + excludeJars.add("joda-time-"); + excludeJars.add("shiro-"); + excludeJars.add("dubbo-"); + excludeJars.add("curator-"); + excludeJars.add("resteasy-"); + excludeJars.add("reactive-"); + excludeJars.add("validation-"); + excludeJars.add("httpclient-"); + excludeJars.add("httpcore-"); + excludeJars.add("httpmime-"); + excludeJars.add("jcip-"); + excludeJars.add("jcl-"); + excludeJars.add("microprofile-"); + excludeJars.add("org.osgi"); + excludeJars.add("zkclient-"); + excludeJars.add("jjwt-"); + excludeJars.add("okhttp-"); + excludeJars.add("okio-"); + excludeJars.add("zbus-"); + excludeJars.add("swagger-"); + excludeJars.add("j2cache-"); + excludeJars.add("caffeine-"); + excludeJars.add("jline-"); + excludeJars.add("qpid-"); + excludeJars.add("geronimo-"); + excludeJars.add("activation-"); + excludeJars.add("org.abego"); + excludeJars.add("antlr-"); + excludeJars.add("antlr4-"); + excludeJars.add("st4-"); + excludeJars.add("icu4j-"); + excludeJars.add("idea_rt"); + excludeJars.add("mrjtoolkit"); + excludeJars.add("logback-"); + excludeJars.add("log4j-"); + excludeJars.add("log4j2-"); + excludeJars.add("aliyun-java-sdk-"); + excludeJars.add("aliyun-sdk-"); + excludeJars.add("archaius-"); + excludeJars.add("aopalliance-"); + excludeJars.add("hdrhistogram-"); + excludeJars.add("jdom-"); + excludeJars.add("rxjava-"); + excludeJars.add("jersey-"); + excludeJars.add("stax-"); + excludeJars.add("stax2-"); + excludeJars.add("jettison-"); + excludeJars.add("commonmark-"); + excludeJars.add("jaxb-"); + excludeJars.add("json-20"); + excludeJars.add("jcseg-"); + excludeJars.add("lucene-"); + excludeJars.add("elasticsearch-"); + excludeJars.add("jopt-"); + excludeJars.add("httpasyncclient-"); + excludeJars.add("jna-"); + excludeJars.add("lang-mustache-client-"); + excludeJars.add("parent-join-client-"); + excludeJars.add("rank-eval-client-"); + excludeJars.add("aggs-matrix-stats-client-"); + excludeJars.add("t-digest-"); + excludeJars.add("compiler-"); + excludeJars.add("hppc-"); + excludeJars.add("libthrift-"); + excludeJars.add("seata-"); + excludeJars.add("eureka-"); + excludeJars.add("netflix-"); + excludeJars.add("nacos-"); + excludeJars.add("apollo-"); + excludeJars.add("guice-"); + excludeJars.add("servlet-"); + excludeJars.add("debugger-agent.jar"); + excludeJars.add("xpp3_min-"); + excludeJars.add("latency"); + excludeJars.add("micrometer-"); + excludeJars.add("xstream-"); + excludeJars.add("jsr311-"); + excludeJars.add("servo-"); + excludeJars.add("compactmap-"); + excludeJars.add("dexx-"); + excludeJars.add("spotbugs-"); + excludeJars.add("xmlpull-"); + excludeJars.add("shardingsphere-"); + excludeJars.add("sentinel-"); + excludeJars.add("spring-"); + excludeJars.add("simpleclient-"); + excludeJars.add("breeze-"); + excludeJars.add("config-"); + excludeJars.add("encrypt-core-"); + excludeJars.add("lombok-"); + excludeJars.add("hutool-"); + excludeJars.add("jakarta."); + excludeJars.add("protostuff-"); + excludeJars.add("poi-"); + excludeJars.add("easypoi-"); + excludeJars.add("ognl-"); + excludeJars.add("xmlbeans-"); + excludeJars.add("master-slave-core-"); + excludeJars.add("shadow-core-rewrite-"); + excludeJars.add("apiguardian-api-"); + excludeJars.add("opentest4j-"); + excludeJars.add("opentracing-"); + excludeJars.add("freemarker-"); + excludeJars.add("protobuf-"); + excludeJars.add("jdom2-"); + excludeJars.add("useragentutils-"); + excludeJars.add("common-io-"); + excludeJars.add("common-image-"); + excludeJars.add("common-lang-"); + excludeJars.add("imageio-"); + excludeJars.add("curvesapi-"); + excludeJars.add("myexcel-"); + excludeJars.add("oshi-"); + excludeJars.add("classmate-"); + excludeJars.add("hibernate-"); + excludeJars.add("aspectjweaver-"); + excludeJars.add("aspectjrt-"); + excludeJars.add("simpleclient_"); + excludeJars.add("rocketmq-"); + excludeJars.add("clickhouse-"); + excludeJars.add("lz4-"); + excludeJars.add("commons-digester-"); + excludeJars.add("opencc4j-"); + excludeJars.add("heaven-"); + excludeJars.add("tinypinyin-"); + excludeJars.add("jieba-"); + excludeJars.add("ahocorasick-"); + excludeJars.add("kotlin-"); + excludeJars.add("xml-apis-"); + excludeJars.add("dom4j-"); + excludeJars.add("ini4j-"); + excludeJars.add("cache-api-"); + excludeJars.add("byte-buddy-"); + excludeJars.add("jodd-"); + excludeJars.add("redisson-"); + excludeJars.add("bcprov-"); + excludeJars.add("pay-java-"); + excludeJars.add("alipay-sdk-"); + excludeJars.add("mapper-extras-"); + excludeJars.add("org.jacoco"); + excludeJars.add("jxl-"); + excludeJars.add("jxls-"); + excludeJars.add("jstl-"); + excludeJars.add("batik-"); + excludeJars.add("xmlsec-"); + excludeJars.add("pdfbox-"); + excludeJars.add("fontbox-"); + excludeJars.add("xk-time-"); + excludeJars.add("geohash-"); + excludeJars.add("ezmorph-"); + excludeJars.add("async-http-"); + excludeJars.add("jsr-"); + excludeJars.add("jsr250"); + excludeJars.add("pinyin4j"); + excludeJars.add("ijpay-"); + excludeJars.add("wildfly-"); + excludeJars.add("liquibase-"); + excludeJars.add("flowable-"); + excludeJars.add("mybatis-"); + excludeJars.add("ip2region-"); + excludeJars.add("java-uuid-generator-"); + excludeJars.add("quartz-"); + excludeJars.add("elasticjob-"); + excludeJars.add("reflections-"); + excludeJars.add("jts-"); + excludeJars.add("json-"); + excludeJars.add("httpclient5-"); + excludeJars.add("httpcore5-"); + excludeJars.add("jul-to-"); + excludeJars.add("calcite-"); + excludeJars.add("avatica-"); + excludeJars.add("encoder-"); + excludeJars.add("aggdesigner-"); + excludeJars.add("uzaygezen-"); + excludeJars.add("memory-"); + excludeJars.add("commons-"); + excludeJars.add("accessors-"); + excludeJars.add("sketches-"); + excludeJars.add("h2-"); + excludeJars.add("cosid-"); + excludeJars.add("mchange-"); + excludeJars.add("janino-"); + excludeJars.add("jnanoid-"); + excludeJars.add("proj4j-"); + excludeJars.add("sparsebitset-"); + excludeJars.add("captcha-"); + excludeJars.add("cryptokit"); + excludeJars.add("isec-"); + excludeJars.add("jurt-"); + excludeJars.add("minio-"); + excludeJars.add("logging-"); + excludeJars.add("simple-xml-"); + excludeJars.add("jodconverter-"); + excludeJars.add("credentials-"); + excludeJars.add("unoil-"); + excludeJars.add("endpoint-"); + excludeJars.add("ridl-"); + excludeJars.add("tencentcloud-"); + excludeJars.add("yauaa-"); + excludeJars.add("tea-"); + excludeJars.add("fr."); + excludeJars.add("vod20"); + excludeJars.add("juh-"); + excludeJars.add("prefixmap-"); + excludeJars.add("dmjdbcdriver"); + } + + + public static void addUnscanClassPrefix(String prefix) { + excludeClasses.add(prefix.trim()); + } + + static { + excludeClasses.add("java."); + excludeClasses.add("javax."); + excludeClasses.add("junit."); + excludeClasses.add("jline."); + excludeClasses.add("redis."); + excludeClasses.add("lombok."); + excludeClasses.add("oshi."); + excludeClasses.add("jodd."); + excludeClasses.add("javassist."); + excludeClasses.add("google."); + excludeClasses.add("com.jfinal."); + excludeClasses.add("com.aliyuncs."); + excludeClasses.add("com.carrotsearch."); + excludeClasses.add("org.aopalliance."); + excludeClasses.add("org.apache."); + excludeClasses.add("org.nustaq."); + excludeClasses.add("net.sf."); + excludeClasses.add("org.slf4j."); + excludeClasses.add("org.antlr."); + excludeClasses.add("org.jboss."); + excludeClasses.add("org.checkerframework."); + excludeClasses.add("org.jsoup."); + excludeClasses.add("org.objenesis."); + excludeClasses.add("org.ow2."); + excludeClasses.add("org.reactivest."); + excludeClasses.add("org.yaml."); + excludeClasses.add("org.checker"); + excludeClasses.add("org.codehaus"); + excludeClasses.add("org.commonmark"); + excludeClasses.add("org.jdom2."); + excludeClasses.add("org.aspectj."); + excludeClasses.add("org.hibernate."); + excludeClasses.add("org.ahocorasick."); + excludeClasses.add("org.lionsoul.jcseg."); + excludeClasses.add("org.ini4j."); + excludeClasses.add("org.jetbrains."); + excludeClasses.add("org.jacoco."); + excludeClasses.add("org.xnio."); + excludeClasses.add("org.bouncycastle."); + excludeClasses.add("org.elasticsearch."); + excludeClasses.add("org.hamcrest."); + excludeClasses.add("org.objectweb."); + excludeClasses.add("org.joda."); + excludeClasses.add("org.wildfly."); + excludeClasses.add("org.owasp."); + excludeClasses.add("aj.org."); + excludeClasses.add("ch.qos."); + excludeClasses.add("joptsimple."); + excludeClasses.add("com.alibaba.csp."); + excludeClasses.add("com.alibaba.nacos."); + excludeClasses.add("com.alibaba.druid."); + excludeClasses.add("com.alibaba.fastjson."); + excludeClasses.add("com.aliyun.open"); + excludeClasses.add("com.caucho"); + excludeClasses.add("com.codahale"); + excludeClasses.add("com.ctrip.framework.apollo"); + excludeClasses.add("com.ecwid."); + excludeClasses.add("com.esotericsoftware."); + excludeClasses.add("com.fasterxml."); + excludeClasses.add("com.github."); + excludeClasses.add("io.github."); + excludeClasses.add("com.google."); + excludeClasses.add("metrics_influxdb."); + excludeClasses.add("com.rabbitmq."); + excludeClasses.add("com.squareup."); + excludeClasses.add("com.sun."); + excludeClasses.add("com.typesafe."); + excludeClasses.add("com.weibo.api.motan."); + excludeClasses.add("com.zaxxer."); + excludeClasses.add("com.mysql."); + excludeClasses.add("com.papertrail."); + excludeClasses.add("com.egzosn."); + excludeClasses.add("com.alipay.api"); + excludeClasses.add("org.gjt."); + excludeClasses.add("org.fusesource."); + excludeClasses.add("org.redisson."); + excludeClasses.add("io.dropwizard"); + excludeClasses.add("io.prometheus"); + excludeClasses.add("io.jsonwebtoken"); + excludeClasses.add("io.lettuce"); + excludeClasses.add("reactor.adapter"); + excludeClasses.add("io.seata."); + excludeClasses.add("io.swagger."); + excludeClasses.add("io.undertow."); + excludeClasses.add("io.netty."); + excludeClasses.add("io.opentracing."); + excludeClasses.add("it.sauronsoftware"); + excludeClasses.add("net.oschina.j2cache"); + excludeClasses.add("net.bytebuddy"); + excludeClasses.add("cn.hutool."); + excludeClasses.add("com.dyuproject."); + excludeClasses.add("io.protostuff."); + excludeClasses.add("io.reactivex."); + excludeClasses.add("freemarker."); + excludeClasses.add("com.twelvemonkeys."); + excludeClasses.add("eu.bitwalker."); + excludeClasses.add("jstl."); + excludeClasses.add("jxl."); + excludeClasses.add("org.jxls"); + excludeClasses.add("org.kordamp"); + excludeClasses.add("org.mybatis"); + excludeClasses.add("org.lisonsoul"); + excludeClasses.add("org.flowable"); + } + + + public static void addScanClassPrefix(String prefix) { + scanClasses.add(prefix.toLowerCase().trim()); + } + + static { + scanClasses.add("io.jboot.support.shiro.directives"); + } + + static { + String scanJarPrefix = JbootConfigManager.me().getConfigValue("jboot.app.scanner.scanJarPrefix"); + if (scanJarPrefix != null) { + String[] prefixes = scanJarPrefix.split(","); + for (String prefix : prefixes) { + if (prefix != null && prefix.trim().length() > 0) { + addScanJarPrefix(prefix.trim()); + } + } + } + + String unScanJarPrefix = JbootConfigManager.me().getConfigValue("jboot.app.scanner.unScanJarPrefix"); + if (unScanJarPrefix != null) { + String[] prefixes = unScanJarPrefix.split(","); + for (String prefix : prefixes) { + if (prefix != null && prefix.trim().length() > 0) { + addUnscanJarPrefix(prefix.trim()); + } + } + } + + String unScanClassPrefix = JbootConfigManager.me().getConfigValue("jboot.app.scanner.unScanClassPrefix"); + if (unScanClassPrefix != null) { + String[] prefixes = unScanClassPrefix.split(","); + for (String prefix : prefixes) { + if (prefix != null && prefix.trim().length() > 0) { + addUnscanClassPrefix(prefix.trim()); + } + } + } + + String scanClassPrefix = JbootConfigManager.me().getConfigValue("jboot.app.scanner.scanClassPrefix"); + if (scanClassPrefix != null) { + String[] prefixes = scanClassPrefix.split(","); + for (String prefix : prefixes) { + if (prefix != null && prefix.trim().length() > 0) { + addScanClassPrefix(prefix.trim()); + } + } + } + + } + + public static List> scanSubClass(Class pclazz) { + return scanSubClass(pclazz, false); + } + + + public static List> scanSubClass(Class pclazz, boolean instantiable) { + initIfNecessary(); + List> classes = new ArrayList<>(); + findChildClasses(classes, pclazz, instantiable); + return classes; + } + + public static List scanClass() { + return scanClass(false); + } + + public static List scanClass(boolean isInstantiable) { + + initIfNecessary(); + + if (!isInstantiable) { + return new ArrayList<>(appClassesCache); + } + + return scanClass(ClassScanner::isInstantiable); + + } + + public static List scanClass(Predicate filter) { + + initIfNecessary(); + + return appClassesCache.stream() + .filter(filter) + .collect(Collectors.toList()); + + } + + public static void clearAppClassesCache() { + appClassesCache.clear(); + } + + + private static boolean isInstantiable(Class clazz) { + return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()); + } + + + public static List scanClassByAnnotation(Class annotationClass, boolean instantiable) { + initIfNecessary(); + + List list = new ArrayList<>(); + for (Class clazz : appClassesCache) { + Annotation annotation = clazz.getAnnotation(annotationClass); + if (annotation == null) { + continue; + } + + if (instantiable && !isInstantiable(clazz)) { + continue; + } + + list.add(clazz); + } + + return list; + } + + private static void initIfNecessary() { + if (appClassesCache.isEmpty()) { + initAppClasses(); + } + } + + + private static void findChildClasses(List> classes, Class parent, boolean instantiable) { + for (Class clazz : appClassesCache) { + + if (!parent.isAssignableFrom(clazz)) { + continue; + } + + if (instantiable && !isInstantiable(clazz)) { + continue; + } + + classes.add(clazz); + } + } + + + private static void initAppClasses() { + + Set jarPaths = new HashSet<>(); + Set classPaths = new HashSet<>(); + + + // jdk8 及以下、 + // tomcat 容器、 + // jfinal-undertow、 + // 以上三种加载模式通过 classloader 获取 + findClassPathsAndJarsByClassloader(jarPaths, classPaths, ClassScanner.class.getClassLoader()); + + //jdk9+ 等其他方式通过 classpath 获取 + findClassPathsAndJarsByClassPath(jarPaths, classPaths); + + + String tomcatClassPath = null; + + for (String classPath : classPaths) { + //过滤tomcat自身的lib 以及 bin 下的jar + File tomcatApiJarFile = new File(classPath, "tomcat-api.jar"); + File tomcatJuliJarFile = new File(classPath, "tomcat-juli.jar"); + if (tomcatApiJarFile.exists() || tomcatJuliJarFile.exists()) { + tomcatClassPath = tomcatApiJarFile + .getParentFile() + .getParentFile().getAbsolutePath().replace('\\', '/'); + continue; + } + + if (isPrintScannerInfoEnable()) { + System.out.println("Jboot Scan ClassPath: " + classPath); + } + + addClassesFromClassPath(classPath); + } + + for (String jarPath : jarPaths) { + + //过滤 tomcat 的 jar,但是不能过滤 webapps 目录下的 + if (tomcatClassPath != null + && jarPath.startsWith(tomcatClassPath) + && !jarPath.contains("webapps")) { + continue; + } + + if (!isIncludeJar(jarPath)) { + continue; + } + + if (isPrintScannerInfoEnable()) { + System.out.println("Jboot Scan Jar: " + jarPath); + } + + addClassesFromJar(jarPath); + } + + + } + + private static void addClassesFromJar(String jarPath) { + JarFile jarFile = null; + try { + jarFile = new JarFile(jarPath); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + String entryName = jarEntry.getName(); + if (jarEntry.isDirectory() && entryName.startsWith("BOOT-INF/classes/")) { + + if (isPrintScannerInfoEnable()) { + System.out.println("Jboot Scan entryName: " + entryName); + } + + if (entryName.endsWith(".class")) { + String className = entryName.replace("/", ".").substring(0, entryName.length() - 6); + addClass(classForName(className)); + } + } else { + if (entryName.endsWith(".class")) { + String className = entryName.replace("/", ".").substring(0, entryName.length() - 6); + addClass(classForName(className)); + } else if (entryName.startsWith("BOOT-INF/lib/") && entryName.endsWith(".jar")) { + if (!isIncludeJar(entryName)) { + continue; + } + + if (isPrintScannerInfoEnable()) { + System.out.println("Jboot Scan Jar: " + entryName); + } + + try (JarInputStream jarStream = new JarInputStream(jarFile + .getInputStream(jarEntry));) { + JarEntry innerEntry = jarStream.getNextJarEntry(); + while (innerEntry != null) { + if (!innerEntry.isDirectory()) { + String nestedEntryName = innerEntry.getName(); + if (nestedEntryName.endsWith(".class")) { + String className = nestedEntryName.replace("/", ".").substring(0, nestedEntryName.length() - 6); + addClass(classForName(className)); + } + } + innerEntry = jarStream.getNextJarEntry(); + } + } + } + } + } + } catch (IOException e1) { + } finally { + if (jarFile != null) { + try { + jarFile.close(); + } catch (IOException e) { + } + } + } + } + + + private static void addClassesFromClassPath(String classPath) { + + List classFileList = new ArrayList<>(); + scanClassFile(classFileList, classPath); + + for (File file : classFileList) { + + int start = classPath.length(); + int end = file.toString().length() - ".class".length(); + + String classFile = file.toString().substring(start + 1, end); + String className = classFile.replace(File.separator, "."); + + addClass(classForName(className)); + } + } + + private static void addClass(Class clazz) { + if (clazz != null && isNotExcludeClass(clazz.getName())) { + appClassesCache.add(clazz); + } + } + + //用于在进行 fatjar 打包时,提高性能 + private static boolean isNotExcludeClass(String clazzName) { + for (String prefix : scanClasses) { + if (clazzName.startsWith(prefix)) { + return true; + } + } + for (String prefix : excludeClasses) { + if (clazzName.startsWith(prefix)) { + return false; + } + } + return true; + } + + + private static void findClassPathsAndJarsByClassloader(Set jarPaths, Set classPaths, ClassLoader classLoader) { + try { + URL[] urls = null; + if (classLoader instanceof URLClassLoader) { + URLClassLoader ucl = (URLClassLoader) classLoader; + urls = ucl.getURLs(); + } + if (urls != null) { + for (URL url : urls) { + String path = url.getPath(); + path = URLDecoder.decode(path, "UTF-8"); + + // path : /d:/xxx + if (path.startsWith("/") && path.indexOf(":") == 2) { + path = path.substring(1); + } + + if (!path.toLowerCase().endsWith(".jar")) { + if (path.toLowerCase().endsWith("!/") || path.toLowerCase().endsWith("!")) { + } else { + classPaths.add(new File(path).getCanonicalPath().replace('\\', '/')); + } + } else { + jarPaths.add(new File(path).getCanonicalPath().replace('\\', '/')); + } + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } + + ClassLoader parent = classLoader.getParent(); + if (parent != null) { + findClassPathsAndJarsByClassloader(jarPaths, classPaths, parent); + } + } + + private static void findClassPathsAndJarsByClassPath(Set jarPaths, Set classPaths) { + String classPath = System.getProperty("java.class.path"); + if (classPath == null || classPath.trim().length() == 0) { + return; + } + String[] classPathArray = classPath.split(File.pathSeparator); + if (classPathArray == null || classPathArray.length == 0) { + return; + } + for (String path : classPathArray) { + path = path.trim(); + + if (path.startsWith("./")) { + path = path.substring(2); + } + + if (path.startsWith("/") && path.indexOf(":") == 2) { + path = path.substring(1); + } + try { + if (!path.toLowerCase().endsWith(".jar") && !jarPaths.contains(path)) { + if (path.toLowerCase().endsWith("!/") || path.toLowerCase().endsWith("!")) { + } else { + classPaths.add(new File(path).getCanonicalPath().replace('\\', '/')); + } + } else { + jarPaths.add(new File(path).getCanonicalPath().replace('\\', '/')); + } + } catch (IOException e) { + } + } + } + + + private static boolean isIncludeJar(String path) { + + String jarName = new File(path).getName().toLowerCase(); + + for (String include : scanJars) { + if (jarName.startsWith(include)) { + return true; + } + } + + for (String exclude : excludeJars) { + if (jarName.startsWith(exclude)) { + return false; + } + } + + //from jre lib + if (path.contains("/jre/lib")) { + return false; + } + + //from java home + if (getJavaHome() != null + && path.startsWith(getJavaHome())) { + return false; + } + + return true; + } + + + @SuppressWarnings("unchecked") + private static Class classForName(String className) { + try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + return Class.forName(className, false, cl); + } catch (Throwable ex) { + //ignore + } + return null; + } + + + private static void scanClassFile(List fileList, String path) { + File[] files = new File(path).listFiles(); + if (null == files || files.length == 0) { + return; + } + for (File file : files) { + if (file.isDirectory()) { + scanClassFile(fileList, file.getAbsolutePath()); + } else if (file.getName().endsWith(".class")) { + fileList.add(file); + } + } + } + + + private static String javaHome; + + private static String getJavaHome() { + if (javaHome == null) { + try { + String javaHomeString = System.getProperty("java.home"); + if (javaHomeString != null && javaHomeString.trim().length() > 0) { + javaHome = new File(javaHomeString, "..").getCanonicalPath().replace('\\', '/'); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return javaHome; + } + +} diff --git a/src/main/java/io/jboot/utils/ClassType.java b/src/main/java/io/jboot/utils/ClassType.java new file mode 100644 index 0000000000000000000000000000000000000000..ddcfa9744d0b516e948ca137f24dfb5cfb2818cc --- /dev/null +++ b/src/main/java/io/jboot/utils/ClassType.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import java.io.Serializable; + +public class ClassType implements Serializable { + + private Class mainClass; //类 + private ClassType[] genericTypes;//泛型 + + public ClassType() { + } + + public ClassType(Class mainClass) { + this.mainClass = mainClass; + } + + public ClassType(Class mainClass, Class[] genericClasses) { + this.mainClass = mainClass; + if (genericClasses != null && genericClasses.length > 0) { + genericTypes = new ClassType[genericClasses.length]; + for (int i = 0; i < genericClasses.length; i++) { + genericTypes[i] = new ClassType(genericClasses[i]); + } + } + } + + public ClassType(Class mainClass, ClassType[] genericTypes) { + this.mainClass = mainClass; + this.genericTypes = genericTypes; + } + + public Class getMainClass() { + return mainClass; + } + + public void setMainClass(Class mainClass) { + this.mainClass = mainClass; + } + + public ClassType[] getGenericTypes() { + return genericTypes; + } + + public void setGenericTypes(ClassType[] genericTypes) { + this.genericTypes = genericTypes; + } + + public String getDataType() { + return mainClass.getSimpleName(); + } + + public boolean isGeneric() { + return genericTypes != null && genericTypes.length > 0; + } + + public boolean isVoid(){ + return mainClass == void.class; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(mainClass.getSimpleName()); + if (isGeneric()) { + sb.append("<"); + for (int i = 0; i < genericTypes.length; i++) { + sb.append(genericTypes[i].toString()); + if (i != genericTypes.length - 1) { + sb.append(","); + } + } + sb.append(">"); + } + return sb.toString(); + } +} diff --git a/src/main/java/io/jboot/utils/ClassUtil.java b/src/main/java/io/jboot/utils/ClassUtil.java index 85735ceb44ab4ce3c93290433eb57a07ad1af2ce..e614219feb22d07b54ddae64e8a1b214f16d15ac 100644 --- a/src/main/java/io/jboot/utils/ClassUtil.java +++ b/src/main/java/io/jboot/utils/ClassUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import io.jboot.aop.annotation.StaticConstruct; import io.jboot.exception.JbootException; import java.lang.reflect.*; +import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -30,8 +31,9 @@ import java.util.concurrent.ConcurrentHashMap; */ public class ClassUtil { - public static Log log = Log.getLog(ClassUtil.class); - private static final Map singletons = new ConcurrentHashMap<>(); + private static Log LOG = Log.getLog(ClassUtil.class); + + private static final Map, Object> singletons = new ConcurrentHashMap<>(); /** @@ -42,23 +44,45 @@ public class ClassUtil { * @return */ public static T singleton(Class clazz) { - Object object = singletons.get(clazz); - if (object == null) { - synchronized (clazz) { - object = singletons.get(clazz); - if (object == null) { - object = newInstance(clazz); - if (object != null) { - singletons.put(clazz, object); - } else { - Log.getLog(clazz).error("cannot new newInstance!!!!"); - } + return singleton(clazz, true); + } - } + + /** + * 获取单利 + * + * @param clazz + * @param createByAop + * @param + * @return + */ + public static synchronized T singleton(Class clazz, boolean createByAop) { + Object ret = singletons.get(clazz); + if (ret == null) { + ret = newInstance(clazz, createByAop); + if (ret != null) { + singletons.put(clazz, ret); + } else { + LOG.error("Can not new instance for class: " + clazz.getName()); } } + return (T) ret; + } - return (T) object; + public static synchronized T singleton(Class clazz, boolean createByAop, boolean inject) { + Object ret = singletons.get(clazz); + if (ret == null) { + ret = newInstance(clazz, createByAop); + if (ret != null) { + if (inject && !createByAop) { + Aop.inject(ret); + } + singletons.put(clazz, ret); + } else { + LOG.error("Can not new instance for class: " + clazz.getName()); + } + } + return (T) ret; } /** @@ -73,6 +97,27 @@ public class ClassUtil { } + /** + * 创建新的实例,并传入初始化参数 + * + * @param clazz + * @param paras + * @param + * @return + */ + public static T newInstance(Class clazz, Object... paras) { + return newInstance(clazz, true, paras); + } + + + /** + * 是否通过 AOP 来实例化 + * + * @param clazz + * @param createByAop + * @param + * @return + */ public static T newInstance(Class clazz, boolean createByAop) { if (createByAop) { return Aop.get(clazz); @@ -82,13 +127,60 @@ public class ClassUtil { constructor.setAccessible(true); return (T) constructor.newInstance(); } catch (Exception e) { - log.error("can not newInstance class:" + clazz + "\n" + e.toString(), e); + LOG.error("Can not new instance for class:" + clazz.getName() + "\n" + e, e); } return null; } } + + public static T newInstance(Class clazz, boolean createByAop, Object... paras) { + try { + Constructor[] constructors = clazz.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + if (isConstructorMatchedParas(constructor, paras)) { +// constructor.setAccessible(true); + Object ret = constructor.newInstance(paras); + if (createByAop) { + Aop.inject(ret); + } + return (T) ret; + } + } + + throw new IllegalArgumentException("Can not matched constructor by paras" + Arrays.toString(paras) + " for class: " + clazz.getName()); + } catch (Exception e) { + LOG.error("Can not new instance for class: " + clazz.getName() + "\n" + e, e); + } + + return null; + } + + + private static boolean isConstructorMatchedParas(Constructor constructor, Object[] paras) { + if (constructor.getParameterCount() == 0) { + return paras == null || paras.length == 0; + } + + if (constructor.getParameterCount() > 0 + && (paras == null || paras.length != constructor.getParameterCount())) { + return false; + } + + Class[] parameterTypes = constructor.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + Class parameterType = parameterTypes[i]; + Object paraObject = paras[i]; + if (paraObject != null && !parameterType.isAssignableFrom(paraObject.getClass())) { + return false; + } + } + + return true; + } + + public static T newInstanceByStaticConstruct(Class clazz) { StaticConstruct staticConstruct = clazz.getAnnotation(StaticConstruct.class); if (staticConstruct == null) { @@ -98,22 +190,21 @@ public class ClassUtil { return newInstanceByStaticConstruct(clazz, staticConstruct); } + public static T newInstanceByStaticConstruct(Class clazz, StaticConstruct staticConstruct) { Method method = getStaticConstruct(staticConstruct.value(), clazz); if (method == null) { - throw new JbootException("can not new instance by static constrauct for class : " + clazz); + throw new JbootException("Can not new instance by static construct for class: " + clazz.getName()); } try { return (T) method.invoke(null, null); } catch (Exception e) { - log.error("can not invoke method:" + method.getName() - + " in class : " - + clazz + "\n" - + e.toString(), e); + LOG.error("Can not invoke method: " + method.getName() + + " in class: " + clazz.getName() + "\n" + e, e); if (e instanceof RuntimeException) { throw (RuntimeException) e; @@ -124,7 +215,7 @@ public class ClassUtil { } - private static Method getStaticConstruct(String name, Class clazz) { + private static Method getStaticConstruct(String name, Class clazz) { Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { if (Modifier.isStatic(method.getModifiers()) @@ -177,28 +268,83 @@ public class ClassUtil { Class clazz = (Class) Class.forName(clazzName, false, classLoader); return newInstance(clazz, createByAop); } catch (Exception e) { - log.error("can not newInstance class:" + clazzName + "\n" + e.toString(), e); + LOG.error("Can not new instance for class: " + clazzName + "\n" + e.toString(), e); } return null; } + private static final String ENHANCER_BY = "$$EnhancerBy"; + private static final String JAVASSIST_BY = "_$$_"; + public static Class getUsefulClass(Class clazz) { //ControllerTest$ServiceTest$$EnhancerByGuice$$40471411#hello //com.demo.blog.Blog$$EnhancerByCGLIB$$69a17158 - return clazz.getName().indexOf("$$EnhancerBy") == -1 ? clazz : clazz.getSuperclass(); + //io.jboot.test.app.TestAppListener_$$_jvstb9f_0 by:javassist + + final String name = clazz.getName(); + if (name.contains(ENHANCER_BY) || name.contains(JAVASSIST_BY)) { + return clazz.getSuperclass(); + } + + return clazz; } - public static Class getGenericClass(Class clazz){ - Type type = getUsefulClass(clazz).getGenericSuperclass(); - return (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + public static ClassType getClassType(Type type, Class runClass) { + if (type instanceof Class) { + return new ClassType((Class) type); + } + + // 泛型定义在参数里,例如 List + else if (type instanceof ParameterizedType) { + ClassType classType = new ClassType((Class) ((ParameterizedType) type).getRawType()); + + Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments(); + ClassType[] genericTypes = new ClassType[actualTypeArguments.length]; + for (int i = 0; i < actualTypeArguments.length; i++) { + genericTypes[i] = getClassType(actualTypeArguments[i], runClass); + } + + classType.setGenericTypes(genericTypes); + return classType; + } + + //泛型定义在 class 里,例如 List,其中 T 是在 class 里的参数 + else if (type instanceof TypeVariable && runClass != null) { + Type variableRawType = getTypeInClassDefined(runClass, ((TypeVariable) type)); + if (variableRawType != null) { + return getClassType(variableRawType, runClass); + } else { + return null; + } + } + + return null; } - public static String buildMethodString(Method method) { + private static Type getTypeInClassDefined(Class runClass, TypeVariable typeVariable) { + Type type = runClass.getGenericSuperclass(); + if (type instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + if (typeArguments.length == 1) { + return typeArguments[0]; + } else if (typeArguments.length > 1) { + TypeVariable[] typeVariables = typeVariable.getGenericDeclaration().getTypeParameters(); + for (int i = 0; i < typeVariables.length; i++) { + if (typeVariable.getName().equals(typeVariables[i].getName())) { + return typeArguments[i]; + } + } + } + } + return null; + } + + public static String buildMethodString(Method method) { StringBuilder sb = new StringBuilder() .append(method.getDeclaringClass().getName()) .append(".") @@ -213,8 +359,24 @@ public class ClassUtil { sb.append(","); } } + return sb.append(")").toString(); + } + + + public static boolean hasClass(String className) { + try { + Class.forName(className, false, getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public static ClassLoader getClassLoader() { + ClassLoader ret = Thread.currentThread().getContextClassLoader(); + return ret != null ? ret : ClassUtil.class.getClassLoader(); } } diff --git a/src/main/java/io/jboot/utils/CollectionUtil.java b/src/main/java/io/jboot/utils/CollectionUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..685587f71f4291f9a00b6447a065edf82523384d --- /dev/null +++ b/src/main/java/io/jboot/utils/CollectionUtil.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import java.util.*; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class CollectionUtil { + + public static Map string2Map(String s) { + Map map = new LinkedHashMap<>(); + String[] strings = s.split(","); + for (String kv : strings) { + if (kv != null && kv.contains(":")) { + String[] keyValue = kv.split(":"); + if (keyValue.length == 2) { + map.put(keyValue[0].trim(), keyValue[1].trim()); + } + } + } + return map; + } + + public static boolean isEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } + + + public static String toString(Collection collection, String delimiter) { + StringJoiner sb = new StringJoiner(delimiter); + if (collection != null) { + for (Object o : collection) { + sb.add(String.valueOf(o)); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/io/jboot/utils/ConfigUtil.java b/src/main/java/io/jboot/utils/ConfigUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..eed5406bee67719ffd46c6fa2f6d62fab6cff960 --- /dev/null +++ b/src/main/java/io/jboot/utils/ConfigUtil.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + + +import io.jboot.app.config.JbootConfigKit; +import io.jboot.app.config.JbootConfigManager; +import io.jboot.app.config.annotation.ConfigModel; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; + + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/19 + */ +public class ConfigUtil { + + public static Map getConfigModels(Class tClass) { + ConfigModel configModel = tClass.getAnnotation(ConfigModel.class); + if (configModel == null) { + throw new IllegalStateException("The Class \"" + tClass.getName() + "\" is not have @ConfigModel annotation"); + } + return getConfigModels(tClass, configModel.prefix()); + } + + + public static Map getConfigModels(Class tClass, String prefix) { + return getConfigModels(JbootConfigManager.me(), tClass, prefix); + } + + + public static Map getConfigModels(JbootConfigManager configManager, Class tClass) { + ConfigModel configModel = tClass.getAnnotation(ConfigModel.class); + if (configModel == null) { + throw new IllegalStateException("The Class \"" + tClass.getName() + "\" is not have @ConfigModel annotation"); + } + return getConfigModels(configManager, tClass, configModel.prefix()); + } + + + public static Map getConfigModels(JbootConfigManager configManager, Class tClass, String prefix) { + Map objMap = new HashMap<>(); + + boolean initDefault = false; + + Set objNames = new HashSet<>(); + + String objPrefix = prefix + "."; + int pointCount = StringUtils.countMatches(prefix, '.'); + + Properties prop = configManager.getProperties(); + + for (Map.Entry entry : prop.entrySet()) { + if (entry.getKey() == null || JbootConfigKit.isBlank(entry.getKey().toString())) { + continue; + } + + String key = entry.getKey().toString().trim(); + + //配置来源于 Docker 的环境变量配置 + if (key.contains("_") && Character.isUpperCase(key.charAt(0))) { + key = key.toLowerCase().replace('_', '.'); + } + + //初始化默认的配置 + if (!initDefault && key.startsWith(prefix) && StringUtils.countMatches(key, '.') == pointCount + 1) { + initDefault = true; + T defaultObj = configManager.get(tClass, prefix, null); + objMap.put("default", defaultObj); + + } + + if (key.startsWith(objPrefix) && entry.getValue() != null) { + String[] keySplits = key.split("\\."); + if (keySplits.length == pointCount + 3) { + objNames.add(keySplits[pointCount + 1]); + } + } + } + + for (String name : objNames) { + T obj = configManager.get(tClass, objPrefix + name, null); + objMap.put(name, obj); + } + + return objMap; + } +} diff --git a/src/main/java/io/jboot/utils/CookieUtil.java b/src/main/java/io/jboot/utils/CookieUtil.java index e1c349da353c9e2b42858f22df6d22c525ae7897..228e0b6212f244f9a7b6bcf215b2189b6c60b55a 100644 --- a/src/main/java/io/jboot/utils/CookieUtil.java +++ b/src/main/java/io/jboot/utils/CookieUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,15 @@ import com.jfinal.core.Controller; import com.jfinal.kit.Base64Kit; import com.jfinal.kit.HashKit; import com.jfinal.kit.LogKit; +import com.jfinal.kit.StrKit; import com.jfinal.log.Log; import io.jboot.Jboot; +import io.jboot.exception.JbootIllegalConfigException; import io.jboot.web.JbootWebConfig; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.math.BigInteger; /** @@ -35,14 +40,21 @@ import java.math.BigInteger; * 加密的cookie工具类 */ public class CookieUtil { + private static final Log LOG = Log.getLog(CookieUtil.class); private final static String COOKIE_SEPARATOR = "#"; - private static String COOKIE_ENCRYPT_KEY = Jboot.config(JbootWebConfig.class).getCookieEncryptKey(); - private static Log log = Log.getLog(CookieUtil.class); + // cookie 加密秘钥 + private static String COOKIE_ENCRYPT_KEY = JbootWebConfig.getInstance().getCookieEncryptKey(); - // 2 days - private static int COOKIE_MAX_AGE = 60 * 60 * 24 * 2; + // 2 days(单位:秒) + private static int COOKIE_MAX_AGE = JbootWebConfig.getInstance().getCookieMaxAge(); + + // 默认的路径, null 默认路径 "/" + private static String defaultPath = null; + + // 默认的域名, null 为当前域名 + private static String defaultDomain = null; /** @@ -54,8 +66,13 @@ public class CookieUtil { COOKIE_ENCRYPT_KEY = key; } + + public static String getEncryptKey() { + return COOKIE_ENCRYPT_KEY; + } + /** - * 设置 默认的 Cookie 有效时间 + * 设置 默认的 Cookie 有效时间,单位:秒 * * @param seconds */ @@ -63,46 +80,89 @@ public class CookieUtil { COOKIE_MAX_AGE = seconds; } + public static int getDefaultCookieMaxAge() { + return COOKIE_MAX_AGE; + } + + + public static String getDefaultPath() { + return defaultPath; + } + + public static void setDefaultPath(String defaultPath) { + CookieUtil.defaultPath = defaultPath; + } + + public static String getDefaultDomain() { + return defaultDomain; + } + + public static void setDefaultDomain(String defaultDomain) { + CookieUtil.defaultDomain = defaultDomain; + } public static void put(Controller ctr, String key, Object value) { - put(ctr, key, value, COOKIE_MAX_AGE, null, null, COOKIE_ENCRYPT_KEY); + put(ctr, key, value, COOKIE_MAX_AGE, defaultPath, defaultDomain, COOKIE_ENCRYPT_KEY); + } + + public static void put(HttpServletResponse response, String key, Object value) { + put(response, key, value, COOKIE_MAX_AGE, defaultPath, defaultDomain, COOKIE_ENCRYPT_KEY); + } + + public static void put(HttpServletResponse response, String key, Object value, int maxAgeInSeconds) { + put(response, key, value, maxAgeInSeconds, defaultPath, defaultDomain, COOKIE_ENCRYPT_KEY); } public static void put(Controller ctr, String key, Object value, String secretKey) { - put(ctr, key, value, COOKIE_MAX_AGE, null, null, secretKey); + put(ctr, key, value, COOKIE_MAX_AGE, defaultPath, defaultDomain, secretKey); } public static void put(Controller ctr, String key, String value, int maxAgeInSeconds) { - put(ctr, key, value, maxAgeInSeconds, null, null, COOKIE_ENCRYPT_KEY); + put(ctr, key, value, maxAgeInSeconds, defaultPath, defaultDomain, COOKIE_ENCRYPT_KEY); } public static void put(Controller ctr, String key, String value, int maxAgeInSeconds, String secretKey) { - put(ctr, key, value, maxAgeInSeconds, null, null, secretKey); + put(ctr, key, value, maxAgeInSeconds, defaultPath, defaultDomain, secretKey); } public static void put(Controller ctr, String key, Object value, int maxAgeInSeconds, String path, String domain, String secretKey) { - String cookie = buildCookieValue(value.toString(), maxAgeInSeconds, secretKey); - ctr.setCookie(key, cookie, maxAgeInSeconds, path, domain, true); + put(ctr.getResponse(), key, value, maxAgeInSeconds, path, domain, secretKey); } + public static void put(HttpServletResponse response, String key, Object value, int maxAgeInSeconds, String path, String domain, String secretKey) { + String cookie = buildCookieValue(value.toString(), maxAgeInSeconds, secretKey); + doSetCookie(response, key, cookie, maxAgeInSeconds, path, domain, true); + } public static void remove(Controller ctr, String key) { - ctr.removeCookie(key); + doSetCookie(ctr.getResponse(), key, null, 0, null, null, null); + } + + public static void remove(HttpServletResponse response, String key) { + doSetCookie(response, key, null, 0, null, null, null); } public static void remove(Controller ctr, String key, String path, String domain) { - ctr.removeCookie(key, path, domain); + doSetCookie(ctr.getResponse(), key, null, 0, path, domain, null); } public static String get(Controller ctr, String key) { return get(ctr, key, COOKIE_ENCRYPT_KEY); } + public static String get(HttpServletRequest request, String key) { + return get(request, key, COOKIE_ENCRYPT_KEY); + } + public static String get(Controller ctr, String key, String secretKey) { - String cookieValue = ctr.getCookie(key); + return get(ctr.getRequest(), key, secretKey); + } + + public static String get(HttpServletRequest request, String key, String secretKey) { + String cookieValue = getCookie(request, key, null); if (cookieValue == null) { return null; @@ -139,8 +199,17 @@ public class CookieUtil { } private static String encrypt(String secretKey, Object saveTime, Object maxAgeInSeconds, String value) { + + // 若使用了默认的加密秘钥 if (JbootWebConfig.DEFAULT_COOKIE_ENCRYPT_KEY.equals(secretKey)) { - log.warn("warn!!! encrypt key is defalut value. please config \"jboot.web.cookieEncryptKey = xxx\" in jboot.properties "); + + if (Jboot.isDevMode()) { + LOG.warn("Warn!!! encrypt key is defalut value. please config \"jboot.web.cookieEncryptKey = xxx\" in jboot.properties"); + } + // 生产环境下,直接抛出错误! + else { + throw new JbootIllegalConfigException("Error!!! Cookie encrypt key is not configured. please config \"jboot.web.cookieEncryptKey = xxx\" in jboot.properties"); + } } return HashKit.md5(secretKey + saveTime.toString() + maxAgeInSeconds.toString() + value); } @@ -152,7 +221,7 @@ public class CookieUtil { } String[] cookieStrings = cookieValue.split(COOKIE_SEPARATOR); - if (cookieStrings == null || cookieStrings.length != 4) { + if (cookieStrings.length != 4) { return null; } @@ -168,16 +237,19 @@ public class CookieUtil { return null; } - long stime = Long.parseLong(saveTime); - long maxtime = Long.parseLong(maxAgeInSeconds) * 1000; + long maxTimeMillis = Long.parseLong(maxAgeInSeconds) * 1000; + //可能设置的时间为 0 或者 -1 + if (maxTimeMillis <= 0) { + return value; + } + + long saveTimeMillis = Long.parseLong(saveTime); // 查看是否过时 - if ((stime + maxtime) - System.currentTimeMillis() > 0) { + if ((saveTimeMillis + maxTimeMillis) - System.currentTimeMillis() > 0) { return value; } - /** - * 已经超时了 - */ + //已经超时了 else { return null; } @@ -213,4 +285,45 @@ public class CookieUtil { return null == value ? defalut : new BigInteger(value); } + + private static String getCookie(HttpServletRequest request, String name, String defaultValue) { + Cookie cookie = getCookieObject(request, name); + return cookie != null ? cookie.getValue() : defaultValue; + } + + /** + * Get cookie object by cookie name. + */ + private static Cookie getCookieObject(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return cookie; + } + } + } + return null; + } + + + private static void doSetCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds, String path, String domain, Boolean isHttpOnly) { + Cookie cookie = new Cookie(name, value); + cookie.setMaxAge(maxAgeInSeconds); + // set the default path value to "/" + if (StrKit.isBlank(path)) { + path = "/"; + } + cookie.setPath(path); + + if (domain != null) { + cookie.setDomain(domain); + } + if (isHttpOnly != null) { + cookie.setHttpOnly(isHttpOnly); + } + response.addCookie(cookie); + } + + } diff --git a/src/main/java/io/jboot/utils/DESUtil.java b/src/main/java/io/jboot/utils/DESUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..f648c4190155f52f9434c0fe8e691fd7801b9ede --- /dev/null +++ b/src/main/java/io/jboot/utils/DESUtil.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import java.security.SecureRandom; + + +/** + * DESUtil 对称加密工具类,非对称加密请参考 RSAUtil + */ +public class DESUtil { + + + /** + * 加密 + * + * @param data + * @param key + * @return + * @throws Exception + */ + public static byte[] encrypt(byte[] data, byte[] key) throws Exception { + Cipher cipher = Cipher.getInstance("DES"); + DESKeySpec ds = new DESKeySpec(key); + SecureRandom sr = new SecureRandom(); + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey skey = secretKeyFactory.generateSecret(ds); + cipher.init(Cipher.ENCRYPT_MODE, skey, sr); + return cipher.doFinal(data); + } + + + /** + * 加密 + * + * @param data + * @param key + * @return + * @throws Exception + */ + public static String encrypt(String data, String key) throws Exception { + return byte2hex(encrypt(data.getBytes(), key.getBytes())); + } + + + /** + * 解密 + * + * @param data + * @param key + * @return + * @throws Exception + */ + public static byte[] decrypt(byte[] data, byte[] key) throws Exception { + Cipher cipher = Cipher.getInstance("DES"); + DESKeySpec ds = new DESKeySpec(key); + SecureRandom sr = new SecureRandom(); + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey skey = secretKeyFactory.generateSecret(ds); + cipher.init(Cipher.DECRYPT_MODE, skey, sr); + return cipher.doFinal(data); + } + + + /** + * 解密 + * + * @param data + * @param key + * @return + * @throws Exception + */ + public static String decrypt(String data, String key) throws Exception { + return new String(decrypt(hex2byte(data.getBytes()), key.getBytes())); + } + + + private static String byte2hex(byte[] b) { + StringBuilder hs = new StringBuilder(); + String stmp; + for (int n = 0; b != null && n < b.length; n++) { + stmp = Integer.toHexString(b[n] & 0XFF); + if (stmp.length() == 1) { + hs.append('0'); + } + hs.append(stmp); + } + return hs.toString().toUpperCase(); + } + + + private static byte[] hex2byte(byte[] b) { + if ((b.length % 2) != 0) { + throw new IllegalArgumentException(); + } + byte[] b2 = new byte[b.length / 2]; + for (int n = 0; n < b.length; n += 2) { + String item = new String(b, n, 2); + b2[n / 2] = (byte) Integer.parseInt(item, 16); + } + return b2; + } +} diff --git a/src/main/java/io/jboot/utils/DateUtil.java b/src/main/java/io/jboot/utils/DateUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..4f557579ef2494e11118d3c0d6a8847cb603e6d8 --- /dev/null +++ b/src/main/java/io/jboot/utils/DateUtil.java @@ -0,0 +1,1089 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import com.jfinal.kit.SyncWriteMap; + +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class DateUtil { + + public static String datePatternWithoutDividing = "yyyyMMdd"; + public static String datePattern = "yyyy-MM-dd"; + public static final String dateMinutePattern = "yyyy-MM-dd HH:mm"; + public static final String dateMinutePattern2 = "yyyy-MM-dd'T'HH:mm"; + public static String datetimePattern = "yyyy-MM-dd HH:mm:ss"; + public static final String dateMillisecondPattern = "yyyy-MM-dd HH:mm:ss SSS"; + public static final String dateCSTPattern = "EEE MMM dd HH:mm:ss zzz yyyy"; + + public static String dateChinesePattern = "yyyy年MM月dd日"; + public static String datetimeChinesePattern = "yyyy年MM月dd日 HH时mm分ss秒"; + + private static final String[] WEEKS = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"}; + + private static final ThreadLocal> TL = ThreadLocal.withInitial(() -> new HashMap<>()); + + private static final Map dateTimeFormatters = new SyncWriteMap<>(); + + public static DateTimeFormatter getDateTimeFormatter(String pattern) { + DateTimeFormatter ret = dateTimeFormatters.get(pattern); + if (ret == null) { + ret = DateTimeFormatter.ofPattern(pattern); + dateTimeFormatters.put(pattern, ret); + } + return ret; + } + + public static SimpleDateFormat getSimpleDateFormat(String pattern) { + SimpleDateFormat ret = TL.get().get(pattern); + if (ret == null) { + if (dateCSTPattern.equals(pattern)) { + ret = new SimpleDateFormat(dateCSTPattern, Locale.US); + } else { + ret = new SimpleDateFormat(pattern); + } + TL.get().put(pattern, ret); + } + return ret; + } + + + public static String toDateString(Date date) { + return toString(date, datePattern); + } + + + public static String toDateMinuteString(Date date) { + return toString(date, dateMinutePattern); + } + + public static String toDateTimeString(Date date) { + return toString(date, datetimePattern); + } + + + public static String toDateMillisecondString(Date date) { + return toString(date, dateMillisecondPattern); + } + + + public static String toString(Date date, String pattern) { + return date == null ? null : getSimpleDateFormat(pattern).format(date); + } + + + public static String toString(LocalDateTime localDateTime, String pattern) { + return localDateTime.format(getDateTimeFormatter(pattern)); + } + + public static String toString(LocalDate localDate, String pattern) { + return localDate.format(getDateTimeFormatter(pattern)); + } + + public static String toString(LocalTime localTime, String pattern) { + return localTime.format(getDateTimeFormatter(pattern)); + } + + + public static Date parseDate(Object value) { + if (value instanceof Number) { + return new Date(((Number) value).longValue()); + } + if (value instanceof Timestamp) { + return new Date(((Timestamp) value).getTime()); + } + if (value instanceof LocalDate) { + return DateUtil.toDate((LocalDate) value); + } + if (value instanceof LocalDateTime) { + return DateUtil.toDate((LocalDateTime) value); + } + if (value instanceof LocalTime) { + return DateUtil.toDate((LocalTime) value); + } + String s = value.toString(); + if (StrUtil.isNumeric(s)) { + return new Date(Long.parseLong(s)); + } + return DateUtil.parseDate(s); + } + + + public static Date parseDate(String dateString) { + if (StrUtil.isBlank(dateString)) { + return null; + } + dateString = dateString.trim(); + try { + SimpleDateFormat sdf = getSimpleDateFormat(getPattern(dateString)); + try { + return sdf.parse(dateString); + } catch (ParseException ex) { + //2022-10-23 00:00:00.0 + int lastIndexOf = dateString.lastIndexOf("."); + if (lastIndexOf == 19) { + return parseDate(dateString.substring(0, lastIndexOf)); + } + + //2022-10-23 00:00:00,0 + lastIndexOf = dateString.lastIndexOf(","); + if (lastIndexOf == 19) { + return parseDate(dateString.substring(0, lastIndexOf)); + } + + //2022-10-23 00:00:00 000123 + lastIndexOf = dateString.lastIndexOf(" "); + if (lastIndexOf == 19) { + return parseDate(dateString.substring(0, lastIndexOf)); + } + + if (dateString.contains(".") || dateString.contains("/")) { + dateString = dateString.replace(".", "-").replace("/", "-"); + return sdf.parse(dateString); + } else { + throw ex; + } + } + } catch (ParseException ex) { + throw new IllegalArgumentException("The date format is not supported for the date string: " + dateString); + } + } + + + private static String getPattern(String dateString) { + int length = dateString.length(); + if (length == datetimePattern.length()) { + return datetimePattern; + } else if (length == datePattern.length()) { + return datePattern; + } else if (length == dateMinutePattern.length()) { + if (dateString.contains("T")) { + return dateMinutePattern2; + } + return dateMinutePattern; + } else if (length == dateMillisecondPattern.length()) { + return dateMillisecondPattern; + } else if (length == datePatternWithoutDividing.length()) { + return datePatternWithoutDividing; + } else if (length == dateCSTPattern.length()) { + return dateCSTPattern; + } else { + throw new IllegalArgumentException("The date format is not supported for the date string: " + dateString); + } + } + + + public static Date parseDate(String dateString, String pattern) { + if (StrUtil.isBlank(dateString)) { + return null; + } + try { + return getSimpleDateFormat(pattern).parse(dateString.trim()); + } catch (ParseException e) { + throw new IllegalArgumentException("The date format is not supported for the date string: " + dateString); + } + } + + + public static LocalDateTime parseLocalDateTime(String localDateTimeString, String pattern) { + return LocalDateTime.parse(localDateTimeString, getDateTimeFormatter(pattern)); + } + + public static LocalDate parseLocalDate(String localDateString, String pattern) { + return LocalDate.parse(localDateString, getDateTimeFormatter(pattern)); + } + + + public static LocalTime parseLocalTime(String localTimeString, String pattern) { + return LocalTime.parse(localTimeString, getDateTimeFormatter(pattern)); + } + + + /** + * java.util.Date --> java.time.LocalDateTime + */ + public static LocalDateTime toLocalDateTime(Date date) { + if (date == null) { + return null; + } + // java.sql.Date 不支持 toInstant(),需要先转换成 java.util.Date + if (date instanceof java.sql.Date) { + date = new Date(date.getTime()); + } + + Instant instant = date.toInstant(); + ZoneId zone = ZoneId.systemDefault(); + return LocalDateTime.ofInstant(instant, zone); + } + + /** + * java.util.Date --> java.time.LocalDate + */ + public static LocalDate toLocalDate(Date date) { + if (date == null) { + return null; + } + // java.sql.Date 不支持 toInstant(),需要先转换成 java.util.Date + if (date instanceof java.sql.Date) { + date = new Date(date.getTime()); + } + + Instant instant = date.toInstant(); + ZoneId zone = ZoneId.systemDefault(); + LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone); + return localDateTime.toLocalDate(); + } + + /** + * java.util.Date --> java.time.LocalTime + */ + public static LocalTime toLocalTime(Date date) { + if (date == null) { + return null; + } + // java.sql.Date 不支持 toInstant(),需要先转换成 java.util.Date + if (date instanceof java.sql.Date) { + date = new Date(date.getTime()); + } + + Instant instant = date.toInstant(); + ZoneId zone = ZoneId.systemDefault(); + LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone); + return localDateTime.toLocalTime(); + } + + /** + * java.time.LocalDateTime --> java.util.Date + */ + public static Date toDate(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + ZoneId zone = ZoneId.systemDefault(); + Instant instant = localDateTime.atZone(zone).toInstant(); + return Date.from(instant); + } + + /** + * java.time.LocalDate --> java.util.Date + */ + public static Date toDate(LocalDate localDate) { + if (localDate == null) { + return null; + } + ZoneId zone = ZoneId.systemDefault(); + Instant instant = localDate.atStartOfDay().atZone(zone).toInstant(); + return Date.from(instant); + } + + /** + * java.time.LocalTime --> java.util.Date + */ + public static Date toDate(LocalTime localTime) { + if (localTime == null) { + return null; + } + LocalDate localDate = LocalDate.now(); + LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime); + ZoneId zone = ZoneId.systemDefault(); + Instant instant = localDateTime.atZone(zone).toInstant(); + return Date.from(instant); + } + + /** + * java.time.LocalTime --> java.util.Date + */ + public static Date toDate(LocalDate localDate, LocalTime localTime) { + if (localDate == null) { + return null; + } + + if (localTime == null) { + localTime = LocalTime.of(0, 0, 0); + } + + LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime); + ZoneId zone = ZoneId.systemDefault(); + Instant instant = localDateTime.atZone(zone).toInstant(); + return Date.from(instant); + } + + + /** + * 任意一天的开始时间 + * + * @return date + */ + public static Date getStartOfDay(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + /** + * 任意一天的结束时间 + * + * @return date + */ + public static Date getEndOfDay(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR_OF_DAY, 24); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + + /** + * 获取今天的开始时间 + * + * @return + */ + public static Date getStartOfToday() { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } + + + /** + * 获取昨天的开始时间 + * + * @return + */ + public static Date getStartOfYesterday() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(getStartOfToday().getTime() - 3600L * 24 * 1000); + return cal.getTime(); + } + + + /** + * 获取最近 7 天的开始时间 + * + * @return + */ + public static Date getStartOfNearest7Days() { + return getStartOfNearestDays(7); + } + + + /** + * 获取最近 N 天的开始时间 + * + * @param days + * @return + */ + public static Date getStartOfNearestDays(int days) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(getStartOfToday().getTime() - 3600L * 24 * 1000 * days); + return cal.getTime(); + } + + + /** + * 获取今天的结束数据 + * + * @return + */ + public static Date getEndOfToday() { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, 24); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } + + + /** + * 获取 本周 的开始时间 + * + * @return + */ + public static Date getStartOfThisWeek() { + Calendar cal = Calendar.getInstance(); + cal.setFirstDayOfWeek(Calendar.MONDAY); + cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONDAY), cal.get(Calendar.DAY_OF_MONTH), 0, 0, 0); + cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + return cal.getTime(); + } + + /** + * 获取 本周 的结束时间 + * + * @return + */ + public static Date getEndOfThisWeek() { + Calendar cal = Calendar.getInstance(); + cal.setFirstDayOfWeek(Calendar.MONDAY); + cal.setTime(getStartOfThisWeek()); + cal.add(Calendar.DAY_OF_WEEK, 7); + return cal.getTime(); + } + + + /** + * 获取 本月 的开始时间 + * + * @return + */ + public static Date getStartOfThisMonth() { + Calendar cal = Calendar.getInstance(); + cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONDAY), cal.get(Calendar.DAY_OF_MONTH), 0, 0, 0); + cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH)); + return cal.getTime(); + } + + /** + * 获取 本月 的结束时间 + * + * @return + */ + public static Date getEndOfThisMonth() { + Calendar cal = Calendar.getInstance(); + cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONDAY), cal.get(Calendar.DAY_OF_MONTH), 0, 0, 0); + cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH)); + cal.set(Calendar.HOUR_OF_DAY, 24); + return cal.getTime(); + } + + + /** + * 获取上个月的开始时间 + * + * @return + */ + public static Date getStartOfLastMonth() { + Calendar cal = Calendar.getInstance(); + cal.setTime(getStartOfThisMonth()); + cal.add(Calendar.MONTH, -1); + return cal.getTime(); + } + + + /** + * 获取 本季度 的开始时间 + * + * @return + */ + public static Date getStartOfThisQuarter() { + Calendar cal = Calendar.getInstance(); + int currentMonth = cal.get(Calendar.MONTH) + 1; + if (currentMonth <= 3) { + cal.set(Calendar.MONTH, 0); + } else if (currentMonth <= 6) { + cal.set(Calendar.MONTH, 3); + } else if (currentMonth <= 9) { + cal.set(Calendar.MONTH, 6); + } else if (currentMonth <= 12) { + cal.set(Calendar.MONTH, 9); + } + cal.set(Calendar.DATE, 0); + + cal.set(Calendar.HOUR_OF_DAY, 24); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.MILLISECOND, 0); + + return cal.getTime(); + } + + + /** + * 获取 本季度的 结束时间 + * + * @return + */ + public static Date getEndOfThisQuarter() { + Calendar cal = Calendar.getInstance(); + cal.setTime(getStartOfThisQuarter()); + cal.add(Calendar.MONTH, 3); + return cal.getTime(); + } + + + /** + * 获取 季度 的开始时间 + * + * @param quarterNumber + * @return + */ + public static Date getStartOfQuarter(int quarterNumber) { + if (quarterNumber < 1 || quarterNumber > 4) { + throw new IllegalArgumentException("quarterNumber must equals 1,2,3,4"); + } + Calendar cal = Calendar.getInstance(); + if (quarterNumber == 1) { + cal.set(Calendar.MONTH, 0); + } else if (quarterNumber == 2) { + cal.set(Calendar.MONTH, 3); + } else if (quarterNumber == 3) { + cal.set(Calendar.MONTH, 6); + } else { + cal.set(Calendar.MONTH, 9); + } + + cal.set(Calendar.DATE, 0); + cal.set(Calendar.HOUR_OF_DAY, 24); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.MILLISECOND, 0); + + return cal.getTime(); + } + + + /** + * 获取 季度 的结束时间 + * + * @param quarterNumber + * @return + */ + public static Date getEndOfQuarter(int quarterNumber) { + Calendar cal = Calendar.getInstance(); + cal.setTime(getStartOfQuarter(quarterNumber)); + cal.add(Calendar.MONTH, 3); + return cal.getTime(); + } + + + /** + * 获取 今年 的开始时间 + * + * @return + */ + public static Date getStartOfThisYear() { + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date()); + cal.set(cal.get(Calendar.YEAR), 0, 1, 0, 0, 0); + return cal.getTime(); + } + + + /** + * 获取 今年 的结束时间 + * + * @return + */ + public static Date getEndOfThisYear() { + Calendar cal = Calendar.getInstance(); + cal.setTime(getStartOfThisYear()); + cal.add(Calendar.YEAR, 1); + return cal.getTime(); + } + + + /** + * 获取 去年的 开始时间 + * + * @return + */ + public static Date getStartOfLastYear() { + Calendar cal = Calendar.getInstance(); + cal.setTime(getStartOfThisYear()); + cal.add(Calendar.YEAR, -1); + return cal.getTime(); + } + + /** + * 获取两个时间直接的间隔:单位 秒 + * + * @param date1 + * @param date2 + * @return + */ + public static int diffSecond(Date date1, Date date2) { + long date1ms = date1.getTime(); + long date2ms = date2.getTime(); + return Math.abs((int) ((date1ms - date2ms) / (1000))); + } + + /** + * 获取两个时间直接的间隔:单位 分钟 + * + * @param date1 + * @param date2 + * @return + */ + public static int diffMinute(Date date1, Date date2) { + long date1ms = date1.getTime(); + long date2ms = date2.getTime(); + return Math.abs((int) ((date1ms - date2ms) / (1000 * 60))); + } + + + /** + * 获取两个时间直接的间隔:单位 小时 + * + * @param date1 + * @param date2 + * @return + */ + public static int diffHours(Date date1, Date date2) { + long date1ms = date1.getTime(); + long date2ms = date2.getTime(); + return Math.abs((int) ((date1ms - date2ms) / (1000 * 60 * 60))); + } + + /** + * 获取两个时间直接的间隔:单位 天 + * + * @param date1 + * @param date2 + * @return + */ + public static int diffDays(Date date1, Date date2) { + long date1ms = date1.getTime(); + long date2ms = date2.getTime(); + return Math.abs((int) ((date1ms - date2ms) / (1000 * 60 * 60 * 24))); + } + + /** + * 获取两个时间直接的间隔:单位 星期 + * + * @param date1 + * @param date2 + * @return + */ + public static int diffWeeks(Date date1, Date date2) { + long date1ms = date1.getTime(); + long date2ms = date2.getTime(); + return Math.abs((int) ((date1ms - date2ms) / (1000 * 60 * 60 * 24 * 7))); + } + + /** + * 获取两个时间直接的间隔:单位 月 + * + * @param date1 + * @param date2 + * @return + */ + public static int diffMonths(Date date1, Date date2) { + int diffYears = diffYears(date1, date2) * 12; + int number1 = getMonthNumber(date1); + int number2 = getMonthNumber(date2); + return Math.abs(diffYears + number1 - number2); + } + + /** + * 获取两个时间直接的间隔:单位 年 + * + * @param date1 + * @param date2 + * @return + */ + public static int diffYears(Date date1, Date date2) { + int number1 = getYearNumber(date1); + int number2 = getYearNumber(date2); + return Math.abs(number1 - number2); + } + + + /** + * 获取日期的月份 + * + * @param date + * @return + */ + public static int getMonthNumber(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(Calendar.MONTH) + 1; + } + + + /** + * 获取日期的季度 + * + * @param date + * @return + */ + public static int getQuarterNumber(Date date) { + int monthNumber = getMonthNumber(date); + if (monthNumber >= 1 && monthNumber <= 3) { + return 1; + } else if (monthNumber >= 4 && monthNumber <= 6) { + return 2; + } else if (monthNumber >= 7 && monthNumber <= 9) { + return 3; + } else { + return 4; + } + } + + + /** + * 获取日期的年份 + * + * @param date + * @return + */ + public static int getYearNumber(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(Calendar.YEAR); + } + + + /** + * 获取日期的是当年的第几天 + * + * @param date + * @return + */ + public static int getDayOfYearNumber(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(Calendar.DAY_OF_YEAR); + } + + /** + * 获取日期的当月的第几天 + * + * @param date + * @return + */ + public static int getDayOfMonthNumber(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return cal.get(Calendar.DAY_OF_MONTH); + } + + /** + * 获取日期的当星期的第几天 + * + * @param date + * @return + */ + public static int getDayOfWeekNumber(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setFirstDayOfWeek(Calendar.MONDAY); + cal.setTime(date); + return cal.get(Calendar.DAY_OF_WEEK); + } + + + /** + * 获取日期的是当年的第个星期 + * + * @param date + * @return + */ + public static int getWeekOfYearNumber(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setFirstDayOfWeek(Calendar.MONDAY); + cal.setTime(date); + return cal.get(Calendar.WEEK_OF_YEAR); + } + + /** + * 获取日期的当月的第几星期 + * + * @param date + * @return + */ + public static int getWeekOfMonthNumber(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setFirstDayOfWeek(Calendar.MONDAY); + cal.setTime(date); + return cal.get(Calendar.WEEK_OF_MONTH); + } + + + /** + * 取得在指定时间上加减seconds天后的时间 + * + * @param date 指定的时间 + * @param seconds 秒钟,正为加,负为减 + * @return 在指定时间上加减seconds天后的时间 + */ + public static Date addSeconds(Date date, int seconds) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.SECOND, seconds); + return cal.getTime(); + } + + + /** + * 取得在指定时间上加减minutes天后的时间 + * + * @param date 指定的时间 + * @param minutes 分钟,正为加,负为减 + * @return 在指定时间上加减minutes天后的时间 + */ + public static Date addMinutes(Date date, int minutes) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.MINUTE, minutes); + return cal.getTime(); + } + + /** + * 取得在指定时间上加减hours天后的时间 + * + * @param date 指定的时间 + * @param hours 小时,正为加,负为减 + * @return 在指定时间上加减dhours天后的时间 + */ + public static Date addHours(Date date, int hours) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.HOUR, hours); + return cal.getTime(); + } + + /** + * 取得在指定时间上加减days天后的时间 + * + * @param date 指定的时间 + * @param days 天数,正为加,负为减 + * @return 在指定时间上加减days天后的时间 + */ + public static Date addDays(Date date, int days) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.DAY_OF_MONTH, days); + return cal.getTime(); + } + + /** + * 取得在指定时间上加减weeks天后的时间 + * + * @param date 指定的时间 + * @param weeks 星期,正为加,负为减 + * @return 在指定时间上加减weeks天后的时间 + */ + public static Date addWeeks(Date date, int weeks) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.WEEK_OF_YEAR, weeks); + return cal.getTime(); + } + + /** + * 取得在指定时间上加减months月后的时间 + * + * @param date 指定时间 + * @param months 月数,正为加,负为减 + * @return 在指定时间上加减months月后的时间 + */ + public static Date addMonths(Date date, int months) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.MONTH, months); + return cal.getTime(); + } + + /** + * 取得在指定时间上加减years年后的时间 + * + * @param date 指定时间 + * @param years 年数,正为加,负为减 + * @return 在指定时间上加减years年后的时间 + */ + public static Date addYears(Date date, int years) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.YEAR, years); + return cal.getTime(); + } + + + /** + * 判断 A 的时间是否在 B 的时间 "之后" + */ + public static boolean isAfter(Date self, Date other) { + return self != null && other != null && self.getTime() > other.getTime(); + } + + /** + * 判断 A 的时间是否在 B 的时间 "之后" + */ + public static boolean isBefore(Date self, Date other) { + return self != null && other != null && self.getTime() < other.getTime(); + } + + /** + * 是否是相同的一天 + */ + public static boolean isSameDay(Date self, Date other) { + return self != null && other != null && getYearNumber(self) == getYearNumber(other) + && getDayOfYearNumber(self) == getDayOfYearNumber(other); + } + + /** + * 是否是相同的星期 + */ + public static boolean isSameWeek(Date self, Date other) { + return self != null && other != null && getYearNumber(self) == getYearNumber(other) + && getWeekOfYearNumber(self) == getWeekOfYearNumber(other); + } + + /** + * 是否是相同的月份 + */ + public static boolean isSameMonth(Date self, Date other) { + return self != null && other != null && getYearNumber(self) == getYearNumber(other) + && getMonthNumber(self) == getMonthNumber(other); + } + + /** + * 是否是相同的月份 + */ + public static boolean isSameQuarter(Date self, Date other) { + return self != null && other != null && getYearNumber(self) == getYearNumber(other) + && getQuarterNumber(self) == getQuarterNumber(other); + } + + /** + * 是否是相同的月份 + */ + public static boolean isSameYear(Date self, Date other) { + return self != null && other != null && getYearNumber(self) == getYearNumber(other); + } + + + /** + * 此日期是否是今天 + */ + public static boolean isToday(Date date) { + return isSameDay(new Date(), date); + } + + /** + * 此日期是否是本星期 + */ + public static boolean isThisWeek(Date date) { + return isSameWeek(new Date(), date); + } + + /** + * 此日期是否是本月份 + */ + public static boolean isThisMonth(Date date) { + return isSameMonth(new Date(), date); + } + + + /** + * 此日期是否是本月份 + */ + public static boolean isThisQuarter(Date date) { + return isSameQuarter(new Date(), date); + } + + /** + * 此日期是否是本年份 + */ + public static boolean isThisYear(Date date) { + return date != null && getYearNumber(new Date()) == getYearNumber(date); + } + + /** + * 判断是否是润年 + */ + public static boolean isLeapYear(Date date) { + return date != null && new GregorianCalendar().isLeapYear(getYearNumber(date)); + } + + /** + * 求出指定的时间那天是星期几 + */ + public static String getWeekDay(Date date) { + return date == null ? null : DateUtil.WEEKS[getDayOfWeekNumber(date) - 1]; + } + + + public static void main(String[] args) { + System.out.println("两天后的开始时间:" + toDateTimeString(getStartOfDay(addDays(new Date(), 2)))); + System.out.println("两天后的结束时间:" + toDateTimeString(getEndOfDay(addDays(new Date(), 2)))); + + System.out.println("CST时间解析:" + toDateTimeString(parseDate("Mon Sep 02 11:23:45 CST 2019"))); + + + System.out.println("当天24点时间:" + toDateTimeString(getEndOfToday())); + System.out.println("当前时间:" + toDateTimeString(new Date())); + System.out.println("当天0点时间:" + toDateTimeString(getStartOfToday())); + System.out.println("昨天0点时间:" + toDateTimeString(getStartOfYesterday())); + System.out.println("近7天时间:" + toDateTimeString(getStartOfNearest7Days())); + System.out.println("本周周一0点时间:" + toDateTimeString(getStartOfThisWeek())); + System.out.println("本周周日24点时间:" + toDateTimeString(getEndOfThisWeek())); + System.out.println("本月初0点时间:" + toDateTimeString(getStartOfThisMonth())); + System.out.println("本月未24点时间:" + toDateTimeString(getEndOfThisMonth())); + System.out.println("上月初0点时间:" + toDateTimeString(getStartOfLastMonth())); + System.out.println("本季度开始点时间:" + toDateTimeString(getStartOfThisQuarter())); + System.out.println("本季度结束点时间:" + toDateTimeString(getEndOfThisQuarter())); + System.out.println("本年开始点时间:" + toDateTimeString(getStartOfThisYear())); + System.out.println("本年结束点时间:" + toDateTimeString(getEndOfThisYear())); + System.out.println("上年开始点时间:" + toDateTimeString(getStartOfLastYear())); + System.out.println("============="); + System.out.println("秒间隔:" + diffSecond(parseDate("2020-02-11 12:21:55"), parseDate("2020-02-11 12:22:58"))); + System.out.println("分钟间隔:" + diffMinute(parseDate("2020-02-11 12:21:55"), parseDate("2020-02-11 12:22:01"))); + System.out.println("小时间隔:" + diffHours(parseDate("2020-02-11 12:21:55"), parseDate("2020-02-12 12:22:01"))); + System.out.println("天间隔:" + diffDays(parseDate("2020-02-11 12:21:55"), parseDate("2020-02-12 12:22:01"))); + System.out.println("星期间隔:" + diffWeeks(parseDate("2020-01-11 12:21:55"), parseDate("2020-02-12 12:22:01"))); + System.out.println("月间隔:" + diffMonths(parseDate("2019-10-11 12:21:55"), parseDate("2020-09-11 12:21:55"))); + System.out.println("年间隔:" + diffYears(parseDate("1990-01-11 12:21:55"), parseDate("2020-02-12 12:22:01"))); + System.out.println("当前年份:" + getYearNumber(new Date())); + System.out.println("当前月份:" + getMonthNumber(new Date())); + System.out.println("============="); + + System.out.println("新增秒:" + toDateTimeString(addSeconds(parseDate("2020-02-11 12:21:55"), 20))); + System.out.println("新增分钟:" + toDateTimeString(addMinutes(parseDate("2020-02-11 12:21:55"), 20))); + System.out.println("新增小时:" + toDateTimeString(addHours(parseDate("2020-02-11 12:21:55"), 20))); + System.out.println("新增天:" + toDateTimeString(addDays(parseDate("2020-02-11 12:21:55"), 20))); + System.out.println("新增星期:" + toDateTimeString(addWeeks(parseDate("2020-02-11 12:21:55"), 10))); + System.out.println("新增月份:" + toDateTimeString(addMonths(parseDate("2020-02-11 12:21:55"), 20))); + System.out.println("新增年份:" + toDateTimeString(addYears(parseDate("2020-02-11 12:21:55"), 20))); + + System.out.println("============="); + System.out.println("今天星期:" + getWeekDay(parseDate("2020-11-24"))); + System.out.println("isToday:" + isToday(parseDate("2020-12-01"))); + System.out.println("isThisWeek:" + isThisWeek(parseDate("2020-11-24"))); + System.out.println("isThisMonth:" + isThisMonth(parseDate("2020-10-02"))); + System.out.println("isThisQuarter:" + isThisQuarter(parseDate("2020-10-02"))); + System.out.println("isThisYear:" + isThisYear(parseDate("2020-02-02"))); + System.out.println("第1季度:" + toDateTimeString(getEndOfQuarter(1))); + System.out.println("第2季度:" + toDateTimeString(getEndOfQuarter(2))); + System.out.println("第3季度:" + toDateTimeString(getEndOfQuarter(3))); + System.out.println("第4季度:" + toDateTimeString(getEndOfQuarter(4))); + System.out.println("本季度:" + toDateTimeString(getStartOfThisQuarter())); + System.out.println("本季度:" + toDateTimeString(getEndOfThisQuarter())); + + + System.out.println("datetime-local解析:" + parseDate("2022-12-03T16:00")); + + } +} diff --git a/src/main/java/io/jboot/utils/FileScanner.java b/src/main/java/io/jboot/utils/FileScanner.java index 015036e8a0a370806f76d87916c132cb8029e14b..82ed5293a16c3ca2db9c3ecbb5204a6ce95980d9 100644 --- a/src/main/java/io/jboot/utils/FileScanner.java +++ b/src/main/java/io/jboot/utils/FileScanner.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,12 +54,12 @@ public abstract class FileScanner { public abstract void onChange(String action, String file); protected void working() { - if (!rootDir.contains(";")) { - scan(new File(rootDir)); + if (!rootDir.contains(",")) { + scan(new File(rootDir.trim())); } else { - String[] paths = rootDir.split(";"); + String[] paths = rootDir.split(","); for (String path : paths) { - scan(new File(path)); + scan(new File(path.trim())); } } diff --git a/src/main/java/io/jboot/utils/FileUtil.java b/src/main/java/io/jboot/utils/FileUtil.java index cac19b57fe3ff11d91fd857df6c5a904863e1fe2..7adddaf8f6e53b46e0ce0e01bb005aa52fb6173e 100644 --- a/src/main/java/io/jboot/utils/FileUtil.java +++ b/src/main/java/io/jboot/utils/FileUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,30 @@ package io.jboot.utils; import com.jfinal.core.JFinal; +import com.jfinal.kit.Base64Kit; +import com.jfinal.kit.HashKit; +import com.jfinal.kit.LogKit; import com.jfinal.kit.PathKit; +import com.jfinal.upload.UploadFile; import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.Enumeration; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class FileUtil { + /** + * 获取文件后缀 + * + * @param fileName eg: jboot.jpg + * @return suffix eg: .jpg + */ public static String getSuffix(String fileName) { if (fileName != null && fileName.contains(".")) { return fileName.substring(fileName.lastIndexOf(".")); @@ -34,9 +48,26 @@ public class FileUtil { } - public static String removePrefix(String src, String prefix) { - if (src != null && src.startsWith(prefix)) { - return src.substring(prefix.length()); + public static String removePrefix(String src, String... prefixes) { + if (src != null) { + for (String prefix : prefixes) { + if (src.startsWith(prefix)) { + return src.substring(prefix.length()); + } + } + } + + return src; + } + + + public static String removeSuffix(String src, String... suffixes) { + if (src != null) { + for (String suffix : suffixes) { + if (src.endsWith(suffix)) { + return src.substring(0, suffix.length()); + } + } } return src; } @@ -46,7 +77,13 @@ public class FileUtil { return removePrefix(src, PathKit.getWebRootPath()); } + public static String readString(File file) { + return readString(file, JFinal.me().getConstants().getEncoding()); + } + + + public static String readString(File file, String charsetName) { ByteArrayOutputStream baos = null; FileInputStream fis = null; try { @@ -56,88 +93,186 @@ public class FileUtil { for (int len = 0; (len = fis.read(buffer)) > 0; ) { baos.write(buffer, 0, len); } - return new String(baos.toByteArray(), JFinal.me().getConstants().getEncoding()); + return baos.toString(charsetName); } catch (Exception e) { + LogKit.error(e.toString(), e); } finally { close(fis, baos); } return null; } - public static void writeString(File file, String string) { + + public static void writeString(File file, String content) { + writeString(file, content, JFinal.me().getConstants().getEncoding()); + } + + + public static void writeString(File file, String content, String charsetName) { + writeString(file, content, charsetName, false); + } + + public static void writeString(File file, String content, String charsetName, boolean append) { FileOutputStream fos = null; try { - fos = new FileOutputStream(file, false); - fos.write(string.getBytes(JFinal.me().getConstants().getEncoding())); + ensuresParentExists(file); + fos = new FileOutputStream(file, append); + fos.write(content.getBytes(charsetName)); } catch (Exception e) { + LogKit.error(e.toString(), e); } finally { - close(null, fos); + close(fos); } } - private static void close(InputStream is, OutputStream os) { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - } + public static void ensuresParentExists(File currentFile) throws IOException { + if (!currentFile.getParentFile().exists() + && !currentFile.getParentFile().mkdirs()) { + throw new IOException("Can not mkdirs for file: " + currentFile.getParentFile()); } - if (os != null) { - try { - os.close(); - } catch (IOException e) { + } + + + /** + * 获取文件的 md5 + * + * @param file + * @return + */ + public static String getFileMD5(File file) { + return getFileMD5(file, false); + } + + + /** + * 获取文件 md5 的 base64 编码 + * + * @param file + * @return + */ + public static String getFileMd5Base64(File file) { + return getFileMD5(file, true); + } + + + private static String getFileMD5(File file, boolean withBase64) { + try (FileInputStream fiStream = new FileInputStream(file)) { + MessageDigest digest = MessageDigest.getInstance("MD5"); + byte[] buffer = new byte[8192]; + int length; + while ((length = fiStream.read(buffer)) != -1) { + digest.update(buffer, 0, length); } + return withBase64 ? Base64Kit.encode(digest.digest()) : HashKit.toHex(digest.digest()); + } catch (Exception e) { + LogKit.error(e.toString(), e); } + return null; } + + public static void close(Closeable... closeable) { + QuietlyUtil.closeQuietly(closeable); + } + + public static void unzip(String zipFilePath) throws IOException { String targetPath = zipFilePath.substring(0, zipFilePath.lastIndexOf(".")); - unzip(zipFilePath, targetPath, true); + unzip(zipFilePath, targetPath, true, StandardCharsets.UTF_8); } + public static void unzip(String zipFilePath, String targetPath) throws IOException { - unzip(zipFilePath, targetPath, true); + unzip(zipFilePath, targetPath, true, StandardCharsets.UTF_8); } - public static void unzip(String zipFilePath, String targetPath, boolean safeUnzip) throws IOException { - ZipFile zipFile = new ZipFile(zipFilePath); + + public static void unzip(String zipFilePath, String targetPath, boolean safeUnzip, Charset charset) throws IOException { + targetPath = getCanonicalPath(new File(targetPath)); + ZipFile zipFile = new ZipFile(zipFilePath, charset); try { Enumeration entryEnum = zipFile.entries(); - if (null != entryEnum) { - while (entryEnum.hasMoreElements()) { - OutputStream os = null; - InputStream is = null; - try { - ZipEntry zipEntry = (ZipEntry) entryEnum.nextElement(); - if (!zipEntry.isDirectory()) { - if (safeUnzip && isNotSafeFile(zipEntry.getName())) { - continue; - } - File targetFile = new File(targetPath + File.separator + zipEntry.getName()); - if (!targetFile.getParentFile().exists()) { - targetFile.getParentFile().mkdirs(); - } - os = new BufferedOutputStream(new FileOutputStream(targetFile)); - is = zipFile.getInputStream(zipEntry); - byte[] buffer = new byte[4096]; - int readLen = 0; - while ((readLen = is.read(buffer, 0, 4096)) > 0) { - os.write(buffer, 0, readLen); - } + while (entryEnum.hasMoreElements()) { + OutputStream os = null; + InputStream is = null; + try { + ZipEntry zipEntry = (ZipEntry) entryEnum.nextElement(); + if (!zipEntry.isDirectory()) { + if (safeUnzip && isNotSafeFile(zipEntry.getName())) { + //Unsafe + continue; + } + + File targetFile = new File(targetPath, zipEntry.getName()); + + ensuresParentExists(targetFile); + + if (!targetFile.toPath().normalize().startsWith(targetPath)) { + throw new IOException("Bad zip entry"); + } + + os = new BufferedOutputStream(new FileOutputStream(targetFile)); + is = zipFile.getInputStream(zipEntry); + byte[] buffer = new byte[4096]; + int readLen = 0; + while ((readLen = is.read(buffer, 0, 4096)) > 0) { + os.write(buffer, 0, readLen); } - } finally { - close(is, os); } + } finally { + close(is, os); } } } finally { - zipFile.close(); + close(zipFile); } } + private static boolean isNotSafeFile(String name) { name = name.toLowerCase(); - return name.contains("..") || name.endsWith(".jsp") || name.endsWith(".jspx"); + return name.endsWith(".jsp") || name.endsWith(".jspx"); + } + + + public static boolean isAbsolutePath(String path) { + return StrUtil.isNotBlank(path) && (path.startsWith("/") || path.indexOf(":") > 0); + } + + + public static String getCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void delete(File file) { + if (file == null) { + return; + } + + if (!file.delete()) { + LogKit.error("File {} can not deleted!", getCanonicalPath(file)); + } + } + + public static void delete(UploadFile file) { + if (file == null) { + return; + } + + delete(file.getFile()); + } + + + public static void delete(List files) { + if (files == null) { + return; + } + + files.forEach(FileUtil::delete); } -} \ No newline at end of file +} diff --git a/src/main/java/io/jboot/utils/HttpUtil.java b/src/main/java/io/jboot/utils/HttpUtil.java index 090d7aa4eab229c613d0e6becf07d21d2fc912ee..cca94563d39220d83d0cb1d0b63fd8d60030db41 100644 --- a/src/main/java/io/jboot/utils/HttpUtil.java +++ b/src/main/java/io/jboot/utils/HttpUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -112,6 +112,19 @@ public class HttpUtil { return httpPost(url, paras, null, postData); } + /** + * Http post 操作 + * + * @param url + * @param paras + * @param headers + * @return + */ + public static String httpPost(String url, Map paras, Map headers) { + return httpPost(url, paras, headers, null); + } + + /** * Http post 操作 * @@ -123,7 +136,7 @@ public class HttpUtil { */ public static String httpPost(String url, Map paras, Map headers, String postData) { JbootHttpRequest request = JbootHttpRequest.create(url, paras, JbootHttpRequest.METHOD_POST); - request.setPostContent(postData); + request.setBodyContent(postData); request.addHeaders(headers); JbootHttpResponse response = handle(request); return response.isError() ? null : response.getContent(); @@ -205,7 +218,6 @@ public class HttpUtil { * @return */ public static String upload(String url, Map paras, Map headers, File file) { - Map newParas = new HashMap(); if (paras != null) { newParas.putAll(paras); diff --git a/src/main/java/io/jboot/utils/JsonUtil.java b/src/main/java/io/jboot/utils/JsonUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..fb3daae847c6aecbc6047c76ecd0600b10ad390d --- /dev/null +++ b/src/main/java/io/jboot/utils/JsonUtil.java @@ -0,0 +1,329 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.jfinal.kit.LogKit; +import io.jboot.web.json.JsonBodyParseInterceptor; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * 基于 FastJson,方便解析 Json 内容 + *

+ * 例如: + *

+ * { + * "array": [ + * 1, + * 2, + * 3 + * ], + * "type": true, + * "null": null, + * "number": 123, + * "object": { + * "a": "b", + * "c": "d", + * "e":1 + * }, + * "key": "welcome to CodeFormat.CN" + * } + *

+ * Boolean type = JsonUtil.getBool(json,"type"); + * //type == true + *

+ * int e = JsonUtil.getInt(json,"object.e") + * // e == 1 + *

+ * BigInteger n = JsonUtil.getBigInteger("number") + * // n == 123 + *

+ * String[] array = JsonUtil.get(json,"array",String[].class) + * //array == ["1","2","3"] + *

+ * int[] array = JsonUtil.get(json,"array",int[].class) + * //array == [1,2,3] + *

+ * Map map = JsonUtil.get(json,"object",Map.class) + * //map == {"a":"b","c":"d","e":1} + *

+ * int x = JsonUtil.getInt(json,"array[1]"); + * // x == 2 + *

+ * String key = JsonUtil.getString(json,"key"); + * // key == "welcome to CodeFormat.CN" + */ +public class JsonUtil { + + public static String getString(String json, String key) { + return get(json, key, String.class); + } + + public static String getString(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, String.class); + } + + public static String getString(String json, String key, String defaultValue) { + String value = getString(json, key); + return value != null ? value : defaultValue; + } + + public static String getString(Object jsonObjectOrArray, String key, String defaultValue) { + String value = getString(jsonObjectOrArray, key); + return value != null ? value : defaultValue; + } + + + public static Boolean getBool(String json, String key) { + return get(json, key, Boolean.class); + } + + public static Boolean getBool(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, Boolean.class); + } + + public static boolean getBool(String json, String key, boolean defaultValue) { + Boolean value = getBool(json, key); + return value != null ? value : defaultValue; + } + + public static boolean getBool(Object jsonObjectOrArray, String key, boolean defaultValue) { + Boolean value = getBool(jsonObjectOrArray, key); + return value != null ? value : defaultValue; + } + + + public static Integer getInt(String json, String key) { + return get(json, key, Integer.class); + } + + public static Integer getInt(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, Integer.class); + } + + public static int getInt(String json, String key, int defaultValue) { + Integer value = getInt(json, key); + return value != null ? value : defaultValue; + } + + public static int getInt(Object jsonObjectOrArray, String key, int defaultValue) { + Integer value = getInt(jsonObjectOrArray, key); + return value != null ? value : defaultValue; + } + + public static Float getFloat(String json, String key) { + return get(json, key, Float.class); + } + + public static Float getFloat(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, Float.class); + } + + public static float getFloat(String json, String key, float defaultValue) { + Float value = getFloat(json, key); + return value != null ? value : defaultValue; + } + + public static float getFloat(Object jsonObjectOrArray, String key, float defaultValue) { + Float value = getFloat(jsonObjectOrArray, key); + return value != null ? value : defaultValue; + } + + public static Double getDouble(String json, String key) { + return get(json, key, Double.class); + } + + public static Double getDouble(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, Double.class); + } + + public static double getDouble(String json, String key, double defaultValue) { + Double value = getDouble(json, key); + return value != null ? value : defaultValue; + } + + public static double getDouble(Object jsonObjectOrArray, String key, double defaultValue) { + Double value = getDouble(jsonObjectOrArray, key); + return value != null ? value : defaultValue; + } + + public static Long getLong(String json, String key) { + return get(json, key, Long.class); + } + + public static Long getLong(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, Long.class); + } + + public static long getLong(String json, String key, long defaultValue) { + Long value = getLong(json, key); + return value != null ? value : defaultValue; + } + + public static long getLong(Object jsonObjectOrArray, String key, long defaultValue) { + Long value = getLong(jsonObjectOrArray, key); + return value != null ? value : defaultValue; + } + + public static BigInteger getBigInteger(String json, String key) { + return get(json, key, BigInteger.class); + } + + public static BigInteger getBigInteger(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, BigInteger.class); + } + + public static BigInteger getBigInteger(String json, String key, BigInteger defaultValue) { + BigInteger value = getBigInteger(json, key); + return value != null ? value : defaultValue; + } + + + public static BigInteger getBigInteger(Object jsonObjectOrArray, String key, BigInteger defaultValue) { + BigInteger value = getBigInteger(jsonObjectOrArray, key); + return value != null ? value : defaultValue; + } + + public static BigDecimal getBigDecimal(String json, String key) { + return get(json, key, BigDecimal.class); + } + + public static BigDecimal getBigDecimal(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, BigDecimal.class); + } + + public static BigDecimal getBigDecimal(String json, String key, BigDecimal defaultValue) { + BigDecimal value = getBigDecimal(json, key); + return value != null ? value : defaultValue; + } + + + public static BigDecimal getBigDecimal(Object jsonObjectOrArray, String key, BigDecimal defaultValue) { + BigDecimal value = getBigDecimal(jsonObjectOrArray, key); + return value != null ? value : defaultValue; + } + + + public static Date getDate(String json, String key) { + return get(json, key, Date.class); + } + + public static Date getDate(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, Date.class); + } + + public static Date getDate(String json, String key, Date defaultValue) { + Date date = get(json, key, Date.class); + return date != null ? date : defaultValue; + } + + public static Date getDate(Object jsonObjectOrArray, String key, Date defaultValue) { + Date date = get(jsonObjectOrArray, key, Date.class); + return date != null ? date : defaultValue; + } + + + public static JSONObject getJSONObject(String json, String key) { + return get(json, key, JSONObject.class); + } + + public static JSONObject getJSONObject(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, JSONObject.class); + } + + public static JSONArray getJSONArray(String json, String key) { + return get(json, key, JSONArray.class); + } + + public static JSONArray getJSONArray(Object jsonObjectOrArray, String key) { + return get(jsonObjectOrArray, key, JSONArray.class); + } + + + public static List getList(String json, String key, Class clazz) { + return getList(getJsonObjectOrArray(json), key, clazz); + } + + + public static List getList(Object jsonObjectOrArray, String key, Class clazz) { + return get(jsonObjectOrArray, key, TypeDef.wrapper(List.class, clazz)); + } + + + public static Set getSet(String json, String key, Class clazz) { + return getSet(getJsonObjectOrArray(json), key, clazz); + } + + + public static Set getSet(Object jsonObjectOrArray, String key, Class clazz) { + return get(jsonObjectOrArray, key, TypeDef.wrapper(Set.class, clazz)); + } + + + public static T get(String json, String key, Class clazz) { + return get(getJsonObjectOrArray(json), key, clazz); + } + + public static T get(Object jsonObjectOrArray, String key, Class clazz) { + if (jsonObjectOrArray == null) { + return null; + } + try { + return (T) JsonBodyParseInterceptor.parseJsonBody(jsonObjectOrArray, clazz, clazz, key); + } catch (Exception e) { + LogKit.error(e.toString(), e); + } + return null; + } + + + public static T get(String json, String key, TypeDef typeDef) { + return get(getJsonObjectOrArray(json), key, typeDef); + } + + + public static T get(Object jsonObjectOrArray, String key, TypeDef typeDef) { + if (jsonObjectOrArray == null) { + return null; + } + try { + return (T) JsonBodyParseInterceptor.parseJsonBody(jsonObjectOrArray, typeDef.getDefClass(), typeDef.getType(), key); + } catch (Exception e) { + LogKit.error(e.toString(), e); + } + return null; + } + + public static Object getJsonObjectOrArray(String json) { + if (StrUtil.isNotBlank(json)) { + try { + return JSON.parse(json); + } catch (Exception e) { + LogKit.error(e.toString(), e); + } + } + return null; + } + + +} diff --git a/src/main/java/io/jboot/utils/ModelCopier.java b/src/main/java/io/jboot/utils/ModelUtil.java similarity index 75% rename from src/main/java/io/jboot/utils/ModelCopier.java rename to src/main/java/io/jboot/utils/ModelUtil.java index f6778d0c1cd0922fd96a084458d94e7acfee1c71..feeccd91cf10ba141f3c26af91bbc049686b75c7 100644 --- a/src/main/java/io/jboot/utils/ModelCopier.java +++ b/src/main/java/io/jboot/utils/ModelUtil.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2019, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -15,6 +15,7 @@ */ package io.jboot.utils; +import com.jfinal.plugin.activerecord.Model; import com.jfinal.plugin.activerecord.Page; import io.jboot.db.model.JbootModel; import io.jboot.exception.JbootException; @@ -29,7 +30,7 @@ import java.util.Set; * @author michael yang (fuhai999@gmail.com) * @Date: 2019/11/21 */ -public class ModelCopier { +public class ModelUtil { /** * copy model list @@ -38,7 +39,7 @@ public class ModelCopier { * @param * @return */ - public static List copy(List modelList) { + public static > List copy(List modelList) { if (modelList == null || modelList.isEmpty()) { return modelList; } @@ -62,7 +63,7 @@ public class ModelCopier { * @param * @return */ - public static Set copy(Set modelSet) { + public static > Set copy(Set modelSet) { if (modelSet == null || modelSet.isEmpty()) { return modelSet; } @@ -81,11 +82,12 @@ public class ModelCopier { /** * copy model array + * * @param models * @param * @return */ - public static M[] copy(M[] models) { + public static > M[] copy(M[] models) { if (models == null || models.length == 0) { return models; } @@ -106,7 +108,7 @@ public class ModelCopier { * @param * @return */ - public static Page copy(Page modelPage) { + public static > Page copy(Page modelPage) { if (modelPage == null) { return null; } @@ -128,7 +130,7 @@ public class ModelCopier { * @param * @return */ - public static M copy(M model) { + public static > M copy(M model) { return model == null ? null : (M) model.copy(); } @@ -137,8 +139,18 @@ public class ModelCopier { try { return clazz.newInstance(); } catch (Exception e) { - throw new JbootException("can not newInstance class:" + clazz + "\n" + e.toString(), e); + throw new JbootException("Can not newInstance with class: " + clazz.getName() + "\n" + e, e); } } + + public static void keep(List models, String... attrs) { + if (models == null || models.isEmpty()) { + return; + } + + models.forEach(model -> model.keep(attrs)); + } + + } diff --git a/src/main/java/io/jboot/utils/NamedThreadFactory.java b/src/main/java/io/jboot/utils/NamedThreadFactory.java index b2455ac5e7af4dc2c5cc68ea2bf4c43c194dbd9c..be819cdf727fc7e555c07fe00109402501ae098a 100644 --- a/src/main/java/io/jboot/utils/NamedThreadFactory.java +++ b/src/main/java/io/jboot/utils/NamedThreadFactory.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2019, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @Date: 2019/11/21 */ public class NamedThreadFactory implements ThreadFactory { + protected static final AtomicInteger POOL_COUNTER = new AtomicInteger(1); protected final AtomicInteger mThreadCounter; protected final String mPrefix; diff --git a/src/main/java/io/jboot/utils/NamedThreadPools.java b/src/main/java/io/jboot/utils/NamedThreadPools.java index c0dc3d74da09a0f86046d128c9cd6f07e83faf52..212edcaaa3c807fa806def8407ce8188f5cd2e6a 100644 --- a/src/main/java/io/jboot/utils/NamedThreadPools.java +++ b/src/main/java/io/jboot/utils/NamedThreadPools.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2019, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -23,16 +23,14 @@ import java.util.concurrent.*; */ public class NamedThreadPools { - public static ExecutorService newFixedThreadPool(int nThreads, String name) { - return newFixedThreadPool(nThreads, new NamedThreadFactory(name)); + public static ExecutorService newFixedThreadPool(String prefix) { + int nThreads = Runtime.getRuntime().availableProcessors(); + return newFixedThreadPool(nThreads, prefix); } - public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { - return new ThreadPoolExecutor(nThreads, nThreads, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - threadFactory); + public static ExecutorService newFixedThreadPool(int nThreads, String name) { + return Executors.newFixedThreadPool(nThreads, new NamedThreadFactory(name)); } diff --git a/src/main/java/io/jboot/utils/NetUtil.java b/src/main/java/io/jboot/utils/NetUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..672239550908c47bcb5302f69d27b4ade1c37d33 --- /dev/null +++ b/src/main/java/io/jboot/utils/NetUtil.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; + +public class NetUtil { + + public static String getLocalIpAddress() { + String hostIpAddress = null; + String siteLocalIpAddress = null;// 外网IP + try { + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + InetAddress address = null; + boolean findSiteLocalIpAddress = false;// 是否找到外网IP + while (networkInterfaces.hasMoreElements() && !findSiteLocalIpAddress) { + NetworkInterface ni = networkInterfaces.nextElement(); + Enumeration addresses = ni.getInetAddresses(); + while (addresses.hasMoreElements()) { + address = addresses.nextElement(); + + if (!address.isSiteLocalAddress() && !address.isLoopbackAddress() + && !address.getHostAddress().contains(":")) { // 外网IP + siteLocalIpAddress = address.getHostAddress(); + findSiteLocalIpAddress = true; + break; + } else if (address.isSiteLocalAddress() + && !address.isLoopbackAddress() + && !address.getHostAddress().contains(":")) { // 内网IP + hostIpAddress = address.getHostAddress(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + // 优先使用配置的外网IP地址 + return StrUtil.isNotBlank(siteLocalIpAddress) ? siteLocalIpAddress : hostIpAddress; + } + +} diff --git a/src/main/java/io/jboot/utils/ObjectFunc.java b/src/main/java/io/jboot/utils/ObjectFunc.java new file mode 100644 index 0000000000000000000000000000000000000000..9a80a758454acd081509b76ecef9673fa5187982 --- /dev/null +++ b/src/main/java/io/jboot/utils/ObjectFunc.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public interface ObjectFunc{ + public Object get(T Object); +} diff --git a/src/main/java/io/jboot/utils/ObjectUtil.java b/src/main/java/io/jboot/utils/ObjectUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..326d1770e3f301877432ea4642dd264ba163ad8f --- /dev/null +++ b/src/main/java/io/jboot/utils/ObjectUtil.java @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collection; +import java.util.Date; +import java.util.Objects; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class ObjectUtil { + + + /** + * 判断 某个 objects 集合里是否包含了某个 object + * + * @param objects object 集合 + * @param compareObject 是否被包 集合 含的对比 object + * @param compareAttrGetters 需要对比的 getter + * @param + * @return true 包含,false 不包含 + */ + public static boolean isContainsObject(Collection objects, T compareObject, ObjectFunc... compareAttrGetters) { + if (objects == null || objects.isEmpty() || compareObject == null) { + return false; + } + + if (compareAttrGetters == null || compareAttrGetters.length == 0) { + throw new IllegalArgumentException("compareAttrGetters must not be null"); + } + + + for (T object : objects) { + if (isSameObject(object, compareObject, compareAttrGetters)) { + return true; + } + } + + return false; + } + + + /** + * 获取 某个 objects 集合里包含的 object + * + * @param objects object 集合 + * @param compareObject 是否被包 集合 含的对比 object + * @param compareAttrGetters 需要对比的 getter + * @param + * @return 返回 objects 结合中对比成功的 object + */ + public static T getContainsObject(Collection objects, T compareObject, ObjectFunc... compareAttrGetters) { + if (objects == null || objects.isEmpty() || compareObject == null) { + return null; + } + + if (compareAttrGetters == null || compareAttrGetters.length == 0) { + throw new IllegalArgumentException("compareAttrGetters must not be null"); + } + + + for (T object : objects) { + if (isSameObject(object, compareObject, compareAttrGetters)) { + return object; + } + } + + return null; + } + + + /** + * 判断两个 Object 是否是同一个 Object,根据传入的 getter 来进行对比 + * + * @param object1 + * @param object2 + * @param compareAttrGetters + * @param + * @return + */ + public static boolean isSameObject(T object1, T object2, ObjectFunc... compareAttrGetters) { + if (object1 == null || object2 == null) { + return object1 == object2; + } + + if (compareAttrGetters == null || compareAttrGetters.length == 0) { + throw new IllegalArgumentException("compareAttrGetters must not be null"); + } + + + for (ObjectFunc getter : compareAttrGetters) { + + if (getter == null) { + throw new IllegalArgumentException("compareAttrGetter must not be null"); + } + + + if (!Objects.equals(getter.get(object1), getter.get(object2))) { + return false; + } + } + + return true; + } + + + /** + * 判断两个 Object 是否是同一个 Object,根据传入的 getter 来进行对比 + * + * @param object1 + * @param object2 + * @param compareAttrGetters + * @param + * @return + */ + public static boolean notSameObject(T object1, T object2, ObjectFunc... compareAttrGetters) { + return !isSameObject(object1, object2, compareAttrGetters); + } + + + public static Object convert(Object value, Class targetClass) { + if (value == null || (value.getClass() == String.class && StrUtil.isBlank((String) value) + && targetClass != String.class)) { + return null; + } + + if (value.getClass().isAssignableFrom(targetClass)) { + return value; + } + if (targetClass == String.class) { + return value.toString(); + } else if (targetClass == Integer.class || targetClass == int.class) { + if (value instanceof Number) { + return ((Number) value).intValue(); + } + return Integer.parseInt(value.toString()); + } else if (targetClass == Long.class || targetClass == long.class) { + if (value instanceof Number) { + return ((Number) value).longValue(); + } + return Long.parseLong(value.toString()); + } else if (targetClass == Double.class || targetClass == double.class) { + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + return Double.parseDouble(value.toString()); + } else if (targetClass == Float.class || targetClass == float.class) { + if (value instanceof Number) { + return ((Number) value).floatValue(); + } + return Float.parseFloat(value.toString()); + } else if (targetClass == Boolean.class || targetClass == boolean.class) { + String v = value.toString().toLowerCase(); + if ("1".equals(v) || "true".equalsIgnoreCase(v)) { + return Boolean.TRUE; + } else if ("0".equals(v) || "false".equalsIgnoreCase(v)) { + return Boolean.FALSE; + } else { + throw new RuntimeException("Can not parse to boolean type of value: \"" + value + "\""); + } + } else if (targetClass == java.math.BigDecimal.class) { + return new java.math.BigDecimal(value.toString()); + } else if (targetClass == java.math.BigInteger.class) { + return new java.math.BigInteger(value.toString()); + } else if (targetClass == byte[].class) { + return value.toString().getBytes(); + } else if (targetClass == Date.class) { + return DateUtil.parseDate(value); + } else if (targetClass == LocalDateTime.class) { + return DateUtil.toLocalDateTime(DateUtil.parseDate(value)); + } else if (targetClass == LocalDate.class) { + return DateUtil.toLocalDate(DateUtil.parseDate(value)); + } else if (targetClass == LocalTime.class) { + return DateUtil.toLocalTime(DateUtil.parseDate(value)); + } else if (targetClass == Short.class || targetClass == short.class) { + if (value instanceof Number) { + return ((Number) value).shortValue(); + } + return Short.parseShort(value.toString()); + } + + throw new RuntimeException("\"" + targetClass.getName() + "\" can not be parsed."); + } + + + public static Object getPrimitiveDefaultValue(Class paraClass) { + if (paraClass == int.class || paraClass == long.class || paraClass == float.class || paraClass == double.class) { + return 0; + } else if (paraClass == boolean.class) { + return Boolean.FALSE; + } else if (paraClass == short.class) { + return (short) 0; + } else if (paraClass == byte.class) { + return (byte) 0; + } else if (paraClass == char.class) { + return '\u0000'; + } else { + //不存在这种类型 + return null; + } + } + + + public static T obtainNotNull(T... ts) { + if (ts == null || ts.length == 0) { + throw new IllegalArgumentException("Arguments is null or empty."); + } + + for (T t : ts) { + if (t != null) { + return t; + } + } + + return null; + } + +} diff --git a/src/main/java/io/jboot/utils/QuietlyUtil.java b/src/main/java/io/jboot/utils/QuietlyUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..268d6963b638e24e44d1006b301be0d691d5449e --- /dev/null +++ b/src/main/java/io/jboot/utils/QuietlyUtil.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import com.jfinal.kit.LogKit; + +import java.io.Closeable; +import java.io.IOException; + +public class QuietlyUtil { + + public static void closeQuietly(Closeable... closeables) { + if (closeables != null) { + for (Closeable closeable : closeables) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + LogKit.error(e.toString(), e); + } + } + } + } + } + + public static void sleepQuietly(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + LogKit.error(e.toString(), e); + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/io/jboot/utils/RSAUtil.java b/src/main/java/io/jboot/utils/RSAUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..cee6ae466a61eeeb6a26af05ab01ec49ae45ecea --- /dev/null +++ b/src/main/java/io/jboot/utils/RSAUtil.java @@ -0,0 +1,339 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import com.jfinal.kit.Base64Kit; + +import javax.crypto.Cipher; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * RSA 非对称加密工具类,对称加密请参考 DESUtil + */ +public class RSAUtil { + + + /** + * 生成key长度为 2048 的秘钥对 + * + * @return + * @throws Exception + */ + public static KeyPair getKeyPair2048() throws Exception { + //《2015 年加密宣言》密码指南建议,RSA 算法使用的密钥长度至少应为 2048 位。 + return getKeyPair(2048); + } + + /** + * 生成密钥对 + * + * @return + * @throws Exception + */ + public static KeyPair getKeyPair(int keysize) throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(keysize); + return keyPairGenerator.generateKeyPair(); + } + + + /** + * 生成key长度为 2048 的秘钥对 + * + * @return [publicKey, privateKey] + * @throws Exception + */ + public static String[] getKeyPairAsBase64() throws Exception { + return getKeyPairAsBase64(2048); + } + + /** + * 生成密钥对 + * + * @return [publicKey, privateKey] + * @throws Exception + */ + public static String[] getKeyPairAsBase64(int keysize) throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(keysize); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + return new String[]{Base64Kit.encode(keyPair.getPublic().getEncoded()) + , Base64Kit.encode(keyPair.getPrivate().getEncoded())}; + } + + + /** + * 公钥字符串转PublicKey实例 + * + * @param publicKey + * @return + * @throws Exception + */ + public static PublicKey getPublicKey(String publicKey) throws Exception { + byte[] publicKeyBytes = Base64.getDecoder().decode(publicKey.getBytes()); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(keySpec); + } + + /** + * 私钥字符串转PrivateKey实例 + * + * @param privateKey + * @return + * @throws Exception + */ + public static PrivateKey getPrivateKey(String privateKey) throws Exception { + byte[] privateKeyBytes = Base64.getDecoder().decode(privateKey.getBytes()); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(keySpec); + } + + /** + * 公钥加密 + * + * @param content + * @param publicKey + * @return + * @throws Exception + */ + public static byte[] encryptByPublicKey(byte[] content, PublicKey publicKey) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return cipher.doFinal(content); + } + + + /** + * 公钥加密 + * + * @param content + * @param publicKeyBase64 + * @return + * @throws Exception + */ + public static byte[] encryptByPublicKey(byte[] content, String publicKeyBase64) throws Exception { + return encryptByPublicKey(content, getPublicKey(publicKeyBase64)); + } + + + /** + * 公钥加密 + * + * @param content + * @param publicKey + * @return + * @throws Exception + */ + public static String encryptToBase64ByPublicKey(byte[] content, PublicKey publicKey) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return Base64Kit.encode(cipher.doFinal(content)); + } + + + /** + * 公钥加密 + * + * @param content + * @param publicKeyBase64 + * @return + * @throws Exception + */ + public static String encryptToBase64ByPublicKey(byte[] content, String publicKeyBase64) throws Exception { + return encryptToBase64ByPublicKey(content, getPublicKey(publicKeyBase64)); + } + + + /** + * 公钥加密 + * + * @param content + * @param publicKeyBase64 + * @return + * @throws Exception + */ + public static String encryptToBase64ByPublicKey(String content, String publicKeyBase64) throws Exception { + return encryptToBase64ByPublicKey(content.getBytes(StandardCharsets.UTF_8), getPublicKey(publicKeyBase64)); + } + + + /** + * 私钥加密 + * + * @param content + * @param privateKey + * @return + * @throws Exception + */ + public static byte[] encryptByPrivateKey(byte[] content, PrivateKey privateKey) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, privateKey); + return cipher.doFinal(content); + } + + + /** + * 私钥加密 + * + * @param content + * @param privateKeyBase64 + * @return + * @throws Exception + */ + public static byte[] encryptByPrivateKey(byte[] content, String privateKeyBase64) throws Exception { + return encryptByPrivateKey(content, getPrivateKey(privateKeyBase64)); + } + + + /** + * 私钥加密 + * + * @param content + * @param privateKey + * @return + * @throws Exception + */ + public static String encryptToBase64ByPrivateKey(byte[] content, PrivateKey privateKey) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, privateKey); + return Base64Kit.encode(cipher.doFinal(content)); + } + + + /** + * 私钥加密 + * + * @param content + * @param privateKeyBase64 + * @return + * @throws Exception + */ + public static String encryptToBase64ByPrivateKey(byte[] content, String privateKeyBase64) throws Exception { + return encryptToBase64ByPrivateKey(content, getPrivateKey(privateKeyBase64)); + } + + + /** + * 私钥加密 + * + * @param content + * @param privateKeyBase64 + * @return + * @throws Exception + */ + public static String encryptToBase64ByPrivateKey(String content, String privateKeyBase64) throws Exception { + return encryptToBase64ByPrivateKey(content.getBytes(StandardCharsets.UTF_8), getPrivateKey(privateKeyBase64)); + } + + + /** + * 私钥解密 + * + * @param content + * @param privateKey + * @return + * @throws Exception + */ + public static byte[] decryptByPrivateKey(byte[] content, PrivateKey privateKey) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return cipher.doFinal(content); + } + + + /** + * 私钥解密 + * + * @param content + * @param privateKeyBase64 + * @return + * @throws Exception + */ + public static byte[] decryptByPrivateKey(byte[] content, String privateKeyBase64) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, getPrivateKey(privateKeyBase64)); + return cipher.doFinal(content); + } + + + /** + * 私钥解密 + * + * @param base64Content + * @param privateKeyBase64 + * @return + * @throws Exception + */ + public static String decryptToStringByPrivateKey(String base64Content, String privateKeyBase64) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, getPrivateKey(privateKeyBase64)); + return new String(cipher.doFinal(Base64Kit.decode(base64Content)), StandardCharsets.UTF_8); + } + + + /** + * 公钥解密 + * + * @param content + * @param publicKey + * @return + * @throws Exception + */ + public static byte[] decrypByPublicKey(byte[] content, PublicKey publicKey) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, publicKey); + return cipher.doFinal(content); + } + + + /** + * 公钥解密 + * + * @param content + * @param publicKeyBase64 + * @return + * @throws Exception + */ + public static byte[] decrypByPublicKey(byte[] content, String publicKeyBase64) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, getPublicKey(publicKeyBase64)); + return cipher.doFinal(content); + } + + + /** + * 公钥解密 + * + * @param base64Content + * @param publicKeyBase64 + * @return + * @throws Exception + */ + public static String decryptToStringByPublicKey(String base64Content, String publicKeyBase64) throws Exception { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, getPublicKey(publicKeyBase64)); + return new String(cipher.doFinal(Base64Kit.decode(base64Content)), StandardCharsets.UTF_8); + } + + +} diff --git a/src/main/java/io/jboot/utils/ReflectUtil.java b/src/main/java/io/jboot/utils/ReflectUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e8b68df263cf7754e2da167debd2f5556b33c213 --- /dev/null +++ b/src/main/java/io/jboot/utils/ReflectUtil.java @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import com.jfinal.kit.LogKit; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Predicate; + +/** + * 反射相关操作的工具类 + */ +public class ReflectUtil { + + public static T getStaticFieldValue(Class dClass, String fieldName) { + return getFieldValue(dClass, fieldName, null); + } + + public static T getFieldValue(Object getFrom, String fieldName) { + return getFieldValue(getFrom.getClass(), fieldName, getFrom); + } + + private static T getFieldValue(Class dClass, String fieldName, Object getFrom) { + try { + if (StrUtil.isBlank(fieldName)) { + throw new IllegalArgumentException("fieldName must not be null or empty."); + } + Field field = searchField(dClass, f -> f.getName().equals(fieldName)); + if (field == null) { + throw new NoSuchFieldException(fieldName); + } + + return getFileValue(getFrom, field); + + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + private static T getFileValue(Object getFrom, Field field) { + boolean accessible = field.isAccessible(); + try { + field.setAccessible(true); + return (T) field.get(getFrom); + } catch (IllegalAccessException e) { + LogKit.error(e.toString(), e); + } finally { + field.setAccessible(accessible); + } + return null; + } + + + public static void setStaticFieldValue(Class dClass, String fieldName, Object value) { + setFieldValue(dClass, null, fieldName, value); + } + + + public static void setFieldValue(Object setTo, String fieldName, Object value) { + setFieldValue(setTo.getClass(), setTo, fieldName, value); + } + + + private static void setFieldValue(Class dClass, Object setTo, String fieldName, Object value) { + setFieldValue(dClass, setTo, f -> f.getName().equals(fieldName), value); + } + + + private static void setFieldValue(Class dClass, Object setTo, Predicate filter, Object value) { + Field field = searchField(dClass, filter); + if (field == null) { + throw new IllegalArgumentException("No such field"); + } + + setFieldValue(setTo, value, field); + } + + public static void setFieldValue(Object setTo, Object value, Field field) { + final boolean accessible = field.isAccessible(); + try { + field.setAccessible(true); + field.set(setTo, value); + } catch (IllegalAccessException e) { + LogKit.error(e.toString(), e); + } finally { + field.setAccessible(accessible); + } + } + + + public static Field searchField(Class dClass, Predicate filter) { + if (dClass == null) { + return null; + } + Field[] fields = dClass.getDeclaredFields(); + for (Field field : fields) { + if (filter.test(field)) { + return field; + } + } + return searchField(dClass.getSuperclass(), filter); + } + + + public static List searchFieldList(Class dClass, Predicate filter) { + List fields = new LinkedList<>(); + doSearchFieldList(dClass, filter, fields); + return fields; + } + + + private static void doSearchFieldList(Class dClass, Predicate filter, List searchToList) { + if (dClass == null || dClass == Object.class) { + return; + } + + Field[] fields = dClass.getDeclaredFields(); + if (fields.length > 0) { + if (filter != null) { + for (Field field : fields) { + if (filter.test(field)) { + searchToList.add(field); + } + } + } else { + searchToList.addAll(Arrays.asList(fields)); + } + } + + doSearchFieldList(dClass.getSuperclass(), filter, searchToList); + } + + + public static Method searchMethod(Class dClass, Predicate filter) { + if (dClass == null) { + return null; + } + Method[] methods = dClass.getDeclaredMethods(); + for (Method method : methods) { + if (filter.test(method)) { + return method; + } + } + return searchMethod(dClass.getSuperclass(), filter); + } + + + public static List searchMethodList(Class dClass, Predicate filter) { + List methods = new LinkedList<>(); + doSearchMethodList(dClass, filter, methods); + return methods; + } + + + private static void doSearchMethodList(Class dClass, Predicate filter, List searchToList) { + if (dClass == null) { + return; + } + Method[] methods = dClass.getDeclaredMethods(); + if (methods.length > 0) { + if (filter != null) { + for (Method method : methods) { + if (filter.test(method)) { + searchToList.add(method); + } + } + } else { + searchToList.addAll(Arrays.asList(methods)); + } + } + + doSearchMethodList(dClass.getSuperclass(), filter, searchToList); + } + + + public static T invokeStaticMethod(Class dClass, String methodName, Object... args) { + return invokeStaticMethod(dClass, m -> m.getName().equals(methodName), args); + } + + + public static T invokeStaticMethod(Class dClass, Predicate filter, Object... args) { + Method method = searchMethod(dClass, filter); + if (method == null) { + throw new IllegalArgumentException("No such method."); + } + return invokeMethod(null, method, args); + } + + + public static T invokeMethod(Object obj, String methodName, Object... args) { + return invokeMethod(obj, m -> m.getName().equals(methodName), args); + } + + + public static T invokeMethod(Object obj, Predicate filter, Object... args) { + Method method = searchMethod(obj.getClass(), filter); + if (method == null) { + throw new IllegalArgumentException("No such method."); + } + return invokeMethod(obj, method, args); + } + + + public static T invokeMethod(Object obj, Method method, Object... args) { + final boolean accessible = method.isAccessible(); + try { + method.setAccessible(true); + return (T) method.invoke(obj, args); + } catch (IllegalAccessException | InvocationTargetException e) { + LogKit.error(e.toString(), e); + } finally { + method.setAccessible(accessible); + } + return null; + } + +} diff --git a/src/main/java/io/jboot/utils/RequestUtil.java b/src/main/java/io/jboot/utils/RequestUtil.java index 61fba70d6ee60702ee943509e1789aeb1140502f..2efee3d431031b491319a6d68364896552c5b44d 100644 --- a/src/main/java/io/jboot/utils/RequestUtil.java +++ b/src/main/java/io/jboot/utils/RequestUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,9 +42,15 @@ public class RequestUtil { return "XMLHttpRequest".equalsIgnoreCase(header); } + public static boolean isJsonContentType(HttpServletRequest request) { + String contentType = request.getContentType(); + return contentType != null && contentType.toLowerCase().contains("application/json"); + } + + public static boolean isMultipartRequest(HttpServletRequest request) { String contentType = request.getContentType(); - return contentType != null && contentType.toLowerCase().indexOf("multipart") != -1; + return contentType != null && contentType.toLowerCase().contains("multipart"); } /** @@ -111,9 +117,9 @@ public class RequestUtil { } public static String getIpAddress(HttpServletRequest request) { - String ip = request.getHeader("X-requested-For"); + String ip = request.getHeader("X-Forwarded-For"); if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("X-Forwarded-For"); + ip = request.getHeader("X-Real-IP"); } if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); @@ -133,8 +139,7 @@ public class RequestUtil { if (ip != null && ip.contains(",")) { String[] ips = ip.split(","); - for (int index = 0; index < ips.length; index++) { - String strIp = ips[index]; + for (String strIp : ips) { if (!("unknown".equalsIgnoreCase(strIp))) { ip = strIp; break; @@ -142,6 +147,10 @@ public class RequestUtil { } } + if ("0:0:0:0:0:0:0:1".equals(ip)) { + ip = "127.0.0.1"; + } + return ip; } @@ -157,12 +166,10 @@ public class RequestUtil { public static String getBaseUrl(HttpServletRequest request) { int port = request.getServerPort(); - StringBuilder defaultDomain = new StringBuilder(request.getScheme()); - defaultDomain.append("://") - .append(request.getServerName()) - .append(port == 80 ? "" : ":" + port) - .append(request.getContextPath()); - return defaultDomain.toString(); + return request.getScheme() + "://" + + request.getServerName() + + (port == 80 || port == 443 ? "" : ":" + port) + + request.getContextPath(); } @@ -180,7 +187,7 @@ public class RequestUtil { public static String getCurrentUrl(HttpServletRequest request) { String queryString = request.getQueryString(); - String url = getBaseUrl(request) + request.getRequestURI(); + String url = getBaseUrl(request) + request.getServletPath(); if (StrUtil.isNotBlank(queryString)) { url = url.concat("?").concat(queryString); } diff --git a/src/main/java/io/jboot/utils/StrUtil.java b/src/main/java/io/jboot/utils/StrUtil.java index 31915fcb31f1bd07575c053c5d486f7606739b85..cd7bf96a23ab5662256b772e81f669cf95874914 100644 --- a/src/main/java/io/jboot/utils/StrUtil.java +++ b/src/main/java/io/jboot/utils/StrUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,14 +18,15 @@ package io.jboot.utils; import com.jfinal.core.JFinal; import com.jfinal.kit.StrKit; import com.jfinal.log.Log; +import com.jfinal.plugin.activerecord.Model; +import io.jboot.web.validate.Regex; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -63,12 +64,58 @@ public class StrUtil extends StrKit { return redirect; } - public static boolean areNotEmpty(String... strings) { - if (strings == null || strings.length == 0) { + private static final Map EMPTY_MAP = new HashMap<>(); + + public static Map queryStringToMap(String queryString) { + if (StrUtil.isBlank(queryString)) { + return EMPTY_MAP; + } + + Map map = new HashMap<>(); + String[] params = queryString.split("&"); + for (String paramPair : params) { + String[] keyAndValue = paramPair.split("="); + if (keyAndValue.length == 2) { + map.put(keyAndValue[0], keyAndValue[1]); + } else if (keyAndValue.length == 1) { + map.put(keyAndValue[0], ""); + } + } + return map; + } + + + public static String mapToQueryString(Map map) { + if (map == null || map.isEmpty()) { + return EMPTY; + } + + StringBuilder sb = new StringBuilder(); + + for (Object key : map.keySet()) { + if (key == null || key.equals(StrUtil.EMPTY)) { + continue; + } + if (sb.length() > 0) { + sb.append("&"); + } + + sb.append(key.toString().trim()); + sb.append("="); + Object value = map.get(key); + sb.append(value == null ? EMPTY : StrUtil.urlEncode(value.toString().trim())); + } + + + return sb.toString(); + } + + public static boolean areNotEmpty(String... strs) { + if (strs == null || strs.length == 0) { return false; } - for (String string : strings) { + for (String string : strs) { if (string == null || EMPTY.equals(string)) { return false; } @@ -76,43 +123,104 @@ public class StrUtil extends StrKit { return true; } - public static String requireNonBlank(String string) { - if (isBlank(string)) { + public static boolean isAnyBlank(String... strs) { + if (strs == null || strs.length == 0) { + return false; + } + + for (String str : strs) { + if (isBlank(str)) { + return true; + } + } + return false; + } + + public static boolean isStartsWithAny(String str, String... prefixes) { + if (isBlank(str) || prefixes == null || prefixes.length == 0) { + return false; + } + + for (String prefix : prefixes) { + if (str.startsWith(prefix)) { + return true; + } + } + return false; + } + + + public static String requireNonBlank(String str) { + if (isBlank(str)) { throw new NullPointerException(); } - return string; + return str; } - public static String requireNonBlank(String string, String message) { - if (isBlank(string)) { + public static String requireNonBlank(String str, String message) { + if (isBlank(str)) { throw new NullPointerException(message); } - return string; + return str; } - public static String obtainDefaultIfBlank(String string, String defaultValue) { - return isBlank(string) ? defaultValue : string; + @Deprecated + public static String obtainDefaultIfBlank(String value, String defaultValue) { + return obtainDefault(value, defaultValue); + } + + + public static String obtainDefault(String value, String defaultValue) { + return isBlank(value) ? defaultValue : value; + } + + + public static String obtainNotBlank(String... values) { + if (values == null || values.length == 0) { + throw new IllegalArgumentException("Arguments is null or empty."); + } + + for (String value : values) { + if (isNotBlank(value)) { + return value; + } + } + + return null; } /** * 不是空数据,注意:空格不是空数据 * - * @param string + * @param str * @return */ - public static boolean isNotEmpty(String string) { - return string != null && !string.equals(""); + public static boolean isNotEmpty(String str) { + return str != null && !str.equals(EMPTY); } /** * 确保不是空白字符串 * - * @param o + * @param str * @return */ - public static boolean isNotBlank(Object o) { - return o == null ? false : notBlank(o.toString()); + public static boolean isNotBlank(Object str) { + return str != null && notBlank(str.toString()); + } + + + /** + * null 或者 空内容字符串 + * 使用 isBlank 代替 + * + * @param str + * @return + */ + @Deprecated + public static boolean isNullOrBlank(String str) { + return isBlank(str); } @@ -137,7 +245,7 @@ public class StrUtil extends StrKit { * @return */ public static boolean isNumeric(String str) { - if (str == null) { + if (isBlank(str)) { return false; } for (int i = str.length(); --i >= 0; ) { @@ -150,20 +258,28 @@ public class StrUtil extends StrKit { } /** - * 这个字符串是否是小数点 + * 这个字符串是否是可能包含小数点的数字 * * @param str * @return */ public static boolean isDecimal(String str) { - if (str == null) { + if (isBlank(str)) { return false; } + boolean hasDot = false; for (int i = str.length(); --i >= 0; ) { int chr = str.charAt(i); if ((chr < 48 || chr > 57) && chr != '.') { return false; } + if (chr == '.') { + if (hasDot) { + return false; + } else { + hasDot = true; + } + } } return true; } @@ -175,18 +291,18 @@ public class StrUtil extends StrKit { * @return */ public static boolean isEmail(String email) { - return Pattern.matches("\\w+@(\\w+.)+[a-z]{2,3}", email); + return isNotBlank(email) && email.matches(Regex.EMAIL); } /** * 是否是中国地区手机号码 * - * @param phoneNumber + * @param mobileNumber * @return */ - public static boolean isMobileNumber(String phoneNumber) { - return Pattern.matches("^(1[3,4,5,6,7,8,9])\\d{9}$", phoneNumber); + public static boolean isMobileNumber(String mobileNumber) { + return isNotBlank(mobileNumber) && mobileNumber.matches(Regex.MOBILE); } @@ -200,6 +316,16 @@ public class StrUtil extends StrKit { } + /** + * 根据逗号分隔为set + * + * @param src + * @return + */ + public static Set splitToSetByComma(String src) { + return splitToSet(src, ","); + } + /** * 把字符串拆分成一个set * @@ -213,7 +339,7 @@ public class StrUtil extends StrKit { } String[] strings = src.split(regex); - Set set = new HashSet<>(); + Set set = new LinkedHashSet<>(); for (String s : strings) { if (StrUtil.isBlank(s)) { continue; @@ -228,12 +354,84 @@ public class StrUtil extends StrKit { private static final String[] escapeChars = {"&", "<", ">", "'", """}; public static String escapeHtml(String content) { - return isBlank(content) ? content : StringUtils.replaceEach(unEscapeHtml(content), htmlChars, escapeChars); + return isBlank(content) ? content : StringUtils.replaceEach(content, htmlChars, escapeChars); } public static String unEscapeHtml(String content) { return isBlank(content) ? content : StringUtils.replaceEach(content, escapeChars, htmlChars); } + public static Model escapeModel(Model model, String... ignoreAttrs) { + String[] attrNames = model._getAttrNames(); + for (String attr : attrNames) { + + if (ArrayUtils.contains(ignoreAttrs, attr)) { + continue; + } + + Object value = model.get(attr); + + if (value instanceof String) { + model.set(attr, escapeHtml(value.toString())); + } + } + + return model; + } + + public static Map escapeMap(Map map, Object... ignoreKeys) { + if (map == null || map.isEmpty()) { + return map; + } + + Set keys = map.keySet(); + for (Object key : keys) { + if (ArrayUtils.contains(ignoreKeys, key)) { + continue; + } + + Object value = map.get(key); + + if (value instanceof String) { + map.put(key, escapeHtml(value.toString())); + } + } + + return map; + } + + + public static String join(String[] array, String split) { + if (array == null || array.length == 0) { + return EMPTY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + sb.append(split); + } + sb.append(array[i]); + } + return sb.toString(); + } + + + public static String join(Collection coll, String split) { + if (coll == null || coll.isEmpty()) { + return EMPTY; + } + + StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (String s : coll) { + if (isFirst) { + isFirst = false; + } else { + sb.append(split); + } + sb.append(s); + } + return sb.toString(); + } } diff --git a/src/main/java/io/jboot/utils/TypeDef.java b/src/main/java/io/jboot/utils/TypeDef.java new file mode 100644 index 0000000000000000000000000000000000000000..ddcb38d6e2a23a400facca91abfdd7cd4f5f43b5 --- /dev/null +++ b/src/main/java/io/jboot/utils/TypeDef.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.utils; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class TypeDef { + + public static final TypeDef LIST_STRING = new TypeDef>() { + }; + public static final TypeDef LIST_INTEGER = new TypeDef>() { + }; + public static final TypeDef LIST_LONG = new TypeDef>() { + }; + public static final TypeDef LIST_DOUBLE = new TypeDef>() { + }; + public static final TypeDef LIST_FLOAT = new TypeDef>() { + }; + public static final TypeDef LIST_BIGINTEGER = new TypeDef>() { + }; + public static final TypeDef LIST_BIGDECIMAL = new TypeDef>() { + }; + + + public static final TypeDef SET_STRING = new TypeDef>() { + }; + public static final TypeDef SET_INTEGER = new TypeDef>() { + }; + public static final TypeDef SET_LONG = new TypeDef>() { + }; + public static final TypeDef SET_DOUBLE = new TypeDef>() { + }; + public static final TypeDef SET_FLOAT = new TypeDef>() { + }; + public static final TypeDef SET_BIGINTEGER = new TypeDef>() { + }; + public static final TypeDef SET_BIGDECIMAL = new TypeDef>() { + }; + + + public static final TypeDef MAP_STRING = new TypeDef>() { + }; + public static final TypeDef MAP_INTEGER = new TypeDef>() { + }; + public static final TypeDef MAP_LONG = new TypeDef>() { + }; + public static final TypeDef MAP_DOUBLE = new TypeDef>() { + }; + public static final TypeDef MAP_FLOAT = new TypeDef>() { + }; + public static final TypeDef MAP_BIGINTEGER = new TypeDef>() { + }; + public static final TypeDef MAP_BIGDECIMAL = new TypeDef>() { + }; + + + protected Type type; + protected Class defClass; + + + protected TypeDef() { + Type superClass = getClass().getGenericSuperclass(); + if (superClass == TypeDef.class) { + throw new IllegalArgumentException("Must appoint generic class in TypeDef."); + } + + Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + if (type instanceof ParameterizedType) { + ParameterizedType paraType = (ParameterizedType) type; + this.type = paraType; + this.defClass = (Class) paraType.getRawType(); + } else { + this.type = type; + this.defClass = (Class) type; + } + } + + + private TypeDef(Class rawType, Type actualTypeArguments) { + this.defClass = rawType; + this.type = actualTypeArguments; + } + + public void setType(Type type) { + this.type = type; + } + + public void setDefClass(Class defClass) { + this.defClass = defClass; + } + + public Type getType() { + return type; + } + + + public Class getDefClass() { + return defClass; + } + + + public static TypeDef wrapper(Class rawType, Type... actualTypeArguments) { + Type type = new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return actualTypeArguments; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type getOwnerType() { + return null; + } + }; + return new TypeDef<>(rawType, type); + } + +} diff --git a/src/main/java/io/jboot/web/HttpStatus.java b/src/main/java/io/jboot/web/HttpStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..569f98d8ecf52384417f827830b1276fb60bb8a3 --- /dev/null +++ b/src/main/java/io/jboot/web/HttpStatus.java @@ -0,0 +1,423 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/4/6 + */ +public enum HttpStatus { + + /** + * {@code 100 Continue}. + * @see HTTP/1.1: Semantics and Content, section 6.2.1 + */ + CONTINUE(100, "Continue"), + /** + * {@code 101 Switching Protocols}. + * @see HTTP/1.1: Semantics and Content, section 6.2.2 + */ + SWITCHING_PROTOCOLS(101, "Switching Protocols"), + /** + * {@code 102 Processing}. + * @see WebDAV + */ + PROCESSING(102, "Processing"), + /** + * {@code 103 Checkpoint}. + * @see A proposal for supporting + * resumable POST/PUT HTTP requests in HTTP/1.0 + */ + CHECKPOINT(103, "Checkpoint"), + + // 2xx Success + + /** + * {@code 200 OK}. + * @see HTTP/1.1: Semantics and Content, section 6.3.1 + */ + OK(200, "OK"), + /** + * {@code 201 Created}. + * @see HTTP/1.1: Semantics and Content, section 6.3.2 + */ + CREATED(201, "Created"), + /** + * {@code 202 Accepted}. + * @see HTTP/1.1: Semantics and Content, section 6.3.3 + */ + ACCEPTED(202, "Accepted"), + /** + * {@code 203 Non-Authoritative Information}. + * @see HTTP/1.1: Semantics and Content, section 6.3.4 + */ + NON_AUTHORITATIVE_INFORMATION(203, "Non-Authoritative Information"), + /** + * {@code 204 No Content}. + * @see HTTP/1.1: Semantics and Content, section 6.3.5 + */ + NO_CONTENT(204, "No Content"), + /** + * {@code 205 Reset Content}. + * @see HTTP/1.1: Semantics and Content, section 6.3.6 + */ + RESET_CONTENT(205, "Reset Content"), + /** + * {@code 206 Partial Content}. + * @see HTTP/1.1: Range Requests, section 4.1 + */ + PARTIAL_CONTENT(206, "Partial Content"), + /** + * {@code 207 Multi-Status}. + * @see WebDAV + */ + MULTI_STATUS(207, "Multi-Status"), + /** + * {@code 208 Already Reported}. + * @see WebDAV Binding Extensions + */ + ALREADY_REPORTED(208, "Already Reported"), + /** + * {@code 226 IM Used}. + * @see Delta encoding in HTTP + */ + IM_USED(226, "IM Used"), + + // 3xx Redirection + + /** + * {@code 300 Multiple Choices}. + * @see HTTP/1.1: Semantics and Content, section 6.4.1 + */ + MULTIPLE_CHOICES(300, "Multiple Choices"), + /** + * {@code 301 Moved Permanently}. + * @see HTTP/1.1: Semantics and Content, section 6.4.2 + */ + MOVED_PERMANENTLY(301, "Moved Permanently"), + /** + * {@code 302 Found}. + * @see HTTP/1.1: Semantics and Content, section 6.4.3 + */ + FOUND(302, "Found"), + /** + * {@code 302 Moved Temporarily}. + * @see HTTP/1.0, section 9.3 + * @deprecated in favor of {@link #FOUND} which will be returned from {@code HttpStatus.valueOf(302)} + */ + @Deprecated + MOVED_TEMPORARILY(302, "Moved Temporarily"), + /** + * {@code 303 See Other}. + * @see HTTP/1.1: Semantics and Content, section 6.4.4 + */ + SEE_OTHER(303, "See Other"), + /** + * {@code 304 Not Modified}. + * @see HTTP/1.1: Conditional Requests, section 4.1 + */ + NOT_MODIFIED(304, "Not Modified"), + /** + * {@code 305 Use Proxy}. + * @see HTTP/1.1: Semantics and Content, section 6.4.5 + * @deprecated due to security concerns regarding in-band configuration of a proxy + */ + @Deprecated + USE_PROXY(305, "Use Proxy"), + /** + * {@code 307 Temporary Redirect}. + * @see HTTP/1.1: Semantics and Content, section 6.4.7 + */ + TEMPORARY_REDIRECT(307, "Temporary Redirect"), + /** + * {@code 308 Permanent Redirect}. + * @see RFC 7238 + */ + PERMANENT_REDIRECT(308, "Permanent Redirect"), + + // --- 4xx Client Error --- + + /** + * {@code 400 Bad Request}. + * @see HTTP/1.1: Semantics and Content, section 6.5.1 + */ + BAD_REQUEST(400, "Bad Request"), + /** + * {@code 401 Unauthorized}. + * @see HTTP/1.1: Authentication, section 3.1 + */ + UNAUTHORIZED(401, "Unauthorized"), + /** + * {@code 402 Payment Required}. + * @see HTTP/1.1: Semantics and Content, section 6.5.2 + */ + PAYMENT_REQUIRED(402, "Payment Required"), + /** + * {@code 403 Forbidden}. + * @see HTTP/1.1: Semantics and Content, section 6.5.3 + */ + FORBIDDEN(403, "Forbidden"), + /** + * {@code 404 Not Found}. + * @see HTTP/1.1: Semantics and Content, section 6.5.4 + */ + NOT_FOUND(404, "Not Found"), + /** + * {@code 405 Method Not Allowed}. + * @see HTTP/1.1: Semantics and Content, section 6.5.5 + */ + METHOD_NOT_ALLOWED(405, "Method Not Allowed"), + /** + * {@code 406 Not Acceptable}. + * @see HTTP/1.1: Semantics and Content, section 6.5.6 + */ + NOT_ACCEPTABLE(406, "Not Acceptable"), + /** + * {@code 407 Proxy Authentication Required}. + * @see HTTP/1.1: Authentication, section 3.2 + */ + PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"), + /** + * {@code 408 Request Timeout}. + * @see HTTP/1.1: Semantics and Content, section 6.5.7 + */ + REQUEST_TIMEOUT(408, "Request Timeout"), + /** + * {@code 409 Conflict}. + * @see HTTP/1.1: Semantics and Content, section 6.5.8 + */ + CONFLICT(409, "Conflict"), + /** + * {@code 410 Gone}. + * @see + * HTTP/1.1: Semantics and Content, section 6.5.9 + */ + GONE(410, "Gone"), + /** + * {@code 411 Length Required}. + * @see + * HTTP/1.1: Semantics and Content, section 6.5.10 + */ + LENGTH_REQUIRED(411, "Length Required"), + /** + * {@code 412 Precondition failed}. + * @see + * HTTP/1.1: Conditional Requests, section 4.2 + */ + PRECONDITION_FAILED(412, "Precondition Failed"), + /** + * {@code 413 Payload Too Large}. + * @since 4.1 + * @see + * HTTP/1.1: Semantics and Content, section 6.5.11 + */ + PAYLOAD_TOO_LARGE(413, "Payload Too Large"), + /** + * {@code 413 Request Entity Too Large}. + * @see HTTP/1.1, section 10.4.14 + * @deprecated in favor of {@link #PAYLOAD_TOO_LARGE} which will be + * returned from {@code HttpStatus.valueOf(413)} + */ + @Deprecated + REQUEST_ENTITY_TOO_LARGE(413, "Request Entity Too Large"), + /** + * {@code 414 URI Too Long}. + * @since 4.1 + * @see + * HTTP/1.1: Semantics and Content, section 6.5.12 + */ + URI_TOO_LONG(414, "URI Too Long"), + /** + * {@code 414 Request-URI Too Long}. + * @see HTTP/1.1, section 10.4.15 + * @deprecated in favor of {@link #URI_TOO_LONG} which will be returned from {@code HttpStatus.valueOf(414)} + */ + @Deprecated + REQUEST_URI_TOO_LONG(414, "Request-URI Too Long"), + /** + * {@code 415 Unsupported Media Type}. + * @see + * HTTP/1.1: Semantics and Content, section 6.5.13 + */ + UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), + /** + * {@code 416 Requested Range Not Satisfiable}. + * @see HTTP/1.1: Range Requests, section 4.4 + */ + REQUESTED_RANGE_NOT_SATISFIABLE(416, "Requested range not satisfiable"), + /** + * {@code 417 Expectation Failed}. + * @see + * HTTP/1.1: Semantics and Content, section 6.5.14 + */ + EXPECTATION_FAILED(417, "Expectation Failed"), + /** + * {@code 418 I'm a teapot}. + * @see HTCPCP/1.0 + */ + I_AM_A_TEAPOT(418, "I'm a teapot"), + /** + * @deprecated See + * + * WebDAV Draft Changes + */ + @Deprecated + INSUFFICIENT_SPACE_ON_RESOURCE(419, "Insufficient Space On Resource"), + /** + * @deprecated See + * + * WebDAV Draft Changes + */ + @Deprecated + METHOD_FAILURE(420, "Method Failure"), + /** + * @deprecated + * See + * WebDAV Draft Changes + */ + @Deprecated + DESTINATION_LOCKED(421, "Destination Locked"), + /** + * {@code 422 Unprocessable Entity}. + * @see WebDAV + */ + UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"), + /** + * {@code 423 Locked}. + * @see WebDAV + */ + LOCKED(423, "Locked"), + /** + * {@code 424 Failed Dependency}. + * @see WebDAV + */ + FAILED_DEPENDENCY(424, "Failed Dependency"), + /** + * {@code 426 Upgrade Required}. + * @see Upgrading to TLS Within HTTP/1.1 + */ + UPGRADE_REQUIRED(426, "Upgrade Required"), + /** + * {@code 428 Precondition Required}. + * @see Additional HTTP Status Codes + */ + PRECONDITION_REQUIRED(428, "Precondition Required"), + /** + * {@code 429 Too Many Requests}. + * @see Additional HTTP Status Codes + */ + TOO_MANY_REQUESTS(429, "Too Many Requests"), + /** + * {@code 431 Request Header Fields Too Large}. + * @see Additional HTTP Status Codes + */ + REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"), + /** + * {@code 451 Unavailable For Legal Reasons}. + * @see + * An HTTP Status Code to Report Legal Obstacles + * @since 4.3 + */ + UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable For Legal Reasons"), + + // --- 5xx Server Error --- + + /** + * {@code 500 Internal Server Error}. + * @see HTTP/1.1: Semantics and Content, section 6.6.1 + */ + INTERNAL_SERVER_ERROR(500, "Internal Server Error"), + /** + * {@code 501 Not Implemented}. + * @see HTTP/1.1: Semantics and Content, section 6.6.2 + */ + NOT_IMPLEMENTED(501, "Not Implemented"), + /** + * {@code 502 Bad Gateway}. + * @see HTTP/1.1: Semantics and Content, section 6.6.3 + */ + BAD_GATEWAY(502, "Bad Gateway"), + /** + * {@code 503 Service Unavailable}. + * @see HTTP/1.1: Semantics and Content, section 6.6.4 + */ + SERVICE_UNAVAILABLE(503, "Service Unavailable"), + /** + * {@code 504 Gateway Timeout}. + * @see HTTP/1.1: Semantics and Content, section 6.6.5 + */ + GATEWAY_TIMEOUT(504, "Gateway Timeout"), + /** + * {@code 505 HTTP Version Not Supported}. + * @see HTTP/1.1: Semantics and Content, section 6.6.6 + */ + HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version not supported"), + /** + * {@code 506 Variant Also Negotiates} + * @see Transparent Content Negotiation + */ + VARIANT_ALSO_NEGOTIATES(506, "Variant Also Negotiates"), + /** + * {@code 507 Insufficient Storage} + * @see WebDAV + */ + INSUFFICIENT_STORAGE(507, "Insufficient Storage"), + /** + * {@code 508 Loop Detected} + * @see WebDAV Binding Extensions + */ + LOOP_DETECTED(508, "Loop Detected"), + /** + * {@code 509 Bandwidth Limit Exceeded} + */ + BANDWIDTH_LIMIT_EXCEEDED(509, "Bandwidth Limit Exceeded"), + /** + * {@code 510 Not Extended} + * @see HTTP Extension Framework + */ + NOT_EXTENDED(510, "Not Extended"), + /** + * {@code 511 Network Authentication Required}. + * @see Additional HTTP Status Codes + */ + NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required"); + + + private final int value; + + private final String reasonPhrase; + + + HttpStatus(int value, String reasonPhrase) { + this.value = value; + this.reasonPhrase = reasonPhrase; + } + + + /** + * Return the integer value of this status code. + */ + public int value() { + return this.value; + } + + /** + * Return the reason phrase of this status code. + */ + public String getReasonPhrase() { + return this.reasonPhrase; + } +} diff --git a/src/main/java/io/jboot/web/JbootActionMapping.java b/src/main/java/io/jboot/web/JbootActionMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..fefda0772b63afa9f44f0d7a25c7fbb552f9e1ee --- /dev/null +++ b/src/main/java/io/jboot/web/JbootActionMapping.java @@ -0,0 +1,151 @@ +package io.jboot.web; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.InterceptorManager; +import com.jfinal.config.Routes; +import com.jfinal.core.*; +import io.jboot.utils.ArrayUtil; +import io.jboot.utils.ClassUtil; + +import java.lang.reflect.*; + +public class JbootActionMapping extends ActionMapping { + + public JbootActionMapping(Routes routes) { + super(routes); + } + + @Override + protected void buildActionMapping() { + mapping.clear(); + Class dc; + InterceptorManager interMan = InterceptorManager.me(); + for (Routes routes : getRoutesList()) { + for (Routes.Route route : routes.getRouteItemList()) { + Class controllerClass = route.getControllerClass(); + Interceptor[] controllerInters = interMan.createControllerInterceptor(controllerClass); + + boolean declaredMethods = !routes.getMappingSuperClass() || controllerClass.getSuperclass() == Controller.class; + + Method[] methods = (declaredMethods ? controllerClass.getDeclaredMethods() : controllerClass.getMethods()); + for (Method method : methods) { + if (declaredMethods) { + if (!Modifier.isPublic(method.getModifiers())) + continue; + } else { + dc = method.getDeclaringClass(); + if (dc == Controller.class || dc == Object.class) + continue; + } + + if (method.getAnnotation(NotAction.class) != null) { + continue; + } + + Interceptor[] actionInters = interMan.buildControllerActionInterceptor(routes.getInterceptors(), controllerInters, controllerClass, method); + String controllerPath = route.getControllerPath(); + + String methodName = method.getName(); + ActionKey ak = method.getAnnotation(ActionKey.class); + String actionKey; + if (ak != null) { + actionKey = ak.value().trim(); + if ("".equals(actionKey)) { + throw new IllegalArgumentException(controllerClass.getName() + "." + methodName + "(): The argument of ActionKey can not be blank."); + } + + if (actionKey.startsWith(SLASH)) { + //actionKey = actionKey + } else if (actionKey.startsWith("./")) { + actionKey = controllerPath + actionKey.substring(1); + } else { + actionKey = SLASH + actionKey; + } +// if (!actionKey.startsWith(SLASH)) { +// actionKey = SLASH + actionKey; +// } + } else if (methodName.equals("index")) { + actionKey = controllerPath; + } else { + actionKey = controllerPath.equals(SLASH) ? SLASH + methodName : controllerPath + SLASH + methodName; + } + +// Action action = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath())); +// if (mapping.put(actionKey, action) != null) { +// throw new RuntimeException(buildMsg(actionKey, controllerClass, method)); +// } + + Action newAction = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath())); + Action existAction = mapping.get(actionKey); + if (existAction == null) { + mapping.put(actionKey, newAction); + } else { + + Type controllerType = controllerClass.getGenericSuperclass(); + Method existActionMethod = existAction.getMethod(); + + // 不是泛型 + if (!(controllerType instanceof ParameterizedType)) { + throw new RuntimeException(buildMsg(actionKey, method, existActionMethod)); + } + + if (method.getParameterCount() == 0 + || method.getParameterCount() != existActionMethod.getParameterCount() + || method.getDeclaringClass() != existActionMethod.getDeclaringClass()) { + throw new RuntimeException(buildMsg(actionKey, method, existActionMethod)); + } + + Type[] argumentTypes = ((ParameterizedType) controllerType).getActualTypeArguments(); + + Class[] paraTypes = method.getParameterTypes(); + Class[] existParaTypes = existActionMethod.getParameterTypes(); + + for (int i = 0; i < paraTypes.length; i++) { + Class newType = paraTypes[i]; + Class existType = existParaTypes[i]; + if (newType == existType) { + continue; + } + // newType 是父类 + else if (newType.isAssignableFrom(existType) && ArrayUtil.contains(argumentTypes, existType)) { + break; + } + // newType 是子类 + else if (existType.isAssignableFrom(newType) && ArrayUtil.contains(argumentTypes, newType)) { + mapping.put(actionKey, newAction); + break; + } else { + throw new RuntimeException(buildMsg(actionKey, method, existActionMethod)); + } + } + + } + } + } + } + routes.clear(); + + // support url = controllerPath + urlParas with "/" of controllerPath + Action action = mapping.get("/"); + if (action != null) { + mapping.put("", action); + } + } + + + protected String buildMsg(String actionKey, Method method, Method existMethod) { + StringBuilder sb = new StringBuilder("The action \"") + .append(ClassUtil.buildMethodString(method)) + .append("\" can not be mapped, actionKey \"") + .append(actionKey) + .append("\" is already in used by: \"") + .append(ClassUtil.buildMethodString(existMethod)) + .append("\""); + + String msg = sb.toString(); + System.err.println("\nException: " + msg); + return msg; + } + + +} diff --git a/src/main/java/io/jboot/web/JbootWebConfig.java b/src/main/java/io/jboot/web/JbootWebConfig.java index da22e0474f3ed6c63136c086d5960821b5667839..0de4c24721a381725bb34ea234e0eace39c03b7f 100644 --- a/src/main/java/io/jboot/web/JbootWebConfig.java +++ b/src/main/java/io/jboot/web/JbootWebConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,21 +15,23 @@ */ package io.jboot.web; +import io.jboot.app.config.JbootConfigManager; import io.jboot.app.config.annotation.ConfigModel; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web */ @ConfigModel(prefix = "jboot.web") public class JbootWebConfig { public static final String DEFAULT_COOKIE_ENCRYPT_KEY = "JBOOT_DEFAULT_ENCRYPT_KEY"; - private String cookieEncryptKey = DEFAULT_COOKIE_ENCRYPT_KEY; + private int cookieMaxAge = 60 * 60 * 24 * 2; // 2 days(单位:秒) private String webSocketEndpoint; + private boolean escapeParasEnable = false; + private boolean pathVariableEnable = false; public String getCookieEncryptKey() { return cookieEncryptKey; @@ -39,6 +41,14 @@ public class JbootWebConfig { this.cookieEncryptKey = cookieEncryptKey; } + public int getCookieMaxAge() { + return cookieMaxAge; + } + + public void setCookieMaxAge(int cookieMaxAge) { + this.cookieMaxAge = cookieMaxAge; + } + public String getWebSocketEndpoint() { return webSocketEndpoint; } @@ -46,4 +56,29 @@ public class JbootWebConfig { public void setWebSocketEndpoint(String webSocketEndpoint) { this.webSocketEndpoint = webSocketEndpoint; } + + public boolean isEscapeParasEnable() { + return escapeParasEnable; + } + + public void setEscapeParasEnable(boolean escapeParasEnable) { + this.escapeParasEnable = escapeParasEnable; + } + + public boolean isPathVariableEnable() { + return pathVariableEnable; + } + + public void setPathVariableEnable(boolean pathVariableEnable) { + this.pathVariableEnable = pathVariableEnable; + } + + private static JbootWebConfig me; + + public static JbootWebConfig getInstance() { + if (me == null) { + me = JbootConfigManager.me().get(JbootWebConfig.class); + } + return me; + } } diff --git a/src/main/java/io/jboot/web/PathVariableActionMapping.java b/src/main/java/io/jboot/web/PathVariableActionMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..5d17efde633a107ab0db4a1a1fd0c157d8f9b755 --- /dev/null +++ b/src/main/java/io/jboot/web/PathVariableActionMapping.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web; + +import com.google.common.base.Joiner; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.InterceptorManager; +import com.jfinal.config.Routes; +import com.jfinal.core.Action; +import com.jfinal.core.ActionKey; +import com.jfinal.core.Controller; +import com.jfinal.core.NotAction; +import io.jboot.utils.AntPathMatcher; +import io.jboot.utils.ArrayUtil; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author 没牙的小朋友 (mjl@nxu.edu.cn) + * @version V1.0 + */ +public class PathVariableActionMapping extends JbootActionMapping { + private static final String PATH_VARIABLE_URL_PATTERN = ".*\\{[a-zA-Z0-9]+\\}.*"; + protected Map pathVariableUrlMapping = new ConcurrentHashMap<>(); + private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); + + public PathVariableActionMapping(Routes routes) { + super(routes); + } + + @Override + protected void buildActionMapping() { + mapping.clear(); + Class dc; + InterceptorManager interMan = InterceptorManager.me(); + for (Routes routes : getRoutesList()) { + for (Routes.Route route : routes.getRouteItemList()) { + Class controllerClass = route.getControllerClass(); + Interceptor[] controllerInters = interMan.createControllerInterceptor(controllerClass); + + boolean declaredMethods = !routes.getMappingSuperClass() || controllerClass.getSuperclass() == Controller.class; + + Method[] methods = (declaredMethods ? controllerClass.getDeclaredMethods() : controllerClass.getMethods()); + for (Method method : methods) { + if (declaredMethods) { + if (!Modifier.isPublic(method.getModifiers())) + continue; + } else { + dc = method.getDeclaringClass(); + if (dc == Controller.class || dc == Object.class) + continue; + } + + if (method.getAnnotation(NotAction.class) != null) { + continue; + } + + Interceptor[] actionInters = interMan.buildControllerActionInterceptor(routes.getInterceptors(), controllerInters, controllerClass, method); + String controllerPath = route.getControllerPath(); + + String methodName = method.getName(); + ActionKey ak = method.getAnnotation(ActionKey.class); + String actionKey; + if (ak != null) { + actionKey = ak.value().trim(); + if ("".equals(actionKey)) { + throw new IllegalArgumentException(controllerClass.getName() + "." + methodName + "(): The argument of ActionKey can not be blank."); + } + if (actionKey.matches(PATH_VARIABLE_URL_PATTERN)) { + Action pathVariableAction = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters, + route.getFinalViewPath(routes.getBaseViewPath())); + pathVariableUrlMapping.put(actionKey, pathVariableAction); + } + if (actionKey.startsWith(SLASH)) { + //actionKey = actionKey + } else if (actionKey.startsWith("./")) { + actionKey = controllerPath + actionKey.substring(1); + } else { + actionKey = SLASH + actionKey; + } +// if (!actionKey.startsWith(SLASH)) { +// actionKey = SLASH + actionKey; +// } + } else if (methodName.equals("index")) { + actionKey = controllerPath; + } else { + actionKey = controllerPath.equals(SLASH) ? SLASH + methodName : controllerPath + SLASH + methodName; + } + +// Action action = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath())); +// if (mapping.put(actionKey, action) != null) { +// throw new RuntimeException(buildMsg(actionKey, controllerClass, method)); +// } + + Action newAction = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath())); + Action existAction = mapping.get(actionKey); + if (existAction == null) { + mapping.put(actionKey, newAction); + } else { + + Type controllerType = controllerClass.getGenericSuperclass(); + Method existActionMethod = existAction.getMethod(); + + // 不是泛型 + if (!(controllerType instanceof ParameterizedType)) { + throw new RuntimeException(buildMsg(actionKey, method, existActionMethod)); + } + + if (method.getParameterCount() == 0 + || method.getParameterCount() != existActionMethod.getParameterCount() + || method.getDeclaringClass() != existActionMethod.getDeclaringClass()) { + throw new RuntimeException(buildMsg(actionKey, method, existActionMethod)); + } + + Type[] argumentTypes = ((ParameterizedType) controllerType).getActualTypeArguments(); + + Class[] paraTypes = method.getParameterTypes(); + Class[] existParaTypes = existActionMethod.getParameterTypes(); + + for (int i = 0; i < paraTypes.length; i++) { + Class newType = paraTypes[i]; + Class existType = existParaTypes[i]; + if (newType == existType) { + continue; + } + // newType 是父类 + else if (newType.isAssignableFrom(existType) && ArrayUtil.contains(argumentTypes, existType)) { + break; + } + // newType 是子类 + else if (existType.isAssignableFrom(newType) && ArrayUtil.contains(argumentTypes, newType)) { + mapping.put(actionKey, newAction); + break; + } else { + throw new RuntimeException(buildMsg(actionKey, method, existActionMethod)); + } + } + + } + } + } + } + routes.clear(); + + // support url = controllerPath + urlParas with "/" of controllerPath + Action action = mapping.get("/"); + if (action != null) { + mapping.put("", action); + } + } + + + + + /** + * Support four types of url + * 1: http://abc.com/controllerPath ---> 00 + * 2: http://abc.com/controllerPath/para ---> 01 + * 3: http://abc.com/controllerPath/method ---> 10 + * 4: http://abc.com/controllerPath/method/para ---> 11 + * 5: http://abc.com/foo/{id}/bar/{name} + * The controllerPath can also contains "/" + * Example: http://abc.com/uvw/xyz/method/para + */ + @Override + public Action getAction(String url, String[] urlPara) { + Action action = mapping.get(url); + if (action != null) { + return action; + } + for (String pattern : pathVariableUrlMapping.keySet()) { + //判断是否有匹配包含路径参数的URL映射 + if (antPathMatcher.match(pattern, url)) { + Action pathVariableUrlAction = pathVariableUrlMapping.get(pattern); + Map pathVariableValues = antPathMatcher.extractUriTemplateVariables(pattern, url); + urlPara[0] = null; + if (urlPara.length > 1) { + //urlPara[1]作为路径参数传入controller + urlPara[1] = Joiner.on("&").withKeyValueSeparator("=").join(pathVariableValues); + } + return pathVariableUrlAction; + } + } + // -------- + int i = url.lastIndexOf('/'); + if (i != -1) { + action = mapping.get(url.substring(0, i)); + if (action != null) { + urlPara[0] = url.substring(i + 1); + } + } + + return action; + } +} diff --git a/src/main/java/io/jboot/web/ResponseEntity.java b/src/main/java/io/jboot/web/ResponseEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..29921c55a7a18239f6fa62d945812d48bf7ca852 --- /dev/null +++ b/src/main/java/io/jboot/web/ResponseEntity.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/4/6 + */ +public class ResponseEntity { + + //响应的数据 + private Object body; + + //自定义响应头部信息 + private Map headers; + + //默认http状态 + private HttpStatus httpStatus = HttpStatus.OK; + + + public ResponseEntity() { + } + + public ResponseEntity(Object body) { + this.body = body; + } + + public ResponseEntity(Map headers, HttpStatus httpStatus) { + this.headers = headers; + this.httpStatus = httpStatus; + } + + public ResponseEntity(Object body, Map headers, HttpStatus httpStatus) { + this.body = body; + this.headers = headers; + this.httpStatus = httpStatus; + } + + public ResponseEntity body(Object body) { + this.body = body; + return this; + } + + public ResponseEntity header(String key, String value) { + if (this.headers == null) { + this.headers = new HashMap<>(); + } + this.headers.put(key, value); + return this; + } + + public ResponseEntity status(int status) { + for (HttpStatus httpStatus : HttpStatus.values()) { + if (Objects.equals(httpStatus.value(), status)) { + this.httpStatus = httpStatus; + break; + } + } + return this; + } + + + public ResponseEntity status(HttpStatus status) { + this.httpStatus = status; + return this; + } + + + public T getBody() { + return (T) body; + } + + public Map getHeaders() { + return headers; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + + public static ResponseEntity ok() { + ResponseEntity responseEntity = new ResponseEntity(); + return responseEntity.status(HttpStatus.OK); + } + + +} diff --git a/src/main/java/io/jboot/web/attachment/AttachmentContainer.java b/src/main/java/io/jboot/web/attachment/AttachmentContainer.java new file mode 100644 index 0000000000000000000000000000000000000000..60aa60a483824365965d78f415dc01fa79c31696 --- /dev/null +++ b/src/main/java/io/jboot/web/attachment/AttachmentContainer.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jboot.web.attachment; + +import java.io.File; +import java.io.InputStream; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public interface AttachmentContainer { + + /** + * 保存文件 + * + * @param file + * @return 返回文件的相对路径 + */ + String saveFile(File file); + + + /** + * 保存文件 + * + * @param file + * @return 返回文件的相对路径 + */ + String saveFile(File file, String toRelativePath); + + /** + * 保存文件 + * + * @param inputStream + * @return + */ + String saveFile(InputStream inputStream, String toRelativePath); + + + /** + * 删除文件 + * + * @param relativePath + * @return + */ + boolean deleteFile(String relativePath); + + + /** + * 通过相对路径获取文件 + * + * @param relativePath + * @return + */ + File getFile(String relativePath); + + + /** + * 通过一个文件,获取其相对路径 + * + * @param file + * @return + */ + String getRelativePath(File file); + + +} diff --git a/src/main/java/io/jboot/web/attachment/AttachmentHandler.java b/src/main/java/io/jboot/web/attachment/AttachmentHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..fa5456d12b770c04933e15d36b95d8b24bbd04b3 --- /dev/null +++ b/src/main/java/io/jboot/web/attachment/AttachmentHandler.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.attachment; + +import com.jfinal.handler.Handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class AttachmentHandler extends Handler { + + private AttachmentManager manager = AttachmentManager.me(); + + @Override + public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { + // manager 成功渲染文件 + if (manager.renderFile(target, request, response)) { + isHandled[0] = true; + } + // manager 不渲染,由容器本身去渲染 + else { + next.handle(target, request, response, isHandled); + } + } +} diff --git a/src/main/java/io/jboot/web/attachment/AttachmentManager.java b/src/main/java/io/jboot/web/attachment/AttachmentManager.java new file mode 100644 index 0000000000000000000000000000000000000000..306525c6dbc18b1a81b47374778e8c98c72ea559 --- /dev/null +++ b/src/main/java/io/jboot/web/attachment/AttachmentManager.java @@ -0,0 +1,320 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jboot.web.attachment; + +import com.jfinal.log.Log; +import com.jfinal.render.IRenderFactory; +import com.jfinal.render.Render; +import com.jfinal.render.RenderManager; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class AttachmentManager { + + private static final Log LOG = Log.getLog(AttachmentManager.class); + + private static Map managers = new HashMap<>(); + + public static AttachmentManager me() { + return use("default"); + } + + public static AttachmentManager use(String name) { + AttachmentManager manager = managers.get(name); + if (manager == null) { + synchronized (AttachmentManager.class) { + manager = managers.get(name); + if (manager == null) { + manager = new AttachmentManager(name); + managers.put(name, manager); + } + } + } + return manager; + } + + /** + * 通过这个方式可以来更改 manager 包括默认的 manager + * + * @param manager + */ + public static void setManager(AttachmentManager manager) { + managers.put(manager.name, manager); + } + + + public AttachmentManager(String name) { + this.name = name; + } + + /** + * 默认的 附件容器 + */ + protected LocalAttachmentContainer defaultContainer = new LocalAttachmentContainer(); + + /** + * 其他附件容器 + */ + protected List containers = new CopyOnWriteArrayList<>(); + + /** + * 自定义文件渲染器 + */ + protected IRenderFactory renderFactory = RenderManager.me().getRenderFactory(); + + /** + * manager 的名称 + */ + protected final String name; + + + public String getName() { + return name; + } + + public IRenderFactory getRenderFactory() { + return renderFactory; + } + + public void setRenderFactory(IRenderFactory renderFactory) { + this.renderFactory = renderFactory; + } + + + public LocalAttachmentContainer getDefaultContainer() { + return defaultContainer; + } + + public void setDefaultContainer(LocalAttachmentContainer defaultContainer) { + this.defaultContainer = defaultContainer; + } + + public void addContainer(AttachmentContainer container) { + containers.add(container); + } + + public void setContainers(List containers) { + this.containers = containers; + } + + + public List getContainers() { + return containers; + } + + + /** + * 保存文件 + * + * @param file + * @return 返回文件的相对路径 + */ + public String saveFile(File file) { + //优先从 默认的 container 去保存文件 + String relativePath = defaultContainer.saveFile(file); + File defaultContainerFile = defaultContainer.getFile(relativePath); + + for (AttachmentContainer container : containers) { + try { + if (container != defaultContainer) { + container.saveFile(defaultContainerFile); + } + } catch (Exception ex) { + LOG.error("Save file error in container: " + container, ex); + } + } + return relativePath.replace("\\", "/"); + } + + + /** + * 保存文件 + * + * @param file + * @return 返回文件的相对路径 + */ + public String saveFile(File file, String toRelativePath) { + //优先从 默认的 container 去保存文件 + String relativePath = defaultContainer.saveFile(file, toRelativePath); + File defaultContainerFile = defaultContainer.getFile(relativePath); + + for (AttachmentContainer container : containers) { + try { + if (container != defaultContainer) { + container.saveFile(defaultContainerFile, toRelativePath); + } + } catch (Exception ex) { + LOG.error("Save file error in container: " + container, ex); + } + } + return relativePath.replace("\\", "/"); + } + + /** + * 保存文件 + * + * @param inputStream + * @return + */ + public String saveFile(InputStream inputStream, String toRelativePath) { + //优先从 默认的 container 去保存文件 + String relativePath = defaultContainer.saveFile(inputStream, toRelativePath); + File defaultContainerFile = defaultContainer.getFile(relativePath); + + for (AttachmentContainer container : containers) { + try { + if (container != defaultContainer) { + container.saveFile(defaultContainerFile, toRelativePath); + } + } catch (Exception ex) { + LOG.error("Save file error in container: " + container, ex); + } + } + return relativePath.replace("\\", "/"); + } + + + /** + * 删除文件 + * + * @param relativePath + * @return + */ + public boolean deleteFile(String relativePath) { + for (AttachmentContainer container : containers) { + try { + container.deleteFile(relativePath); + } catch (Exception ex) { + LOG.error("Delete file error in container: " + container, ex); + } + } + return defaultContainer.deleteFile(relativePath); + } + + /** + * 通过相对路径获取文件 + * + * @param relativePath + * @return + */ + public File getFile(String relativePath) { + //优先从 默认的 container 去获取 + return getFile(relativePath, true); + } + + + /** + * 通过相对路径获取文件 + * + * @param relativePath + * @param localFirst + * @return + */ + public File getFile(String relativePath, boolean localFirst) { + //优先从 默认的 container 去获取 + if (localFirst) { + File localFile = defaultContainer.getFile(relativePath); + if (localFile.exists()) { + return localFile; + } + } + + for (AttachmentContainer container : containers) { + if (container != defaultContainer) { + try { + File file = container.getFile(relativePath); + if (file != null && file.exists()) { + return file; + } + } catch (Exception ex) { + LOG.error("Get file error in container: " + container, ex); + } + } + } + + //文件不存在,也返回该文件 + return defaultContainer.getFile(relativePath); + } + + /** + * 通过一个文件,获取其相对路径 + * + * @param file + * @return + */ + public String getRelativePath(File file) { + String relativePath = defaultContainer.getRelativePath(file); + return relativePath != null ? relativePath.replace("\\", "/") : null; + } + + + /** + * 创建一个新的文件 + * 使用创建一般是创建一个空的文件,然后由外部逻辑进行写入 + * + * @param suffix + * @return + */ + public File createNewFile(String suffix) { + return getDefaultContainer().creatNewFile(suffix); + } + + + /** + * 渲染文件到浏览器 + * + * @param target + * @param request + * @param response + * @return true 渲染成功,false 不进行渲染 + */ + public boolean renderFile(String target, HttpServletRequest request, HttpServletResponse response) { + if (target.startsWith(defaultContainer.getTargetPrefix()) + && target.lastIndexOf('.') != -1) { + Render render; + if (target.contains("..")) { + render = renderFactory.getErrorRender(404); + } else { + File file = getFile(target); + render = getFileRender(file); + } + render.setContext(request, response).render(); + return true; + } else { + return false; + } + } + + + protected Render getFileRender(File file) { + return file == null || !file.isFile() + ? renderFactory.getErrorRender(404) + : renderFactory.getFileRender(file); + } + + +} diff --git a/src/main/java/io/jboot/web/attachment/LocalAttachmentContainer.java b/src/main/java/io/jboot/web/attachment/LocalAttachmentContainer.java new file mode 100644 index 0000000000000000000000000000000000000000..b2ab13867bf45c60cd90553003c9e7b27504a9f6 --- /dev/null +++ b/src/main/java/io/jboot/web/attachment/LocalAttachmentContainer.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.attachment; + +import com.jfinal.ext.kit.DateKit; +import com.jfinal.kit.LogKit; +import io.jboot.utils.FileUtil; +import io.jboot.utils.StrUtil; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class LocalAttachmentContainer implements AttachmentContainer { + + private String rootPath; + private String targetPrefix; + + public LocalAttachmentContainer() { + LocalAttachmentContainerConfig config = LocalAttachmentContainerConfig.getInstance(); + this.rootPath = config.getRootPath(); + this.targetPrefix = config.getTargetPrefix(); + } + + + /** + * @param rootPath + * @param targetPrefix 不能以 / 开头 + */ + public LocalAttachmentContainer(String rootPath, String targetPrefix) { + this.rootPath = rootPath; + this.targetPrefix = targetPrefix; + } + + + @Override + public String saveFile(File file) { + File newfile = creatNewFile(FileUtil.getSuffix(file.getName())); + + if (!newfile.getParentFile().exists()) { + newfile.getParentFile().mkdirs(); + } + + try { + org.apache.commons.io.FileUtils.moveFile(file, newfile); + newfile.setReadable(true, false); + } catch (IOException e) { + LogKit.error(e.toString(), e); + } + + String attachmentRoot = getRootPath(); + return FileUtil.removePrefix(newfile.getAbsolutePath(), attachmentRoot); + } + + + @Override + public String saveFile(File file, String toRelativePath) { + File toFile = new File(getRootPath(), toRelativePath); + try { + + //相同的文件,不需要做任何处理 + if (toFile.equals(file)) { + return toRelativePath; + } + + if (!toFile.getParentFile().exists()) { + toFile.getParentFile().mkdirs(); + } + + org.apache.commons.io.FileUtils.moveFile(file, toFile); + toFile.setReadable(true, false); + + } catch (IOException e) { + LogKit.error(e.toString(), e); + } + + return toRelativePath; + + + } + + @Override + public String saveFile(InputStream inputStream, String toRelativePath) { + + File toFile = new File(getRootPath(), toRelativePath); + + if (toFile.exists()) { + toFile.delete(); + } + + if (!toFile.getParentFile().exists()) { + toFile.getParentFile().mkdirs(); + } + + FileOutputStream fOutStream = null; + try { + fOutStream = new FileOutputStream(toFile); + byte[] buffer = new byte[1024]; + int len; + while ((len = inputStream.read(buffer)) > -1) { + fOutStream.write(buffer, 0, len); + } + } catch (IOException e) { + LogKit.error(e.toString(), e); + } finally { + FileUtil.close(fOutStream, inputStream); + } + return toRelativePath; + } + + + @Override + public boolean deleteFile(String relativePath) { + File file = getFile(relativePath); + return file.exists() && file.delete(); + } + + + public File creatNewFile(String suffix) { + String rootPath = getRootPath(); + + StringBuilder newFileName = new StringBuilder(rootPath).append(targetPrefix) + .append(File.separator).append(DateKit.toStr(new Date(), "yyyyMMdd")) + .append(File.separator).append(StrUtil.uuid()) + .append(suffix); + + return new File(newFileName.toString()); + } + + + @Override + public File getFile(String relativePath) { + return new File(getRootPath(), relativePath); + } + + + @Override + public String getRelativePath(File file) { + String rootPath = getRootPath(); + String filePath = file.getAbsolutePath(); + return filePath.startsWith(rootPath) + ? filePath.substring(rootPath.length()) + : filePath; + } + + + public String getRootPath() { + return rootPath; + } + + public void setRootPath(String rootPath) { + this.rootPath = rootPath; + } + + public String getTargetPrefix() { + return targetPrefix; + } + + public void setTargetPrefix(String targetPrefix) { + this.targetPrefix = targetPrefix; + } + + +} diff --git a/src/main/java/io/jboot/web/attachment/LocalAttachmentContainerConfig.java b/src/main/java/io/jboot/web/attachment/LocalAttachmentContainerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..578a063637eea953c121a82f7884e07c4a48c048 --- /dev/null +++ b/src/main/java/io/jboot/web/attachment/LocalAttachmentContainerConfig.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.attachment; + +import com.jfinal.kit.PathKit; +import io.jboot.Jboot; +import io.jboot.app.config.annotation.ConfigModel; + +import java.io.File; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +@ConfigModel(prefix = "jboot.attachment") +public class LocalAttachmentContainerConfig { + + private String rootPath = PathKit.getWebRootPath(); + private String targetPrefix = "/attachment"; + + public String getRootPath() { + return rootPath; + } + + public void setRootPath(String rootPath) { + this.rootPath = rootPath; + } + + public String getTargetPrefix() { + return targetPrefix; + } + + public void setTargetPrefix(String targetPrefix) { + this.targetPrefix = targetPrefix; + } + + public static LocalAttachmentContainerConfig getInstance() { + return Jboot.config(LocalAttachmentContainerConfig.class); + } + + public String buildUploadAbsolutePath() { + return new File(rootPath, targetPrefix).getAbsolutePath(); + } +} diff --git a/src/main/java/io/jboot/web/controller/GetMappingInterceptor.java b/src/main/java/io/jboot/web/controller/GetMappingInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..927d74ff81c7683ed357633d6677ef5b8d6fbb3f --- /dev/null +++ b/src/main/java/io/jboot/web/controller/GetMappingInterceptor.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.core.Controller; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.web.controller.annotation.*; + +import java.lang.reflect.Method; + +@AutoLoad +public class GetMappingInterceptor implements Interceptor, InterceptorBuilder { + + + private static final String GET = "GET"; + + @Override + public void intercept(Invocation inv) { + Controller controller = inv.getController(); + if (GET.equalsIgnoreCase(controller.getRequest().getMethod())) { + inv.invoke(); + } else { + controller.renderError(405); + } + } + + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.isController(targetClass) + && Util.hasAnnotation(targetClass, GetMapping.class) + && !Util.hasAnyAnnotation(method, GetRequest.class, PostRequest.class, PutRequest.class, DeleteRequest.class, PatchRequest.class)) { + interceptors.addIfNotExist(this); + } + } +} diff --git a/src/main/java/io/jboot/web/controller/JbootController.java b/src/main/java/io/jboot/web/controller/JbootController.java index 988e089954e860986012c6dc307600e290c3451f..770bd14fcc15fb84fb74475f743330c35656a5ac 100644 --- a/src/main/java/io/jboot/web/controller/JbootController.java +++ b/src/main/java/io/jboot/web/controller/JbootController.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,42 @@ package io.jboot.web.controller; import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Sets; +import com.jfinal.core.ActionException; import com.jfinal.core.Controller; import com.jfinal.core.NotAction; +import com.jfinal.kit.StrKit; +import com.jfinal.render.RenderManager; +import com.jfinal.upload.UploadFile; import io.jboot.support.jwt.JwtManager; +import io.jboot.utils.FileUtil; import io.jboot.utils.RequestUtil; import io.jboot.utils.StrUtil; +import io.jboot.utils.TypeDef; +import io.jboot.web.json.JsonBodyParseInterceptor; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; public class JbootController extends Controller { + private Object rawObject; + private Map jwtParas; + private Map jwtAttrs; + + + @Override + protected void _clear_() { + super._clear_(); + this.rawObject = null; + this.jwtParas = null; + this.jwtAttrs = null; + } + /** * 是否是手机浏览器 * @@ -113,15 +135,13 @@ public class JbootController extends Controller { } - private HashMap jwtMap; - @NotAction public Controller setJwtAttr(String name, Object value) { - if (jwtMap == null) { - jwtMap = new HashMap<>(); + if (jwtAttrs == null) { + jwtAttrs = new HashMap<>(); } - jwtMap.put(name, value); + jwtAttrs.put(name, value); return this; } @@ -129,44 +149,127 @@ public class JbootController extends Controller { @NotAction public Controller setJwtMap(Map map) { if (map == null) { - throw new NullPointerException("map is null"); + throw new NullPointerException("Jwt map is null"); } - if (jwtMap == null) { - jwtMap = new HashMap<>(); + if (jwtAttrs == null) { + jwtAttrs = new HashMap<>(); } - jwtMap.putAll(map); + jwtAttrs.putAll(map); + return this; + } + + + @NotAction + public Controller setJwtEmpty() { + jwtAttrs = new HashMap<>(); return this; } @NotAction public T getJwtAttr(String name) { - return jwtMap == null ? null : (T) jwtMap.get(name); + return jwtAttrs == null ? null : (T) jwtAttrs.get(name); } @NotAction - public HashMap getJwtAttrs() { - return jwtMap; + public Map getJwtAttrs() { + return jwtAttrs; + } + + + @NotAction + public T getJwtPara(String name, Object defaultValue) { + T ret = getJwtPara(name); + return ret != null ? ret : (T) defaultValue; } @NotAction public T getJwtPara(String name) { - return JwtManager.me().getPara(name); + return (T) getJwtParas().get(name); + } + + + @NotAction + public Integer getJwtParaToInt(String name, Integer defaultValue) { + Integer ret = getJwtParaToInt(name); + return ret != null ? ret : defaultValue; } + @NotAction + public Integer getJwtParaToInt(String name) { + Object ret = getJwtParas().get(name); + if (ret instanceof Number) { + return ((Number) ret).intValue(); + } + return ret != null ? Integer.valueOf(ret.toString()) : null; + } + + @NotAction + public Long getJwtParaToLong(String name, Long defaultValue) { + Long ret = getJwtParaToLong(name); + return ret != null ? ret : defaultValue; + } + + + @NotAction + public Long getJwtParaToLong(String name) { + Object ret = getJwtParas().get(name); + if (ret instanceof Number) { + return ((Number) ret).longValue(); + } + return ret != null ? Long.valueOf(ret.toString()) : null; + } + + + @NotAction + public String getJwtParaToString(String name, String defaultValue) { + String ret = getJwtParaToString(name); + return StrUtil.isNotBlank(ret) ? ret : defaultValue; + } + + + @NotAction + public String getJwtParaToString(String name) { + Object ret = getJwtParas().get(name); + return ret != null ? ret.toString() : null; + } + + + @NotAction + public BigInteger getJwtParaToBigInteger(String name, BigInteger defaultValue) { + BigInteger ret = getJwtParaToBigInteger(name); + return ret != null ? ret : defaultValue; + } + + @NotAction + public BigInteger getJwtParaToBigInteger(String name) { + Object ret = getJwtParas().get(name); + if (ret instanceof BigInteger) { + return (BigInteger) ret; + } else if (ret instanceof Number) { + return BigInteger.valueOf(((Number) ret).longValue()); + } + return ret != null ? toBigInteger(ret.toString(), null) : null; + } + + @NotAction public Map getJwtParas() { - return JwtManager.me().getParas(); + if (jwtParas == null) { + jwtParas = JwtManager.me().parseJwtToken(this); + } + return jwtParas; } + @NotAction public String createJwtToken() { - if (jwtMap == null) { - throw new NullPointerException("jwt attrs is null"); + if (jwtAttrs == null) { + jwtAttrs = new HashMap<>(); } - return JwtManager.me().createJwtToken(jwtMap); + return JwtManager.me().createJwtToken(jwtAttrs); } /** @@ -181,42 +284,187 @@ public class JbootController extends Controller { @NotAction - public String getCurrentUrl(){ + public String getCurrentUrl() { return RequestUtil.getCurrentUrl(getRequest()); } + + /** + * 接收 Json 转化为 JsonObject 或者 JsonArray + * + * @return + */ + @NotAction + public T getRawObject() { + if (rawObject == null && StrUtil.isNotBlank(getRawData())) { + rawObject = JSON.parse(getRawData()); + } + return (T) rawObject; + } + + /** * 接收 json 转化为 object * - * @param tClass + * @param typeClass * @param * @return */ @NotAction - public T getRawObject(Class tClass) { - return StrUtil.isBlank(getRawData()) ? null : JSON.parseObject(getRawData(), tClass); + public T getRawObject(Class typeClass) { + return getRawObject(typeClass, null); } /** - * 接收 Json 转化为 JSONObject + * 接收 json 转化为 object * + * @param typeClass + * @param jsonKey + * @param * @return */ @NotAction - public JSONObject getRawObject() { - return StrUtil.isBlank(getRawData()) ? null : JSON.parseObject(getRawData()); + public T getRawObject(Class typeClass, String jsonKey) { + try { + return (T) JsonBodyParseInterceptor.parseJsonBody(getRawObject(), typeClass, typeClass, jsonKey); + } catch (Exception ex) { + throw new ActionException(400, RenderManager.me().getRenderFactory().getErrorRender(400), ex.getMessage()); + } } - @Override + /** + * 接收 json 转化为 object + * + * @param typeDef 泛型的定义类 + * @param + * @return + */ @NotAction - public String getPara(String name) { - String value = super.getPara(name); - return "".equals(value) ? null : value; + public T getRawObject(TypeDef typeDef) { + return getRawObject(typeDef, null); + } + + + /** + * 接收 json 转化为 object + * + * @param typeDef 泛型的定义类 + * @param jsonKey + * @param + * @return + */ + @NotAction + public T getRawObject(TypeDef typeDef, String jsonKey) { + try { + return (T) JsonBodyParseInterceptor.parseJsonBody(getRawObject(), typeDef.getDefClass(), typeDef.getType(), jsonKey); + } catch (Exception ex) { + throw new ActionException(400, RenderManager.me().getRenderFactory().getErrorRender(400), ex.getMessage()); + } + } + + + /** + * 接收 Json 转化为 JsonObject 或者 JsonArray + * + * @return + */ + @NotAction + public T getJsonBody() { + if (rawObject == null && StrUtil.isNotBlank(getRawData())) { + rawObject = JSON.parse(getRawData()); + } + return (T) rawObject; + } + + + /** + * 接收 json 转化为 object + * + * @param typeClass + * @param + * @return + */ + @NotAction + public T getJsonBody(Class typeClass) { + return getJsonBody(typeClass, null); } + /** + * 接收 json 转化为 object + * + * @param typeClass + * @param jsonKey + * @param + * @return + */ + @NotAction + public T getJsonBody(Class typeClass, String jsonKey) { + try { + return (T) JsonBodyParseInterceptor.parseJsonBody(getJsonBody(), typeClass, typeClass, jsonKey); + } catch (Exception ex) { + throw new ActionException(400, RenderManager.me().getRenderFactory().getErrorRender(400), ex.getMessage()); + } + } + + + /** + * 接收 json 转化为 object + * + * @param typeDef 泛型的定义类 + * @param + * @return + */ + @NotAction + public T getJsonBody(TypeDef typeDef) { + return getJsonBody(typeDef, null); + } + + + /** + * 接收 json 转化为 object + * + * @param typeDef 泛型的定义类 + * @param jsonKey + * @param + * @return + */ + @NotAction + public T getJsonBody(TypeDef typeDef, String jsonKey) { + try { + return (T) JsonBodyParseInterceptor.parseJsonBody(getJsonBody(), typeDef.getDefClass(), typeDef.getType(), jsonKey); + } catch (Exception ex) { + throw new ActionException(400, RenderManager.me().getRenderFactory().getErrorRender(400), ex.getMessage()); + } + } + + + /** + * BeanGetter 会调用此方法生成 bean,在 Map List Array 下,JFinal + * 通过 Injector.injectBean 去实例化的时候会出错,从而无法实现通过 @JsonBody 对 map list array 的注入 + * + * @param beanClass + * @param beanName + * @param skipConvertError + * @param + * @return + */ + @NotAction + @Override + public T getBean(Class beanClass, String beanName, boolean skipConvertError) { + if (Collection.class.isAssignableFrom(beanClass) + || Map.class.isAssignableFrom(beanClass) + || beanClass.isArray()) { + return null; + } else { + return super.getBean(beanClass, beanName, skipConvertError); + } + } + + + @NotAction public Map getParas() { Map map = null; @@ -232,26 +480,56 @@ public class JbootController extends Controller { } + @Override + @NotAction + public String getPara() { + return tryToTrim(super.getPara()); + } + + + @Override + @NotAction + public String getPara(String name) { + return tryToTrim(super.getPara(name)); + } + + + @Override + @NotAction + public String getPara(int index, String defaultValue) { + return tryToTrim(super.getPara(index, defaultValue)); + } + + + @Override + @NotAction + public String getPara(String name, String defaultValue) { + return tryToTrim(super.getPara(name, defaultValue)); + } + + @NotAction public String getTrimPara(String name) { - String value = super.getPara(name); - value = (value == null ? null : value.trim()); - return "".equals(value) ? null : value; + return getPara(name); } @NotAction public String getTrimPara(int index) { - String value = super.getPara(index); - value = (value == null ? null : value.trim()); - return "".equals(value) ? null : value; + return getPara(index); + } + + + @NotAction + private String tryToTrim(String value){ + return value != null ? value.trim() : null; } @NotAction public String getEscapePara(String name) { String value = getTrimPara(name); - if (value == null || "".equals(value)) { + if (value == null || value.length() == 0) { return null; } return StrUtil.escapeHtml(value); @@ -261,10 +539,409 @@ public class JbootController extends Controller { @NotAction public String getEscapePara(String name, String defaultValue) { String value = getTrimPara(name); - if (value == null || "".equals(value)) { + if (value == null || value.length() == 0) { return defaultValue; } return StrUtil.escapeHtml(value); } + + @NotAction + public String getUnescapePara(String name) { + String value = getTrimPara(name); + if (value == null || value.length() == 0) { + return null; + } + return StrUtil.unEscapeHtml(value); + } + + + @NotAction + public String getUnescapePara(String name, String defaultValue) { + String value = getTrimPara(name); + if (value == null || value.length() == 0) { + return defaultValue; + } + return StrUtil.unEscapeHtml(value); + } + + + @NotAction + public String getOriginalPara(String name) { + String value = getOrginalRequest().getParameter(name); + if (value == null || value.length() == 0) { + return null; + } + return value; + } + + + @NotAction + public HttpServletRequest getOrginalRequest() { + HttpServletRequest req = getRequest(); + if (req instanceof HttpServletRequestWrapper) { + req = getOrginalRequest((HttpServletRequestWrapper) req); + } + return req; + } + + + private HttpServletRequest getOrginalRequest(HttpServletRequestWrapper wrapper) { + HttpServletRequest req = (HttpServletRequest) wrapper.getRequest(); + if (req instanceof HttpServletRequestWrapper) { + return getOrginalRequest((HttpServletRequestWrapper) req); + } + return req; + } + + + @NotAction + public String getOriginalPara(String name, String defaultValue) { + String value = getOriginalPara(name); + return value != null ? value : defaultValue; + } + + + private BigInteger toBigInteger(String value, BigInteger defaultValue) { + try { + if (StrKit.isBlank(value)) { + return defaultValue; + } + value = value.trim(); + if (value.startsWith("N") || value.startsWith("n")) { + return BigInteger.ZERO.subtract(new BigInteger(value.substring(1))); + } + return new BigInteger(value); + } catch (Exception e) { + throw new ActionException(400, RenderManager.me().getRenderFactory().getErrorRender(400), "Can not parse the parameter \"" + value + "\" to BigInteger value."); + } + } + + /** + * Returns the value of a request parameter and convert to BigInteger. + * + * @return a BigInteger representing the single value of the parameter + */ + @NotAction + public BigInteger getParaToBigInteger() { + return toBigInteger(getPara(), null); + } + + /** + * Returns the value of a request parameter and convert to BigInteger. + * + * @param name a String specifying the name of the parameter + * @return a BigInteger representing the single value of the parameter + */ + @NotAction + public BigInteger getParaToBigInteger(String name) { + return toBigInteger(getTrimPara(name), null); + } + + /** + * Returns the value of a request parameter and convert to BigInteger with a default value if it is null. + * + * @param name a String specifying the name of the parameter + * @return a BigInteger representing the single value of the parameter + */ + @NotAction + public BigInteger getParaToBigInteger(String name, BigInteger defaultValue) { + return toBigInteger(getTrimPara(name), defaultValue); + } + + + @NotAction + public BigInteger getParaToBigInteger(int index) { + return toBigInteger(getTrimPara(index), null); + } + + + @NotAction + public BigInteger getParaToBigInteger(int index, BigInteger defaultValue) { + return toBigInteger(getTrimPara(index), defaultValue); + } + + + /** + * Returns the value of a request parameter and convert to BigInteger. + * + * @return a BigInteger representing the single value of the parameter + */ + @NotAction + public BigInteger getBigInteger() { + return toBigInteger(getPara(), null); + } + + /** + * Returns the value of a request parameter and convert to BigInteger. + * + * @param name a String specifying the name of the parameter + * @return a BigInteger representing the single value of the parameter + */ + @NotAction + public BigInteger getBigInteger(String name) { + return toBigInteger(getTrimPara(name), null); + } + + /** + * Returns the value of a request parameter and convert to BigInteger with a default value if it is null. + * + * @param name a String specifying the name of the parameter + * @return a BigInteger representing the single value of the parameter + */ + @NotAction + public BigInteger getBigInteger(String name, BigInteger defaultValue) { + return toBigInteger(getTrimPara(name), defaultValue); + } + + + @NotAction + public BigInteger getBigInteger(int index) { + return toBigInteger(getTrimPara(index), null); + } + + + @NotAction + public BigInteger getBigInteger(int index, BigInteger defaultValue) { + return toBigInteger(getTrimPara(index), defaultValue); + } + + + private BigDecimal toBigDecimal(String value, BigDecimal defaultValue) { + try { + if (StrKit.isBlank(value)) { + return defaultValue; + } + value = value.trim(); + if (value.startsWith("N") || value.startsWith("n")) { + return BigDecimal.ZERO.subtract(new BigDecimal(value.substring(1))); + } + return new BigDecimal(value); + } catch (Exception e) { + throw new ActionException(400, RenderManager.me().getRenderFactory().getErrorRender(400), "Can not parse the parameter \"" + value + "\" to BigDecimal value."); + } + } + + + /** + * Returns the value of a request parameter and convert to BigDecimal. + * + * @return a BigDecimal representing the single value of the parameter + */ + @NotAction + public BigDecimal getParaToBigDecimal() { + return toBigDecimal(getPara(), null); + } + + + /** + * Returns the value of a request parameter and convert to BigDecimal. + * + * @param name a String specifying the name of the parameter + * @return a BigDecimal representing the single value of the parameter + */ + @NotAction + public BigDecimal getParaToBigDecimal(String name) { + return toBigDecimal(getTrimPara(name), null); + } + + /** + * Returns the value of a request parameter and convert to BigDecimal with a default value if it is null. + * + * @param name a String specifying the name of the parameter + * @return a BigDecimal representing the single value of the parameter + */ + @NotAction + public BigDecimal getParaToBigDecimal(String name, BigDecimal defaultValue) { + return toBigDecimal(getTrimPara(name), defaultValue); + } + + + /** + * Returns the value of a request parameter and convert to BigDecimal. + * + * @return a BigDecimal representing the single value of the parameter + */ + @NotAction + public BigDecimal getBigDecimal() { + return toBigDecimal(getPara(), null); + } + + /** + * Returns the value of a request parameter and convert to BigDecimal. + * + * @param name a String specifying the name of the parameter + * @return a BigDecimal representing the single value of the parameter + */ + @NotAction + public BigDecimal getBigDecimal(String name) { + return toBigDecimal(getTrimPara(name), null); + } + + /** + * Returns the value of a request parameter and convert to BigDecimal with a default value if it is null. + * + * @param name a String specifying the name of the parameter + * @return a BigDecimal representing the single value of the parameter + */ + @NotAction + public BigDecimal getBigDecimal(String name, BigDecimal defaultValue) { + return toBigDecimal(getTrimPara(name), defaultValue); + } + + + /** + * 获取所有 attr 信息 + * + * @return attrs map + */ + @NotAction + public Map getAttrs() { + Map attrs = new HashMap<>(); + for (Enumeration names = getAttrNames(); names.hasMoreElements(); ) { + String attrName = names.nextElement(); + attrs.put(attrName, getAttr(attrName)); + } + return attrs; + } + + + /** + * 使用 getFirstFileOnly,否则恶意上传的安全问题 + * + * @return + */ + @NotAction + @Override + public UploadFile getFile() { + return getFirstFileOnly(); + } + + + /** + * 只获取第一个文件,若上传多个文件,则删除其他文件 + * + * @return + */ + @NotAction + public UploadFile getFirstFileOnly() { + List uploadFiles = getFiles(); + if (uploadFiles == null || uploadFiles.isEmpty()) { + return null; + } + + if (uploadFiles.size() == 1) { + return uploadFiles.get(0); + } + + for (int i = 1; i < uploadFiles.size(); i++) { + UploadFile uploadFile = uploadFiles.get(i); + FileUtil.delete(uploadFile); + } + + return uploadFiles.get(0); + } + + + /** + * 值返回特定文件,其他文件则删除 + * + * @param name + * @return + */ + @NotAction + public UploadFile getFileOnly(String name) { + List uploadFiles = getFiles(); + if (uploadFiles == null || uploadFiles.isEmpty()) { + return null; + } + + UploadFile ret = null; + for (UploadFile uploadFile : uploadFiles) { + if (ret == null && name.equals(uploadFile.getParameterName())) { + ret = uploadFile; + } else { + FileUtil.delete(uploadFile); + } + } + + return ret; + } + + + /** + * 只返回特定的名称的文件,其他文件则删除 + * + * @param paraNames + * @return + */ + @NotAction + public Map getFilesOnly(String... paraNames) { + if (paraNames == null || paraNames.length == 0) { + throw new IllegalArgumentException("names can not be null or empty."); + } + return getFilesOnly(Sets.newHashSet(paraNames)); + } + + + /** + * 只返回特定的名称的文件,其他文件则删除 + * @param paraNames + * @return + */ + @NotAction + public Map getFilesOnly(Set paraNames) { + if (paraNames == null || paraNames.size() == 0) { + throw new IllegalArgumentException("names can not be null or empty."); + } + + List allUploadFiles = getFiles(); + if (allUploadFiles == null || allUploadFiles.isEmpty()) { + return null; + } + + Map filesMap = new HashMap<>(); + + for (UploadFile uploadFile : allUploadFiles) { + String parameterName = uploadFile.getParameterName(); + if (StrUtil.isBlank(parameterName)) { + FileUtil.delete(uploadFile); + continue; + } + + parameterName = parameterName.trim(); + + if (paraNames.contains(parameterName) && !filesMap.containsKey(parameterName)) { + filesMap.put(parameterName, uploadFile); + } else { + FileUtil.delete(uploadFile); + } + } + + return filesMap; + } + + + @NotAction + public String renderToStringWithAttrs(String template) { + return super.renderToString(template, getAttrs()); + } + + + @NotAction + public String renderToStringWithAttrs(String template, Map data) { + if (data == null) { + data = getAttrs(); + } else { + data = new HashMap(data); + for (Enumeration names = getAttrNames(); names.hasMoreElements(); ) { + String attrName = names.nextElement(); + if (!data.containsKey(attrName)) { + data.put(attrName, getAttr(attrName)); + } + } + } + + return super.renderToString(template, data); + } } diff --git a/src/main/java/io/jboot/web/controller/JbootControllerContext.java b/src/main/java/io/jboot/web/controller/JbootControllerContext.java index 85802fdd2b5ef925cf19ff18d866aeb81438ebfa..d37dc8415b0e7726339a5c839844265488f9d533 100644 --- a/src/main/java/io/jboot/web/controller/JbootControllerContext.java +++ b/src/main/java/io/jboot/web/controller/JbootControllerContext.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.jfinal.core.Controller; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web */ public class JbootControllerContext { diff --git a/src/main/java/io/jboot/web/controller/JbootControllerManager.java b/src/main/java/io/jboot/web/controller/JbootControllerManager.java index 4a9c363bd83e2284946a62dfa956807a49d0e853..244c7388ac6a1c699ca70fadf1e402b9c00dbab7 100644 --- a/src/main/java/io/jboot/web/controller/JbootControllerManager.java +++ b/src/main/java/io/jboot/web/controller/JbootControllerManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import com.jfinal.core.ControllerFactory; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web */ public class JbootControllerManager extends ControllerFactory { @@ -36,11 +35,6 @@ public class JbootControllerManager extends ControllerFactory { private JbootControllerManager() { } -// public Controller getController(Class controllerClass) throws InstantiationException, IllegalAccessException { -// Controller controller = controllerClass.newInstance(); -// return Aop.inject(controller); -// } - private BiMap> controllerMapping = HashBiMap.create(); diff --git a/src/main/java/io/jboot/web/controller/PostMappingInterceptor.java b/src/main/java/io/jboot/web/controller/PostMappingInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..6064e61e37b191bca02a69ac96a4d1b05f539082 --- /dev/null +++ b/src/main/java/io/jboot/web/controller/PostMappingInterceptor.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.core.Controller; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.web.controller.annotation.*; + +import java.lang.reflect.Method; + +@AutoLoad +public class PostMappingInterceptor implements Interceptor, InterceptorBuilder { + + private static final String POST = "POST"; + + @Override + public void intercept(Invocation inv) { + Controller controller = inv.getController(); + if (POST.equalsIgnoreCase(controller.getRequest().getMethod())) { + inv.invoke(); + } else { + controller.renderError(405); + } + } + + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.isController(targetClass) + && Util.hasAnnotation(targetClass, PostMapping.class) + && !Util.hasAnyAnnotation(method, GetRequest.class, PostRequest.class, PutRequest.class, DeleteRequest.class, PatchRequest.class)) { + interceptors.addIfNotExist(this); + } + } +} diff --git a/src/main/java/io/jboot/web/controller/RequestMethodLimitInterceptor.java b/src/main/java/io/jboot/web/controller/RequestMethodLimitInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..369257d8e08614437af04dc82493a960f0f39e75 --- /dev/null +++ b/src/main/java/io/jboot/web/controller/RequestMethodLimitInterceptor.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.core.Controller; + +import java.util.Set; + +public class RequestMethodLimitInterceptor implements Interceptor { + + + private final Set supportMethods; + + public RequestMethodLimitInterceptor(Set supportMethods) { + this.supportMethods = supportMethods; + } + + @Override + public void intercept(Invocation inv) { + Controller controller = inv.getController(); + if (supportMethods.contains(controller.getRequest().getMethod().toUpperCase())) { + inv.invoke(); + } else { + controller.renderError(405); + } + } + + +} diff --git a/src/main/java/io/jboot/web/controller/RequestMethodLimitInterceptorBuilder.java b/src/main/java/io/jboot/web/controller/RequestMethodLimitInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..39335c61dffb2294741295ea61de9b6b4d38d098 --- /dev/null +++ b/src/main/java/io/jboot/web/controller/RequestMethodLimitInterceptorBuilder.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller; + +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.web.controller.annotation.*; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +@AutoLoad +public class RequestMethodLimitInterceptorBuilder implements InterceptorBuilder { + + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.isController(targetClass)) { + + Set supportMethods = new HashSet<>(); + + if (Util.hasAnnotation(method, GetRequest.class)) { + supportMethods.add("GET"); + } + if (Util.hasAnnotation(method, PostRequest.class)) { + supportMethods.add("POST"); + } + if (Util.hasAnnotation(method, PutRequest.class)) { + supportMethods.add("PUT"); + } + if (Util.hasAnnotation(method, DeleteRequest.class)) { + supportMethods.add("DELETE"); + } + if (Util.hasAnnotation(method, PatchRequest.class)) { + supportMethods.add("PATCH"); + } + + if (!supportMethods.isEmpty()) { + interceptors.add(new RequestMethodLimitInterceptor(supportMethods)); + } + } + } +} diff --git a/src/main/java/io/jboot/web/controller/annotation/DeleteRequest.java b/src/main/java/io/jboot/web/controller/annotation/DeleteRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..9b2f0e3837a4aed47211b1ae94918f31722aacd4 --- /dev/null +++ b/src/main/java/io/jboot/web/controller/annotation/DeleteRequest.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface DeleteRequest { + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/controller/annotation/GetMapping.java b/src/main/java/io/jboot/web/controller/annotation/GetMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..2b31c15f3f8f58201ba376dcad5b5d9999737d3b --- /dev/null +++ b/src/main/java/io/jboot/web/controller/annotation/GetMapping.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface GetMapping { + + /** + * 由于空字符串可以被用于配置 viewPath,所以使用如下字符串表示默认值 + */ + String NULL_VIEW_PATH = "*"; + + String value(); + + String viewPath() default NULL_VIEW_PATH; +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/controller/annotation/GetRequest.java b/src/main/java/io/jboot/web/controller/annotation/GetRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..546d00f1bd78fc3564a771bff2b16cb5f2d83c2f --- /dev/null +++ b/src/main/java/io/jboot/web/controller/annotation/GetRequest.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface GetRequest { + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/controller/annotation/PatchRequest.java b/src/main/java/io/jboot/web/controller/annotation/PatchRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..0f86b983444d67313ee2f9aff966c8eaed6804d8 --- /dev/null +++ b/src/main/java/io/jboot/web/controller/annotation/PatchRequest.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface PatchRequest { + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/controller/annotation/PostMapping.java b/src/main/java/io/jboot/web/controller/annotation/PostMapping.java new file mode 100644 index 0000000000000000000000000000000000000000..77398eccedc9a14fae75d4f636160cfcb64cab48 --- /dev/null +++ b/src/main/java/io/jboot/web/controller/annotation/PostMapping.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface PostMapping { + + /** + * 由于空字符串可以被用于配置 viewPath,所以使用如下字符串表示默认值 + */ + String NULL_VIEW_PATH = "*"; + + String value(); + + String viewPath() default NULL_VIEW_PATH; +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/controller/annotation/PostRequest.java b/src/main/java/io/jboot/web/controller/annotation/PostRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..4b1feca8f2223e9deaaf1cb953fa9463342d0e9e --- /dev/null +++ b/src/main/java/io/jboot/web/controller/annotation/PostRequest.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface PostRequest{ + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/controller/annotation/PutRequest.java b/src/main/java/io/jboot/web/controller/annotation/PutRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..2fe289f9a5fec32423dcc52bc3de79bda0f954c0 --- /dev/null +++ b/src/main/java/io/jboot/web/controller/annotation/PutRequest.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.controller.annotation; + +import java.lang.annotation.*; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface PutRequest { + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/controller/annotation/RequestMapping.java b/src/main/java/io/jboot/web/controller/annotation/RequestMapping.java index b3103d797db8eb24c275707b900b5ea0c5c9d593..3928a66bbf97c8491cfe86e613f448830b2ab73a 100644 --- a/src/main/java/io/jboot/web/controller/annotation/RequestMapping.java +++ b/src/main/java/io/jboot/web/controller/annotation/RequestMapping.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,13 @@ import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface RequestMapping { + + /** + * 由于空字符串可以被用于配置 viewPath,所以使用如下字符串表示默认值 + */ + String NULL_VIEW_PATH = "*"; + String value(); - String viewPath() default ""; + String viewPath() default NULL_VIEW_PATH; } \ No newline at end of file diff --git a/src/main/java/io/jboot/web/converter/ArrayConverters.java b/src/main/java/io/jboot/web/converter/ArrayConverters.java new file mode 100644 index 0000000000000000000000000000000000000000..f20d99436dfbe6fd9f52f2dd35e926a089cf0a36 --- /dev/null +++ b/src/main/java/io/jboot/web/converter/ArrayConverters.java @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.converter; + +import com.jfinal.core.converter.IConverter; +import com.jfinal.core.converter.TypeConverter; +import io.jboot.utils.StrUtil; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * 针对 数组 类型实现 IConverter 接口 + */ +public class ArrayConverters { + + private static String array_separator = ","; + + public static String getArraySeparator() { + return array_separator; + } + + public static void setArraySeparator(String arraySeparator) { + ArrayConverters.array_separator = arraySeparator; + } + + public static class IntArrayConverter implements IConverter { + @Override + public int[] convert(String s) { + String[] strings = s.split(array_separator); + int[] ret = new int[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? 0 : Integer.parseInt(str); + } + return ret; + } + } + + + public static class Int1ArrayConverter implements IConverter { + @Override + public Integer[] convert(String s) { + String[] strings = s.split(array_separator); + Integer[] ret = new Integer[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? null : Integer.parseInt(str); + } + return ret; + } + } + + + public static class LongArrayConverter implements IConverter { + @Override + public long[] convert(String s) { + String[] strings = s.split(array_separator); + long[] ret = new long[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? 0 : Long.parseLong(str); + } + return ret; + } + } + + + public static class Long1ArrayConverter implements IConverter { + @Override + public Long[] convert(String s) { + String[] strings = s.split(array_separator); + Long[] ret = new Long[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? null : Long.parseLong(str); + } + return ret; + } + } + + + public static class FloatArrayConverter implements IConverter { + @Override + public float[] convert(String s) { + String[] strings = s.split(array_separator); + float[] ret = new float[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? 0 : Float.parseFloat(str); + } + return ret; + } + } + + + public static class Float1ArrayConverter implements IConverter { + @Override + public Float[] convert(String s) { + String[] strings = s.split(array_separator); + Float[] ret = new Float[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? null : Float.parseFloat(str); + } + return ret; + } + } + + public static class DoubleArrayConverter implements IConverter { + @Override + public double[] convert(String s) { + String[] strings = s.split(array_separator); + double[] ret = new double[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? 0 : Double.parseDouble(str); + } + return ret; + } + } + + + public static class Double1ArrayConverter implements IConverter { + @Override + public Double[] convert(String s) { + String[] strings = s.split(array_separator); + Double[] ret = new Double[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? null : Double.parseDouble(str); + } + return ret; + } + } + + + public static class BigIntegerArrayConverter implements IConverter { + @Override + public BigInteger[] convert(String s) { + String[] strings = s.split(array_separator); + BigInteger[] ret = new BigInteger[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? null : new BigInteger(str); + } + return ret; + } + } + + + public static class BigDecimalArrayConverter implements IConverter { + @Override + public BigDecimal[] convert(String s) { + String[] strings = s.split(array_separator); + BigDecimal[] ret = new BigDecimal[strings.length]; + for (int i = 0; i < strings.length; i++) { + String str = strings[i]; + ret[i] = StrUtil.isBlank(str) ? null : new BigDecimal(str); + } + return ret; + } + } + + + public static void init() { + TypeConverter.me().regist(int[].class, new IntArrayConverter()); + TypeConverter.me().regist(Integer[].class, new Int1ArrayConverter()); + + TypeConverter.me().regist(long[].class, new LongArrayConverter()); + TypeConverter.me().regist(Long[].class, new Long1ArrayConverter()); + + TypeConverter.me().regist(float[].class, new FloatArrayConverter()); + TypeConverter.me().regist(Float[].class, new Float1ArrayConverter()); + + TypeConverter.me().regist(double[].class, new DoubleArrayConverter()); + TypeConverter.me().regist(Double[].class, new Double1ArrayConverter()); + + TypeConverter.me().regist(BigInteger[].class, new BigIntegerArrayConverter()); + TypeConverter.me().regist(BigDecimal[].class, new BigDecimalArrayConverter()); + } + +} diff --git a/src/main/java/io/jboot/web/converter/TypeConverterFunc.java b/src/main/java/io/jboot/web/converter/TypeConverterFunc.java new file mode 100644 index 0000000000000000000000000000000000000000..d312bc3106dd6700c5f153322d1dd5d11cbf3268 --- /dev/null +++ b/src/main/java/io/jboot/web/converter/TypeConverterFunc.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.converter; + +import com.jfinal.core.JFinal; +import com.jfinal.core.converter.IConverter; +import com.jfinal.core.converter.TypeConverter; +import com.jfinal.kit.Func; +import io.jboot.utils.StrUtil; + +import java.text.ParseException; + +public class TypeConverterFunc implements Func.F21, String, Object> { + + + @Override + public Object call(Class type, String s) { + if (s == null) { + return null; + } + + // mysql type: varchar, char, enum, set, text, tinytext, mediumtext, longtext + if (type == String.class) { +// return ("".equals(s) ? null : s); // 用户在表单域中没有输入内容时将提交过来 "", 因为没有输入,所以要转成 null. + return StrUtil.isBlank(s) ? null : s.trim(); + } + s = s.trim(); + if ("".equals(s)) { // 前面的 String跳过以后,所有的空字符串全都转成 null, 这是合理的 + return null; + } + + // 以上两种情况无需转换,直接返回, 注意, 本方法不接受null为 s 参数(经测试永远不可能传来null, 因为无输入传来的也是"") + //String.class提前处理 + + if (type.isEnum()) { + for (Enum e : ((Class>) type).getEnumConstants()) { + if (e.name().equals(s)) { + return e; + } + } + } + + // -------- + IConverter converter = TypeConverter.me().getConverterMap().get(type); + if (converter != null) { + try { + return converter.convert(s); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + if (JFinal.me().getConstants().getDevMode()) { + throw new RuntimeException("Please add code in " + TypeConverter.class + ". The type can't be converted: " + type.getName()); + } else { + throw new RuntimeException(type.getName() + " can not be converted, please use other type of attributes in your model!"); + } + } +} diff --git a/src/main/java/io/jboot/web/cors/CORSConfig.java b/src/main/java/io/jboot/web/cors/CORSConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..d07f20f7233391d927bdb9582d1598e2c11c8d16 --- /dev/null +++ b/src/main/java/io/jboot/web/cors/CORSConfig.java @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.cors; + +import com.jfinal.ext.cors.EnableCORS; +import io.jboot.utils.AnnotationUtil; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class CORSConfig { + + private static final CORSConfig DEFAULT_CONFIG = new CORSConfig(); + private static final Map cache = new ConcurrentHashMap<>(); + + private String allowOrigin = "*"; + private String allowCredentials = "true"; + private String allowHeaders = "Origin,X-Requested-With,Content-Type,Accept,Authorization,Jwt"; + private String allowMethods = "GET,PUT,POST,DELETE,PATCH,OPTIONS"; + private String exposeHeaders = ""; + private String requestHeaders = ""; + private String requestMethod = ""; + private String origin = ""; + private String maxAge = "3600"; + + public static CORSConfig getDefaultConfig() { + return DEFAULT_CONFIG; + } + + public static CORSConfig fromAnnotation(EnableCORS enableCORS) { + int identityHashCode = System.identityHashCode(enableCORS); + CORSConfig corsConfig = cache.get(identityHashCode); + if (corsConfig == null) { + corsConfig = new CORSConfig(enableCORS); + if (corsConfig.equals(DEFAULT_CONFIG)) { + corsConfig = DEFAULT_CONFIG; + } + cache.put(identityHashCode, corsConfig); + } + return corsConfig; + } + + + public CORSConfig() { + } + + public CORSConfig(String allowOrigin) { + this.allowOrigin = allowOrigin; + } + + public CORSConfig(String allowOrigin, String allowHeaders) { + this.allowOrigin = allowOrigin; + this.allowHeaders = allowHeaders; + } + + public CORSConfig(String allowOrigin + , String allowCredentials + , String allowHeaders + , String allowMethods + , String exposeHeaders + , String requestHeaders + , String requestMethod + , String origin + , String maxAge) { + this.allowOrigin = allowOrigin; + this.allowCredentials = allowCredentials; + this.allowHeaders = allowHeaders; + this.allowMethods = allowMethods; + this.exposeHeaders = exposeHeaders; + this.requestHeaders = requestHeaders; + this.requestMethod = requestMethod; + this.origin = origin; + this.maxAge = maxAge; + } + + public CORSConfig(EnableCORS enableCORS) { + this.allowOrigin = AnnotationUtil.get(enableCORS.allowOrigin()); + this.allowCredentials = AnnotationUtil.get(enableCORS.allowCredentials()); + this.allowHeaders = AnnotationUtil.get(enableCORS.allowHeaders()); + this.allowMethods = AnnotationUtil.get(enableCORS.allowMethods()); + this.exposeHeaders = AnnotationUtil.get(enableCORS.exposeHeaders()); + this.requestHeaders = AnnotationUtil.get(enableCORS.requestHeaders()); + this.requestMethod = AnnotationUtil.get(enableCORS.requestMethod()); + this.origin = AnnotationUtil.get(enableCORS.origin()); + this.maxAge = AnnotationUtil.get(enableCORS.maxAge()); + } + + + public String getAllowOrigin() { + return allowOrigin; + } + + public void setAllowOrigin(String allowOrigin) { + this.allowOrigin = allowOrigin; + } + + public String getAllowCredentials() { + return allowCredentials; + } + + public void setAllowCredentials(String allowCredentials) { + this.allowCredentials = allowCredentials; + } + + public String getAllowHeaders() { + return allowHeaders; + } + + public void setAllowHeaders(String allowHeaders) { + this.allowHeaders = allowHeaders; + } + + public String getAllowMethods() { + return allowMethods; + } + + public void setAllowMethods(String allowMethods) { + this.allowMethods = allowMethods; + } + + public String getExposeHeaders() { + return exposeHeaders; + } + + public void setExposeHeaders(String exposeHeaders) { + this.exposeHeaders = exposeHeaders; + } + + public String getRequestHeaders() { + return requestHeaders; + } + + public void setRequestHeaders(String requestHeaders) { + this.requestHeaders = requestHeaders; + } + + public String getRequestMethod() { + return requestMethod; + } + + public void setRequestMethod(String requestMethod) { + this.requestMethod = requestMethod; + } + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public String getMaxAge() { + return maxAge; + } + + public void setMaxAge(String maxAge) { + this.maxAge = maxAge; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CORSConfig that = (CORSConfig) o; + return Objects.equals(allowOrigin, that.allowOrigin) + && Objects.equals(allowCredentials, that.allowCredentials) + && Objects.equals(allowHeaders, that.allowHeaders) + && Objects.equals(allowMethods, that.allowMethods) + && Objects.equals(exposeHeaders, that.exposeHeaders) + && Objects.equals(requestHeaders, that.requestHeaders) + && Objects.equals(requestMethod, that.requestMethod) + && Objects.equals(origin, that.origin) + && Objects.equals(maxAge, that.maxAge); + } + + @Override + public int hashCode() { + return Objects.hash(allowOrigin, allowCredentials, allowHeaders, allowMethods, exposeHeaders, requestHeaders, requestMethod, origin, maxAge); + } +} diff --git a/src/main/java/io/jboot/web/cors/CORSInterceptor.java b/src/main/java/io/jboot/web/cors/CORSInterceptor.java index 76b62198d4ee2b2cf27b375947505e9329532f32..a489cf07374729225ffcb8a47dc789a97ce07528 100644 --- a/src/main/java/io/jboot/web/cors/CORSInterceptor.java +++ b/src/main/java/io/jboot/web/cors/CORSInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,82 +15,91 @@ */ package io.jboot.web.cors; +import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; -import io.jboot.utils.AnnotationUtil; +import com.jfinal.ext.cors.EnableCORS; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; import io.jboot.utils.StrUtil; -import io.jboot.web.fixedinterceptor.FixedInterceptor; import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 * @Title: CORS 处理相关 拦截器 - * @Package io.jboot.web.cors */ -public class CORSInterceptor implements FixedInterceptor { +@AutoLoad +public class CORSInterceptor implements Interceptor, InterceptorBuilder { private static final String METHOD_OPTIONS = "OPTIONS"; + @Override public void intercept(Invocation inv) { - EnableCORS enableCORS = inv.getMethod().getAnnotation(EnableCORS.class); - - if (enableCORS == null) { - enableCORS = inv.getController().getClass().getAnnotation(EnableCORS.class); - } + EnableCORS enableCORS = getAnnotation(inv); if (enableCORS == null) { inv.invoke(); return; } - doConfigCORS(inv, enableCORS); + process(inv, CORSConfig.fromAnnotation(enableCORS)); + } + + + private EnableCORS getAnnotation(Invocation inv) { + EnableCORS enableCORS = inv.getController().getClass().getAnnotation(EnableCORS.class); + return enableCORS != null ? enableCORS : inv.getMethod().getAnnotation(EnableCORS.class); + } + + + public static void process(Invocation inv, CORSConfig config) { + //配置 http 头信息 + configHeaders(inv.getController().getResponse(), config); String method = inv.getController().getRequest().getMethod(); - if (METHOD_OPTIONS.equals(method)) { + if (METHOD_OPTIONS.equalsIgnoreCase(method)) { inv.getController().renderText(""); } else { inv.invoke(); } } - private void doConfigCORS(Invocation inv, EnableCORS enableCORS) { - - HttpServletResponse response = inv.getController().getResponse(); - String allowOrigin = AnnotationUtil.get(enableCORS.allowOrigin()); - String allowCredentials = AnnotationUtil.get(enableCORS.allowCredentials()); - String allowHeaders = AnnotationUtil.get(enableCORS.allowHeaders()); - String allowMethods = AnnotationUtil.get(enableCORS.allowMethods()); - String exposeHeaders = AnnotationUtil.get(enableCORS.exposeHeaders()); - String requestHeaders = AnnotationUtil.get(enableCORS.requestHeaders()); - String requestMethod = AnnotationUtil.get(enableCORS.requestMethod()); - String origin = AnnotationUtil.get(enableCORS.origin()); - String maxAge = AnnotationUtil.get(enableCORS.maxAge()); + private static void configHeaders(HttpServletResponse response, CORSConfig config) { + response.setHeader("Access-Control-Allow-Origin", config.getAllowOrigin()); + response.setHeader("Access-Control-Allow-Methods", config.getAllowMethods()); + response.setHeader("Access-Control-Allow-Headers", config.getAllowHeaders()); + response.setHeader("Access-Control-Max-Age", config.getMaxAge()); + response.setHeader("Access-Control-Allow-Credentials", config.getAllowCredentials()); - response.setHeader("Access-Control-Allow-Origin", allowOrigin); - response.setHeader("Access-Control-Allow-Methods", allowMethods); - response.setHeader("Access-Control-Allow-Headers", allowHeaders); - response.setHeader("Access-Control-Max-Age", maxAge); - response.setHeader("Access-Control-Allow-Credentials", allowCredentials); - - if (StrUtil.isNotBlank(exposeHeaders)) { - response.setHeader("Access-Control-Expose-Headers", exposeHeaders); + if (StrUtil.isNotBlank(config.getExposeHeaders())) { + response.setHeader("Access-Control-Expose-Headers", config.getExposeHeaders()); } - if (StrUtil.isNotBlank(requestHeaders)) { - response.setHeader("Access-Control-Request-Headers", requestHeaders); + if (StrUtil.isNotBlank(config.getRequestHeaders())) { + response.setHeader("Access-Control-Request-Headers", config.getRequestHeaders()); } - if (StrUtil.isNotBlank(requestMethod)) { - response.setHeader("Access-Control-Request-Method", requestMethod); + if (StrUtil.isNotBlank(config.getRequestMethod())) { + response.setHeader("Access-Control-Request-Method", config.getRequestMethod()); } - if (StrUtil.isNotBlank(origin)) { - response.setHeader("Origin", origin); + if (StrUtil.isNotBlank(config.getOrigin())) { + response.setHeader("Origin", config.getOrigin()); } + } + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.isController(targetClass) && Util.hasAnnotation(targetClass, method, EnableCORS.class)) { + interceptors.addToFirstIfNotExist(CORSInterceptor.class); + } } + } diff --git a/src/main/java/io/jboot/web/directive/JbootOutputDirectiveFactory.java b/src/main/java/io/jboot/web/directive/JbootOutputDirectiveFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..4b3eeb54aff0a3c686529a0f39013cbae0b90a3e --- /dev/null +++ b/src/main/java/io/jboot/web/directive/JbootOutputDirectiveFactory.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.directive; + +import com.jfinal.kit.LogKit; +import com.jfinal.template.Env; +import com.jfinal.template.expr.ast.ExprList; +import com.jfinal.template.io.Writer; +import com.jfinal.template.stat.Location; +import com.jfinal.template.stat.OutputDirectiveFactory; +import com.jfinal.template.stat.Scope; +import com.jfinal.template.stat.ast.Output; +import io.jboot.Jboot; + +/** + * 主要作用:在生产环境下,忽略模板引擎的错误输出。 + */ +public class JbootOutputDirectiveFactory extends OutputDirectiveFactory { + + public static final JbootOutputDirectiveFactory me = new JbootOutputDirectiveFactory(); + + private boolean ignoreTemplateException = !Jboot.isDevMode(); + + public boolean isIgnoreTemplateException() { + return ignoreTemplateException; + } + + public void setIgnoreTemplateException(boolean ignoreTemplateException) { + this.ignoreTemplateException = ignoreTemplateException; + } + + @Override + public Output getOutputDirective(ExprList exprList, Location location) { + return new JbootOutput(exprList, location); + } + + + public static class JbootOutput extends Output { + + public JbootOutput(ExprList exprList, Location location) { + super(exprList, location); + } + + @Override + public void exec(Env env, Scope scope, Writer writer) { + try { + super.exec(env, scope, writer); + } catch (Exception e) { + if (me.ignoreTemplateException) { + LogKit.error(e.toString(), e); + } else { + throw e; + } + } + } + } +} diff --git a/src/main/java/io/jboot/web/directive/JbootPaginateDirective.java b/src/main/java/io/jboot/web/directive/JbootPaginateDirective.java index ec637d9b939f5c495ebf1b487139ef2e45125229..cdc1e48d22a00f733251f50c703fd486f27695ce 100644 --- a/src/main/java/io/jboot/web/directive/JbootPaginateDirective.java +++ b/src/main/java/io/jboot/web/directive/JbootPaginateDirective.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package io.jboot.web.directive; +import com.jfinal.core.Controller; import com.jfinal.plugin.activerecord.Page; import com.jfinal.template.Env; import com.jfinal.template.io.Writer; @@ -24,6 +25,8 @@ import io.jboot.web.controller.JbootControllerContext; import io.jboot.web.directive.base.PaginateDirectiveBase; import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; +import java.util.Map; public class JbootPaginateDirective extends PaginateDirectiveBase { @@ -94,8 +97,37 @@ public class JbootPaginateDirective extends PaginateDirectiveBase { @Override protected Page getPage(Env env, Scope scope, Writer writer) { + Controller controller = JbootControllerContext.get(); + if (controller == null) { + return null; + } + String pageAttr = getPara(KEY_PAGE_ATTR, scope, DEFAULT_PAGE_ATTR); - return JbootControllerContext.get().getAttr(pageAttr); + Object page = controller.getAttr(pageAttr); + if (page instanceof Page) { + return (Page) page; + } + + Enumeration attrNames = controller.getAttrNames(); + if (attrNames != null) { + while (attrNames.hasMoreElements()) { + Object attrValue = controller.getAttr(attrNames.nextElement()); + if (attrValue instanceof Page) { + return (Page) attrValue; + } + } + } + + Map rootData = scope.getRootData(); + if (rootData != null){ + for (Object data : rootData.values()) { + if (data instanceof Page){ + return (Page) data; + } + } + } + + return null; } } \ No newline at end of file diff --git a/src/main/java/io/jboot/web/directive/SharedEnumObject.java b/src/main/java/io/jboot/web/directive/SharedEnumObject.java new file mode 100644 index 0000000000000000000000000000000000000000..8c46fd4698e80f805ed361c1b6cc7273b4eb8ac9 --- /dev/null +++ b/src/main/java/io/jboot/web/directive/SharedEnumObject.java @@ -0,0 +1,278 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.directive; + +import com.jfinal.kit.LogKit; +import com.jfinal.template.expr.ast.MethodKeyBuilder; +import io.jboot.app.JdkUtil; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtMethod; +import javassist.NotFoundException; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * JFinalEnumObject 的主要目的是为了动态创建一个包装 Enum 枚举的类,方便模板引擎直接调用。 + *

+ * 添加枚举类型,便于在模板中使用 + * + *

+ * 例子:
+ * 1:定义枚举类型
+ *
+ * @JFinalSharedEnum
+ * public enum UserType {
+ *
+ *   ADMIN(1,"管理员"),
+ *   USER(2,"用户");
+ *
+ *   private int value;
+ *   private String text;
+ *
+ *   UserType(int value, String text) {
+ *     this.value = value;
+ *     this.text = text;
+ *   }
+ *
+ *
+ *    public static String text(Integer value) {
+ *          for (UserType type : values()) {
+ *                 if (type.value == value) {
+ *                     return type.text;
+ *                 }
+ *           }
+ *         return null
+ *    }
+ *
+ * }
+ *
+ *
+ * 2:模板中使用
+ * ### 以下的对象 u 通过 Controller 中的 setAttr("u", UserType.ADMIN) 传递
+ *
+ * #if( u == UserType.ADMIN )
+ *    #(UserType.ADMIN)
+ *
+ *    #(UserType.ADMIN.name())
+ *
+ *    #(UserType.ADMIN.hello())
+ * #end
+ *
+ * 或者
+ *
+ *  #(UserType.text(1))
+ *
+ * 
+ */ +public class SharedEnumObject extends LinkedHashMap { + + + private Class> enumClass; + private Map staticMethods; + + + void init(Class> enumClass, Map staticMethods) { + this.enumClass = enumClass; + this.staticMethods = staticMethods; + for (Enum e : enumClass.getEnumConstants()) { + put(e.name(), e); + } + } + + + protected Object invokeEnumMethod(String methodName) { + return doInvokeEnumMethod(methodName); + } + + protected Object invokeEnumMethod(String methodName, Object para1) { + return doInvokeEnumMethod(methodName, para1); + } + + + protected Object invokeEnumMethod(String methodName, Object para1, Object para2) { + return doInvokeEnumMethod(methodName, para1, para2); + } + + protected Object invokeEnumMethod(String methodName, Object para1, Object para2, Object para3) { + return doInvokeEnumMethod(methodName, para1, para2, para3); + } + + protected Object invokeEnumMethod(String methodName, Object para1, Object para2, Object para3, Object para4) { + return doInvokeEnumMethod(methodName, para1, para2, para3, para4); + } + + protected Object invokeEnumMethod(String methodName, Object para1, Object para2, Object para3, Object para4, Object para5) { + return doInvokeEnumMethod(methodName, para1, para2, para3, para4, para5); + } + + private Object doInvokeEnumMethod(String methodName, Object... paras) { + Method method = findMethod(methodName, paras); + if (method == null) { + throw new RuntimeException("Can not find the method: " + methodName + " with paras: " + Arrays.toString(paras) + " in enum: " + this.enumClass); + } + try { + return method.invoke(null, paras); + } catch (Exception ex) { + throw new RuntimeException(ex.toString(), ex); + } + } + + +// public Object value(Object text){ +// return invokeEnumMethod("value",text); +// } + + public Object invoke(String method, Object... paras) { + return doInvokeEnumMethod(method, paras); + } + + + public static SharedEnumObject create(Class> enumClass) { + try { + ClassPool pool = ClassPool.getDefault(); + CtClass objectCtClass = pool.getCtClass(Object.class.getName()); + CtClass supperClass = pool.get(SharedEnumObject.class.getName()); + + CtClass newClass = pool.makeClass(SharedEnumObject.class.getName() + "_$$_" + enumClass.getSimpleName()); + newClass.setSuperclass(supperClass); + newClass.setModifiers(Modifier.PUBLIC); + + Map enumStaticMethods = findEnumStaticMethods(enumClass); + + if (enumStaticMethods != null) { + for (Method originalMethod : enumStaticMethods.values()) { + boolean isReturnVoid = (void.class == originalMethod.getReturnType()); + CtClass returnClass = isReturnVoid ? CtClass.voidType : objectCtClass; + + CtClass[] parameterClassArray = createParameterClassArray(originalMethod, pool); + CtMethod ctMethod = new CtMethod(returnClass, originalMethod.getName(), parameterClassArray, newClass); + ctMethod.setModifiers(Modifier.PUBLIC); + + if (isReturnVoid) { + ctMethod.setBody("{invokeEnumMethod(\"" + originalMethod.getName() + "\",$$);}"); + } else { + ctMethod.setBody("{return invokeEnumMethod(\"" + originalMethod.getName() + "\",$$);}"); + } + + newClass.addMethod(ctMethod); + } + } + + // jdk 17 + // toClass() must add neighbor class in jdk17 + // neighbor: A class belonging to the same package that this class belongs to + + SharedEnumObject ret = JdkUtil.isJdk1x() ? (SharedEnumObject) newClass.toClass().newInstance() + : (SharedEnumObject) newClass.toClass(SharedEnumObject.class).newInstance(); + ret.init(enumClass, enumStaticMethods); + return ret; + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + + private static CtClass[] createParameterClassArray(Method originalMethod, ClassPool pool) throws NotFoundException { + if (originalMethod.getParameterCount() == 0) { + return new CtClass[0]; + } + CtClass[] ret = new CtClass[originalMethod.getParameterCount()]; + int index = 0; + for (Class clazz : originalMethod.getParameterTypes()) { + ret[index++] = pool.getCtClass(clazz.getName()); + } + return ret; + } + + + private static Map findEnumStaticMethods(Class> enumClass) { + Map retMap = null; + try { + Method[] methods = enumClass.getDeclaredMethods(); + for (Method method : methods) { + int methodModifiers = method.getModifiers(); + if (Modifier.isPublic(methodModifiers) && Modifier.isStatic(methodModifiers)) { + if (retMap == null) { + retMap = new HashMap<>(); + } + retMap.put(getMethodKey(enumClass, method.getName(), method.getParameterTypes()), method); + } + } + } catch (Exception ex) { + LogKit.logNothing(ex); + } + return retMap; + } + + + private Method findMethod(String methodName, Object... args) { + Class[] argTypes = null; + if (args != null && args.length > 0) { + argTypes = new Class[args.length]; + int index = 0; + for (Object arg : args) { + argTypes[index++] = arg != null ? arg.getClass() : null; + } + } + + long key = getMethodKey(this.enumClass, methodName, argTypes); + Method method = this.staticMethods.get(key); + if (method != null) { + return method; + } + + for (Method m : this.staticMethods.values()) { + if (m.getName().equals(methodName) && isMatchParas(m.getParameterTypes(), args)) { + this.staticMethods.put(key, m); + return m; + } + } + + return null; + } + + private boolean isMatchParas(Class[] parameterTypes, Object[] args) { + if (args == null || args.length == 0) { + return parameterTypes.length == 0; + } + + if (parameterTypes.length != args.length) { + return false; + } + + for (int i = 0; i < parameterTypes.length; i++) { + if (args[i] != null && !parameterTypes[i].isAssignableFrom(args[i].getClass())) { + return false; + } + } + return true; + } + + + private static Long getMethodKey(Class targetClass, String methodName, Class[] argTypes) { + return MethodKeyBuilder.getInstance().getMethodKey(targetClass, methodName, argTypes); + } + + +} diff --git a/src/main/java/io/jboot/web/directive/annotation/JFinalDirective.java b/src/main/java/io/jboot/web/directive/annotation/JFinalDirective.java index ce8d25a05a46248b2bd346a389e20394685cdeed..b276dae96ed68486cb991c4a06eee804e6cf5ba5 100644 --- a/src/main/java/io/jboot/web/directive/annotation/JFinalDirective.java +++ b/src/main/java/io/jboot/web/directive/annotation/JFinalDirective.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* 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 + * 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, @@ -20,9 +20,11 @@ import java.lang.annotation.*; @Inherited @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.TYPE }) +@Target({ElementType.TYPE}) public @interface JFinalDirective { String value(); + boolean override() default false; + } diff --git a/src/main/java/io/jboot/web/directive/annotation/JFinalSharedEnum.java b/src/main/java/io/jboot/web/directive/annotation/JFinalSharedEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..8308c422bd63dc48b0e219e130391248201bd278 --- /dev/null +++ b/src/main/java/io/jboot/web/directive/annotation/JFinalSharedEnum.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.directive.annotation; + +import java.lang.annotation.*; + + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface JFinalSharedEnum { + String value() default ""; + boolean override() default false; +} diff --git a/src/main/java/io/jboot/web/directive/annotation/JFinalSharedMethod.java b/src/main/java/io/jboot/web/directive/annotation/JFinalSharedMethod.java index b791d9037334dd1e962ad596f287de8b7d9ac751..b531b2a2bf9d66e9e47034174a6035a403b6605a 100644 --- a/src/main/java/io/jboot/web/directive/annotation/JFinalSharedMethod.java +++ b/src/main/java/io/jboot/web/directive/annotation/JFinalSharedMethod.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/web/directive/annotation/JFinalSharedObject.java b/src/main/java/io/jboot/web/directive/annotation/JFinalSharedObject.java index a8ebb2cb6499fc66cc485c630a75cd92886a0418..8b77a42ad2cc7eb91c2e66ecb1f12c285433d328 100644 --- a/src/main/java/io/jboot/web/directive/annotation/JFinalSharedObject.java +++ b/src/main/java/io/jboot/web/directive/annotation/JFinalSharedObject.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/web/directive/annotation/JFinalSharedStaticMethod.java b/src/main/java/io/jboot/web/directive/annotation/JFinalSharedStaticMethod.java index 5597137baf4814d4613f3ce0609e617ed0abab29..ce1847eb9d3fa47965dd21309789b43f3384d9f1 100644 --- a/src/main/java/io/jboot/web/directive/annotation/JFinalSharedStaticMethod.java +++ b/src/main/java/io/jboot/web/directive/annotation/JFinalSharedStaticMethod.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/web/directive/base/JbootDirectiveBase.java b/src/main/java/io/jboot/web/directive/base/JbootDirectiveBase.java index 2461616a23776b3fdad1e2dd2a84639bb2254b9d..9d66ea2ce64fe6f207db4e98546ebc5ca2ec0c9c 100644 --- a/src/main/java/io/jboot/web/directive/base/JbootDirectiveBase.java +++ b/src/main/java/io/jboot/web/directive/base/JbootDirectiveBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,16 +22,18 @@ import com.jfinal.template.TemplateException; import com.jfinal.template.expr.ast.ExprList; import com.jfinal.template.io.Writer; import com.jfinal.template.stat.Scope; +import io.jboot.utils.StrUtil; import java.io.IOException; +import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Map; /** * Jfinal 指令的基类 */ public abstract class JbootDirectiveBase extends Directive { - public JbootDirectiveBase() { Aop.inject(this); } @@ -88,12 +90,45 @@ public abstract class JbootDirectiveBase extends Directive { return (T) (data == null ? defaultValue : data); } + public String getParaToString(String key, Scope scope) { + Object object = getPara(key, scope, null); + if (object == null || object instanceof String) { + return (String) object; + } + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : objStr; + } + + + public String getParaToString(String key, Scope scope, String defaultValue) { + String v = getParaToString(key, scope); + return v == null ? defaultValue : v; + } + + + public String getParaToString(int index, Scope scope) { + Object object = getPara(index, scope, null); + if (object == null || object instanceof String) { + return (String) object; + } + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : objStr; + } + + + public String getParaToString(int index, Scope scope, String defaultValue) { + String v = getParaToString(index, scope); + return v == null ? defaultValue : v; + } + + public Integer getParaToInt(String key, Scope scope) { Object object = getPara(key, scope, null); if (object == null || object instanceof Integer) { return (Integer) object; } - return Integer.valueOf(object.toString()); + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : Integer.valueOf(objStr); } public Integer getParaToInt(String key, Scope scope, Integer defaultValue) { @@ -106,7 +141,8 @@ public abstract class JbootDirectiveBase extends Directive { if (object == null || object instanceof Integer) { return (Integer) object; } - return Integer.valueOf(object.toString()); + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : Integer.valueOf(objStr); } public Integer getParaToInt(int index, Scope scope, Integer defaultValue) { @@ -120,7 +156,8 @@ public abstract class JbootDirectiveBase extends Directive { if (object == null || object instanceof Long) { return (Long) object; } - return Long.valueOf(object.toString()); + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : Long.valueOf(objStr); } public Long getParaToLong(String key, Scope scope, Long defaultValue) { @@ -133,7 +170,8 @@ public abstract class JbootDirectiveBase extends Directive { if (object == null || object instanceof Long) { return (Long) object; } - return Long.valueOf(object.toString()); + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : Long.valueOf(objStr); } public Long getParaToLong(int index, Scope scope, Long defaultValue) { @@ -146,7 +184,8 @@ public abstract class JbootDirectiveBase extends Directive { if (object == null || object instanceof Boolean) { return (Boolean) object; } - return Boolean.valueOf(object.toString()); + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : Boolean.valueOf(objStr); } public Boolean getParaToBool(String key, Scope scope, Boolean defaultValue) { @@ -159,7 +198,8 @@ public abstract class JbootDirectiveBase extends Directive { if (object == null || object instanceof Boolean) { return (Boolean) object; } - return Boolean.valueOf(object.toString()); + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : Boolean.valueOf(objStr); } public Boolean getParaToBool(int index, Scope scope, Boolean defaultValue) { @@ -172,7 +212,8 @@ public abstract class JbootDirectiveBase extends Directive { if (object == null || object instanceof BigInteger) { return (BigInteger) object; } - return new BigInteger(object.toString()); + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : new BigInteger(objStr); } public BigInteger getParaToBigInteger(String key, Scope scope, BigInteger defaultValue) { @@ -185,7 +226,8 @@ public abstract class JbootDirectiveBase extends Directive { if (object == null || object instanceof BigInteger) { return (BigInteger) object; } - return new BigInteger(object.toString()); + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : new BigInteger(objStr); } public BigInteger getParaToBigInteger(int index, Scope scope, BigInteger defaultValue) { @@ -194,4 +236,43 @@ public abstract class JbootDirectiveBase extends Directive { } + public BigDecimal getParaToBigDecimal(String key, Scope scope) { + Object object = getPara(key, scope, null); + if (object == null || object instanceof BigDecimal) { + return (BigDecimal) object; + } + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : new BigDecimal(objStr); + } + + public BigDecimal getParaToBigDecimal(String key, Scope scope, BigDecimal defaultValue) { + BigDecimal v = getParaToBigDecimal(key, scope); + return v == null ? defaultValue : v; + } + + public BigDecimal getParaToBigDecimal(int index, Scope scope) { + Object object = getPara(index, scope, null); + if (object == null || object instanceof BigDecimal) { + return (BigDecimal) object; + } + String objStr = object.toString(); + return StrUtil.isBlank(objStr) ? null : new BigDecimal(objStr); + } + + + public BigDecimal getParaToBigDecimal(int index, Scope scope, BigDecimal defaultValue) { + BigDecimal v = getParaToBigDecimal(index, scope); + return v == null ? defaultValue : v; + } + + + public Map getParas(Scope scope) { + return scope.getData(); + } + + public Map getRootParas(Scope scope) { + return scope.getRootData(); + } + + } diff --git a/src/main/java/io/jboot/web/directive/base/PaginateDirectiveBase.java b/src/main/java/io/jboot/web/directive/base/PaginateDirectiveBase.java index 82f43caa77f72904d83eeefc33034bcc1a06a281..422488ed9caf841ce67f5ecfa706d027a18d44cc 100644 --- a/src/main/java/io/jboot/web/directive/base/PaginateDirectiveBase.java +++ b/src/main/java/io/jboot/web/directive/base/PaginateDirectiveBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,10 @@ public abstract class PaginateDirectiveBase extends JbootDirectiveBase { private static final String PREVIOUS_TEXT_KEY = "previousText"; private static final String NEXT_TEXT_KEY = "nextText"; private static final String PAGE_ITEMS_NAME_KEY = "pageItemsName"; + private static final String PAGE_DATA_KEY = "pageData"; + private static final String SIBLINGS_ITEM_COUNT_KEY = "siblingsItemCount"; + private static final String START_ITEM_COUNT_KEY = "startItemCount"; + private static final String END_ITEM_COUNT_KEY = "endItemCount"; private static final String DEFAULT_PREVIOUS_CLASS = "previous"; @@ -46,10 +50,15 @@ public abstract class PaginateDirectiveBase extends JbootDirectiveBase { private static final String DEFAULT_PREVIOUS_TEXT = "上一页"; private static final String DEFAULT_NEXT_TEXT = "下一页"; private static final String DEFAULT_PAGE_ITEMS_NAME = "pages"; + private static final String DEFAULT_PAGE_DATA_KEY = "pageData"; private static final String JAVASCRIPT_TEXT = "javascript:;"; private static final String ELLIPSIS_TEXT = "..."; + private static final int SIBLINGS_ITEM_COUNT = 2; + private static final int START_ITEM_COUNT = 1; + private static final int END_ITEM_COUNT = 1; + @Override public void onRender(Env env, Scope scope, Writer writer) { @@ -64,69 +73,105 @@ public abstract class PaginateDirectiveBase extends JbootDirectiveBase { String nextText = getPara(NEXT_TEXT_KEY, scope, DEFAULT_NEXT_TEXT); String pageItemsName = getPara(PAGE_ITEMS_NAME_KEY, scope, DEFAULT_PAGE_ITEMS_NAME); + String pageDataKey = getPara(PAGE_DATA_KEY, scope, DEFAULT_PAGE_DATA_KEY); + + int siblingsItemCount = getParaToInt(SIBLINGS_ITEM_COUNT_KEY, scope, SIBLINGS_ITEM_COUNT); + if (siblingsItemCount < 1) { + siblingsItemCount = SIBLINGS_ITEM_COUNT; + } + + + int startItemCount = getParaToInt(START_ITEM_COUNT_KEY, scope, START_ITEM_COUNT); + if (startItemCount < 1) { + startItemCount = START_ITEM_COUNT; + } + + + int endItemCount = getParaToInt(END_ITEM_COUNT_KEY, scope, END_ITEM_COUNT); + if (endItemCount < 1) { + endItemCount = END_ITEM_COUNT; + } + Page page = getPage(env, scope, writer); - int currentPage = page == null ? 1 : page.getPageNumber(); - int totalPage = page == null ? 1 : page.getTotalPage(); + int currentPageNumber = page == null ? 1 : page.getPageNumber(); + int totalPage = page == null ? 0 : page.getTotalPage(); - if ((totalPage <= 0) || (currentPage > totalPage)) { + if (totalPage == 0) { return; } - int startPage = currentPage - 4; - if (startPage < 1) { - startPage = 1; - } - int endPage = currentPage + 4; - if (endPage > totalPage) { - endPage = totalPage; + if (currentPageNumber > totalPage) { + currentPageNumber = totalPage; } - if (currentPage <= 8) { + int startPage = currentPageNumber - siblingsItemCount; + if (startPage < 1) { startPage = 1; } - if ((totalPage - currentPage) < 8) { + int endPage = currentPageNumber + siblingsItemCount; + if (endPage > totalPage) { endPage = totalPage; } List pages = new ArrayList(); - if (currentPage == 1) { + if (currentPageNumber == 1) { pages.add(new PaginateDirectiveBase.PaginateItem(previousClass + StrUtil.SPACE + disabledClass, JAVASCRIPT_TEXT, previousText)); } else { - pages.add(new PaginateDirectiveBase.PaginateItem(previousClass, getUrl(currentPage - 1, env, scope, writer), previousText)); - } - - if (currentPage > 8 && !onlyShowPreviousAndNext) { - pages.add(new PaginateDirectiveBase.PaginateItem(StrUtil.EMPTY, getUrl(1, env, scope, writer), 1)); - pages.add(new PaginateDirectiveBase.PaginateItem(StrUtil.EMPTY, getUrl(2, env, scope, writer), 2)); - pages.add(new PaginateDirectiveBase.PaginateItem(disabledClass, JAVASCRIPT_TEXT, ELLIPSIS_TEXT)); + pages.add(new PaginateDirectiveBase.PaginateItem(previousClass, getUrl(currentPageNumber - 1, env, scope, writer), previousText)); } if (!onlyShowPreviousAndNext) { + + //开始页码 + for (int i = 1; i <= startItemCount; i++) { + if (i < currentPageNumber - siblingsItemCount) { + pages.add(new PaginateDirectiveBase.PaginateItem(StrUtil.EMPTY, getUrl(i, env, scope, writer), i)); + } + } + + //省略号 + if (currentPageNumber > startItemCount + siblingsItemCount + 1) { + pages.add(new PaginateDirectiveBase.PaginateItem(disabledClass, JAVASCRIPT_TEXT, ELLIPSIS_TEXT)); + } + + + //中间页码 for (int i = startPage; i <= endPage; i++) { - if (currentPage == i) { + if (currentPageNumber == i) { pages.add(new PaginateDirectiveBase.PaginateItem(activeClass, JAVASCRIPT_TEXT, i)); } else { pages.add(new PaginateDirectiveBase.PaginateItem(StrUtil.EMPTY, getUrl(i, env, scope, writer), i)); } } - } - if ((totalPage - currentPage) >= 8 && !onlyShowPreviousAndNext) { - pages.add(new PaginateDirectiveBase.PaginateItem(disabledClass, JAVASCRIPT_TEXT, ELLIPSIS_TEXT)); - pages.add(new PaginateDirectiveBase.PaginateItem(StrUtil.EMPTY, getUrl(totalPage - 1, env, scope, writer), totalPage - 1)); - pages.add(new PaginateDirectiveBase.PaginateItem(StrUtil.EMPTY, getUrl(totalPage, env, scope, writer), totalPage)); + + //省略号 + if (currentPageNumber < totalPage - siblingsItemCount - endItemCount) { + pages.add(new PaginateDirectiveBase.PaginateItem(disabledClass, JAVASCRIPT_TEXT, ELLIPSIS_TEXT)); + } + + //后边页码 + for (int i = (endItemCount - 1); i >= 0; i--) { + if (i < totalPage - (currentPageNumber + siblingsItemCount)) { + pages.add(new PaginateDirectiveBase.PaginateItem(StrUtil.EMPTY, getUrl(totalPage - i, env, scope, writer), totalPage - i)); + } + + } } - if (currentPage == totalPage) { + + if (currentPageNumber == totalPage) { pages.add(new PaginateDirectiveBase.PaginateItem(nextClass + StrUtil.SPACE + disabledClass, JAVASCRIPT_TEXT, nextText)); } else { - pages.add(new PaginateDirectiveBase.PaginateItem(nextClass, getUrl(currentPage + 1, env, scope, writer), nextText)); + pages.add(new PaginateDirectiveBase.PaginateItem(nextClass, getUrl(currentPageNumber + 1, env, scope, writer), nextText)); } scope.setLocal(pageItemsName, pages); + scope.setLocal(pageDataKey, page); + renderBody(env, scope, writer); } diff --git a/src/main/java/io/jboot/web/fixedinterceptor/FixedInterceptors.java b/src/main/java/io/jboot/web/fixedinterceptor/FixedInterceptors.java deleted file mode 100644 index 3dad2418f0d475e7d6caf702ec2d0f534b5f31f4..0000000000000000000000000000000000000000 --- a/src/main/java/io/jboot/web/fixedinterceptor/FixedInterceptors.java +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.jboot.web.fixedinterceptor; - -import com.jfinal.aop.Aop; -import io.jboot.components.limiter.LimiterInterceptor; -import io.jboot.support.jwt.JwtInterceptor; -import io.jboot.support.metric.JbootMetricInterceptor; -import io.jboot.support.seata.interceptor.SeataGlobalTransactionalInterceptor; -import io.jboot.support.sentinel.SentinelInterceptor; -import io.jboot.support.shiro.JbootShiroInterceptor; -import io.jboot.web.validate.ParaValidateInterceptor; -import io.jboot.web.cors.CORSInterceptor; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; - -/** - * @author Michael Yang 杨福海 (fuhai999@gmail.com) - * @version V1.0 - * @Package io.jboot.web.fixedinterceptor - */ -public class FixedInterceptors { - - private static final FixedInterceptors me = new FixedInterceptors(); - - public static FixedInterceptors me() { - return me; - } - - - /** - * 默认的 Jboot 系统拦截器 - */ - private FixedInterceptorWapper[] defaultInters = new FixedInterceptorWapper[]{ - new FixedInterceptorWapper(new SentinelInterceptor(), 9), - new FixedInterceptorWapper(new LimiterInterceptor(), 10), - new FixedInterceptorWapper(new CORSInterceptor(), 20), - new FixedInterceptorWapper(new ParaValidateInterceptor(), 30), - new FixedInterceptorWapper(new JwtInterceptor(), 40), - new FixedInterceptorWapper(new JbootShiroInterceptor(), 50), - new FixedInterceptorWapper(new JbootMetricInterceptor(), 60), - new FixedInterceptorWapper(new SeataGlobalTransactionalInterceptor(), 80), - }; - - private List userInters = new ArrayList<>(); - - private FixedInterceptor[] allInters = null; - - private List inters; - - FixedInterceptor[] all() { - if (allInters == null) { - synchronized (this) { - if (allInters == null) { - initInters(); - } - } - } - return allInters; - } - - - private void initInters() { - - FixedInterceptor[] interceptors = new FixedInterceptor[defaultInters.length + userInters.size()]; - inters = new ArrayList<>(); - inters.addAll(Arrays.asList(defaultInters)); - inters.addAll(userInters); - inters.sort(Comparator.comparingInt(FixedInterceptorWapper::getOrderNo)); - - int i = 0; - for (FixedInterceptorWapper interceptor : inters) { - interceptors[i++] = interceptor.getFixedInterceptor(); - } - - allInters = interceptors; - } - - - public void add(FixedInterceptor interceptor) { - Aop.inject(interceptor); - userInters.add(new FixedInterceptorWapper(interceptor)); - } - - public void add(FixedInterceptor interceptor, int orderNo) { - if (orderNo < 0) { - orderNo = 0; - } - Aop.inject(interceptor); - userInters.add(new FixedInterceptorWapper(interceptor, orderNo)); - } - - public List list() { - return inters; - } -} diff --git a/src/main/java/io/jboot/web/handler/ConsoleColor.java b/src/main/java/io/jboot/web/handler/ConsoleColor.java new file mode 100644 index 0000000000000000000000000000000000000000..84f3ea6470f61c5e27a28ab31bf50e5789a09a22 --- /dev/null +++ b/src/main/java/io/jboot/web/handler/ConsoleColor.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jboot.web.handler; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public enum ConsoleColor { + + //颜色结尾字符串,重置颜色的 + RESET("\033[0m"), + + // Regular Colors 普通颜色,不带加粗,背景色等 + BLACK("\033[0;30m"), // BLACK + RED("\033[0;31m"), // RED + GREEN("\033[0;32m"), // GREEN + YELLOW("\033[0;33m"), // YELLOW + BLUE("\033[0;34m"), // BLUE + MAGENTA("\033[0;35m"), // MAGENTA + CYAN("\033[0;36m"), // CYAN + WHITE("\033[0;37m"), // WHITE + + // Bold + BLACK_BOLD("\033[1;30m"), // BLACK + RED_BOLD("\033[1;31m"), // RED + GREEN_BOLD("\033[1;32m"), // GREEN + YELLOW_BOLD("\033[1;33m"), // YELLOW + BLUE_BOLD("\033[1;34m"), // BLUE + MAGENTA_BOLD("\033[1;35m"), // MAGENTA + CYAN_BOLD("\033[1;36m"), // CYAN + WHITE_BOLD("\033[1;37m"), // WHITE + + // Underline + BLACK_UNDERLINED("\033[4;30m"), // BLACK + RED_UNDERLINED("\033[4;31m"), // RED + GREEN_UNDERLINED("\033[4;32m"), // GREEN + YELLOW_UNDERLINED("\033[4;33m"), // YELLOW + BLUE_UNDERLINED("\033[4;34m"), // BLUE + MAGENTA_UNDERLINED("\033[4;35m"), // MAGENTA + CYAN_UNDERLINED("\033[4;36m"), // CYAN + WHITE_UNDERLINED("\033[4;37m"), // WHITE + + // Background + BLACK_BACKGROUND("\033[40m"), // BLACK + RED_BACKGROUND("\033[41m"), // RED + GREEN_BACKGROUND("\033[42m"), // GREEN + YELLOW_BACKGROUND("\033[43m"), // YELLOW + BLUE_BACKGROUND("\033[44m"), // BLUE + MAGENTA_BACKGROUND("\033[45m"), // MAGENTA + CYAN_BACKGROUND("\033[46m"), // CYAN + WHITE_BACKGROUND("\033[47m"), // WHITE + + // High Intensity + BLACK_BRIGHT("\033[0;90m"), // BLACK + RED_BRIGHT("\033[0;91m"), // RED + GREEN_BRIGHT("\033[0;92m"), // GREEN + YELLOW_BRIGHT("\033[0;93m"), // YELLOW + BLUE_BRIGHT("\033[0;94m"), // BLUE + MAGENTA_BRIGHT("\033[0;95m"), // MAGENTA + CYAN_BRIGHT("\033[0;96m"), // CYAN + WHITE_BRIGHT("\033[0;97m"), // WHITE + + // Bold High Intensity + BLACK_BOLD_BRIGHT("\033[1;90m"), // BLACK + RED_BOLD_BRIGHT("\033[1;91m"), // RED + GREEN_BOLD_BRIGHT("\033[1;92m"), // GREEN + YELLOW_BOLD_BRIGHT("\033[1;93m"), // YELLOW + BLUE_BOLD_BRIGHT("\033[1;94m"), // BLUE + MAGENTA_BOLD_BRIGHT("\033[1;95m"), // MAGENTA + CYAN_BOLD_BRIGHT("\033[1;96m"), // CYAN + WHITE_BOLD_BRIGHT("\033[1;97m"), // WHITE + + // High Intensity backgrounds + BLACK_BACKGROUND_BRIGHT("\033[0;100m"), // BLACK + RED_BACKGROUND_BRIGHT("\033[0;101m"), // RED + GREEN_BACKGROUND_BRIGHT("\033[0;102m"), // GREEN + YELLOW_BACKGROUND_BRIGHT("\033[0;103m"), // YELLOW + BLUE_BACKGROUND_BRIGHT("\033[0;104m"), // BLUE + MAGENTA_BACKGROUND_BRIGHT("\033[0;105m"), // MAGENTA + CYAN_BACKGROUND_BRIGHT("\033[0;106m"), // CYAN + WHITE_BACKGROUND_BRIGHT("\033[0;107m"); // WHITE + + private final String code; + + ConsoleColor(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/handler/JbootActionHandler.java b/src/main/java/io/jboot/web/handler/JbootActionHandler.java index e2cf42ba37817f51ebd6f52f53079f8dea7cf4e9..0434f8f2dbcd79bd1ebb75f2f7b2b687f2c86f83 100644 --- a/src/main/java/io/jboot/web/handler/JbootActionHandler.java +++ b/src/main/java/io/jboot/web/handler/JbootActionHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,19 @@ package io.jboot.web.handler; import com.jfinal.aop.Invocation; import com.jfinal.core.*; import com.jfinal.log.Log; +import com.jfinal.render.IRenderFactory; import com.jfinal.render.Render; import com.jfinal.render.RenderException; +import com.jfinal.template.TemplateException; +import io.jboot.app.JbootApplicationConfig; +import io.jboot.components.valid.ValidErrorRender; +import io.jboot.components.valid.ValidException; +import io.jboot.components.valid.ValidUtil; import io.jboot.utils.ClassUtil; import io.jboot.web.controller.JbootControllerContext; -import io.jboot.web.fixedinterceptor.FixedInvocation; +import io.jboot.web.render.JbootErrorRender; +import io.jboot.web.render.JbootRenderFactory; +import io.jboot.web.render.JbootReturnValueRender; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -30,23 +38,47 @@ import javax.servlet.http.HttpServletResponse; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web */ public class JbootActionHandler extends ActionHandler { - private static final Log log = Log.getLog(JbootActionHandler.class); + private static final Log LOG = Log.getLog(JbootActionHandler.class); + private static final JbootApplicationConfig appConfig = JbootApplicationConfig.get(); /** - * 方便子类复写、从而可以实现 自定义Action的功能 + * 方便子类复写、从而可以实现 自定义 Action 的功能 + * + * @param target + * @param urlPara + * @param request + * @return + */ + public Action getAction(String target, String[] urlPara, HttpServletRequest request) { + return this.getAction(target, urlPara); + } + + + /** + * 方便子类复写、从而可以实现 自定义 Action 的功能 * * @param target * @param urlPara * @return */ @Override - public Action getAction(String target, String[] urlPara) { - return actionMapping.getAction(target, urlPara); + protected Action getAction(String target, String[] urlPara) { + return super.getAction(target, urlPara); + } + + /** + * 方便子类复写、从而可以实现 自定义 Invocation 的功能 + * + * @param action + * @param controller + * @return + */ + public Invocation getInvocation(Action action, Controller controller) { + return JbootActionReporter.isReportEnable() ? new JbootActionReporterInvocation(action, controller) : new JbootActionInvocation(action, controller); } @@ -58,86 +90,156 @@ public class JbootActionHandler extends ActionHandler { */ @Override public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { - if (target.indexOf('.') != -1) { + if (target.lastIndexOf('.') != -1) { return; } isHandled[0] = true; String[] urlPara = {null}; - Action action = getAction(target, urlPara); + Action action = getAction(target, urlPara, request); if (action == null) { - if (log.isWarnEnabled()) { + if (!appConfig.isHandle404()) { + isHandled[0] = false; + return; + } + + if (LOG.isWarnEnabled()) { String qs = request.getQueryString(); - log.warn("404 Action Not Found: " + (qs == null ? target : target + "?" + qs)); + LOG.warn("404 Action Not Found: " + (qs == null ? target : target + "?" + qs)); } renderManager.getRenderFactory().getErrorRender(404).setContext(request, response).render(); return; } - - Controller controller = null; try { controller = controllerFactory.getController(action.getControllerClass()); - if (injectDependency) { - com.jfinal.aop.Aop.inject(controller); - } - JbootControllerContext.hold(controller); -// controller.init(request, response, urlPara[0]); + //controller.init(request, response, urlPara[0]); CPI._init_(controller, action, request, response, urlPara[0]); - Invocation invocation = new Invocation(action, controller); - if (devMode) { - if (ActionReporter.isReportAfterInvocation(request)) { - invokeInvocation(invocation); - ActionReporter.report(target, controller, action); - } else { - ActionReporter.report(target, controller, action); - invokeInvocation(invocation); - } - } else { - invokeInvocation(invocation); - } + JbootControllerContext.hold(controller); - Render render = controller.getRender(); - if (render instanceof ForwardActionRender) { - String actionUrl = ((ForwardActionRender) render).getActionUrl(); - if (target.equals(actionUrl)) { - throw new RuntimeException("The forward action url is the same as before."); - } else { - handle(actionUrl, request, response, isHandled); + //Invocation invocation = new Invocation(action, controller); + Invocation invocation = getInvocation(action, controller); + + if (JbootActionReporter.isReportEnable()) { + long time = System.currentTimeMillis(); + try { + doStartRender(target, action, controller, invocation, isHandled); + } finally { + JbootActionReporter.report(target, controller, action, invocation, time); } - return; + } else { + doStartRender(target, action, controller, invocation, isHandled); } - if (render == null) { - render = renderManager.getRenderFactory().getDefaultRender(action.getViewPath() + action.getMethodName()); - } + doAfterRender(action, controller); - render.setContext(request, response, action.getViewPath()).render(); } catch (RenderException e) { - if (log.isErrorEnabled()) { + if (LOG.isErrorEnabled()) { String qs = request.getQueryString(); - log.error(qs == null ? target : target + "?" + qs, e); + LOG.error(qs == null ? target : target + "?" + qs, e); } } catch (ActionException e) { + if (e.getErrorCode() == 404 && !appConfig.isHandle404()) { + isHandled[0] = false; + return; + } handleActionException(target, request, response, action, e); + } catch (ValidException e) { + handleValidException(target, request, response, action, e); + } catch (TemplateException e) { + handleTemplateException(target, request, response, action, e); } catch (Exception e) { - if (log.isErrorEnabled()) { - String qs = request.getQueryString(); - String targetInfo = qs == null ? target : target + "?" + qs; - String info = ClassUtil.buildMethodString(action.getMethod()); - log.error(info + " : " + targetInfo, e); - } - renderManager.getRenderFactory().getErrorRender(500).setContext(request, response, action.getViewPath()).render(); + handleException(target, request, response, action, e); } finally { JbootControllerContext.release(); controllerFactory.recycle(controller); } } + protected boolean isJspTarget(String target) { + return target.toLowerCase().contains(".jsp"); + } + + + protected void doAfterRender(Action action, Controller controller) { + } + + + protected void doStartRender(String target + , Action action + , Controller controller + , Invocation invocation + , boolean[] isHandled) { + + invocation.invoke(); + + Render render = controller.getRender(); + if (render instanceof ForwardActionRender) { + String actionUrl = ((ForwardActionRender) render).getActionUrl(); + if (target.equals(actionUrl)) { + throw new RuntimeException("The forward action url is the same as before."); + } else { + handle(actionUrl, controller.getRequest(), controller.getResponse(), isHandled); + } + } else { + if (render == null && void.class != action.getMethod().getReturnType() + && renderManager.getRenderFactory() instanceof JbootRenderFactory) { + + JbootRenderFactory factory = (JbootRenderFactory) renderManager.getRenderFactory(); + JbootReturnValueRender returnValueRender = factory.getReturnValueRender(invocation.getReturnValue()); - private void handleActionException(String target, HttpServletRequest request, HttpServletResponse response, Action action, ActionException e) { + String forwardTo = returnValueRender.getForwardTo(); + if (forwardTo != null) { + handle(getRealForwrdTo(forwardTo, target, action), controller.getRequest(), controller.getResponse(), isHandled); + return; + } else { + render = returnValueRender; + //重新设置到 Controller,JbootActionReporter 才能 Controller 获取 render 判断 render 类型 + controller.render(render); + } + } + + if (render == null) { + render = renderManager.getRenderFactory().getDefaultRender(action.getViewPath() + action.getMethodName()); + + //重新设置到 Controller,JbootActionReporter 才能 Controller 获取 render 判断 render 类型 + controller.render(render); + } + + render.setContext(controller.getRequest(), controller.getResponse(), action.getViewPath()).render(); + } + } + + public String getRealForwrdTo(String forwardTo, String currentTarget, Action action) { + if ("".equals(forwardTo)) { + throw new IllegalArgumentException(ClassUtil.buildMethodString(action.getMethod()) + ": The forward key can not be blank."); + } + + if (forwardTo.startsWith("/")) { + return forwardTo; + } + + + if (forwardTo.startsWith("./")) { + return currentTarget.substring(0, currentTarget.lastIndexOf("/")) + forwardTo.substring(1); + } + + return "/" + forwardTo; + } + + + /** + * 处理错误信息 + * + * @param target + * @param request + * @param response + * @param action + * @param e + */ + protected void handleActionException(String target, HttpServletRequest request, HttpServletResponse response, Action action, ActionException e) { int errorCode = e.getErrorCode(); String msg = null; if (errorCode == 404) { @@ -150,19 +252,25 @@ public class JbootActionHandler extends ActionHandler { msg = "403 Forbidden: "; } + if (msg != null) { - if (log.isWarnEnabled()) { - String qs = request.getQueryString(); - msg = msg + (qs == null ? target : target + "?" + qs); - if (e.getMessage() != null) { - msg = msg + "\n" + e.getMessage(); + if (errorCode == 404 || errorCode == 401 || errorCode == 403) { + if (LOG.isWarnEnabled()) { + String qs = request.getQueryString(); + msg = msg + (qs == null ? target : target + "?" + qs); + LOG.info(msg, e); + } + } else { + if (LOG.isErrorEnabled()) { + String qs = request.getQueryString(); + msg = msg + (qs == null ? target : target + "?" + qs); + LOG.error(msg, e); } - log.warn(msg); } } else { - if (log.isErrorEnabled()) { + if (LOG.isErrorEnabled()) { String qs = request.getQueryString(); - log.error(errorCode + " Error: " + (qs == null ? target : target + "?" + qs), e); + LOG.error(errorCode + " Error: " + (qs == null ? target : target + "?" + qs), e); } } @@ -170,9 +278,56 @@ public class JbootActionHandler extends ActionHandler { } + /** + * 处理参数验证错误 + */ + protected void handleValidException(String target, HttpServletRequest request, HttpServletResponse response, Action action, ValidException validException) { + if (LOG.isErrorEnabled()) { +// String qs = request.getQueryString(); +// String targetInfo = qs == null ? target : target + "?" + qs; +// LOG.error(validException.getReason() + " : " + targetInfo, validException); + LOG.error("Invalid parameter: " + validException.getReason()); + } + IRenderFactory factory = renderManager.getRenderFactory(); + if (factory instanceof JbootRenderFactory) { + ValidErrorRender render = ((JbootRenderFactory) factory).getValidErrorRender(validException); + render.setContext(request, response, action.getViewPath()).render(); + } else { + Render render = renderManager.getRenderFactory().getErrorRender(ValidUtil.getErrorCode()); + if (render instanceof JbootErrorRender) { + ((JbootErrorRender) render).setThrowable(validException); + } + render.setContext(request, response, action.getViewPath()).render(); + } + } + + + /** + * 处理模板错误 + */ + protected void handleTemplateException(String target, HttpServletRequest request, HttpServletResponse response, Action action, TemplateException e) { + String qs = request.getQueryString(); + String targetInfo = qs == null ? target : target + "?" + qs; + String info = ClassUtil.buildMethodString(action.getMethod()); + LOG.error(info + " \nQuery: " + targetInfo + "\n", e); + + IRenderFactory factory = renderManager.getRenderFactory(); + if (factory instanceof JbootRenderFactory) { + ((JbootRenderFactory) factory).getTemplateErrorRender(e).setContext(request, response, action.getViewPath()).render(); + } + } + - private void invokeInvocation(Invocation inv) { - new FixedInvocation(inv).invoke(); + /** + * 处理其他业务错误 + */ + protected void handleException(String target, HttpServletRequest request, HttpServletResponse response, Action action, Exception e) { + String qs = request.getQueryString(); + String targetInfo = qs == null ? target : target + "?" + qs; + String info = ClassUtil.buildMethodString(action.getMethod()); + LOG.error(info + " \nQuery: " + targetInfo + "\n", e); + renderManager.getRenderFactory().getErrorRender(500).setContext(request, response, action.getViewPath()).render(); } + } diff --git a/src/main/java/io/jboot/web/fixedinterceptor/FixedInvocation.java b/src/main/java/io/jboot/web/handler/JbootActionInvocation.java similarity index 33% rename from src/main/java/io/jboot/web/fixedinterceptor/FixedInvocation.java rename to src/main/java/io/jboot/web/handler/JbootActionInvocation.java index fe85f12e90ee0085887fe1348e942b645f37092e..7e872ea73e32e9b56f697516c7006a5c56e91495 100644 --- a/src/main/java/io/jboot/web/fixedinterceptor/FixedInvocation.java +++ b/src/main/java/io/jboot/web/handler/JbootActionInvocation.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,29 +13,56 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.web.fixedinterceptor; +package io.jboot.web.handler; +import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; +import com.jfinal.core.Action; import com.jfinal.core.Controller; +import io.jboot.aop.InterceptorBuilderManager; +import io.jboot.aop.InterceptorCache; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** - * @author Michael Yang 杨福海 (fuhai999@gmail.com) - * @version V1.0 - * @Package io.jboot.web.handler + * @author michael yang (fuhai999@gmail.com) */ -public class FixedInvocation extends Invocation { +public class JbootActionInvocation extends Invocation { - private Invocation origin; + protected Action action; + protected Object target; + protected Object[] args; + protected Interceptor[] inters; + protected int index = 0; - private FixedInterceptor[] inters = FixedInterceptors.me().all(); - private int index = 0; + protected Object returnValue; - public FixedInvocation(Invocation invocation) { - this.origin = invocation; + protected static InterceptorBuilderManager builderManager = InterceptorBuilderManager.me(); + + public JbootActionInvocation(Action action, Controller controller) { + + this.action = action; + this.inters = buildInterceptors(action); + this.target = controller; + + this.args = action.getParameterGetter().get(action, controller); + } + + + protected Interceptor[] buildInterceptors(Action action) { + + InterceptorCache.MethodKey key = InterceptorCache.getMethodKey(action.getControllerClass(), action.getMethod()); + Interceptor[] inters = InterceptorCache.get(key); + if (inters == null) { + inters = action.getInterceptors(); + inters = builderManager.build(action.getControllerClass(), action.getMethod(), inters); + InterceptorCache.put(key, inters); + } + + return inters; } @@ -44,78 +71,133 @@ public class FixedInvocation extends Invocation { if (index < inters.length) { inters[index++].intercept(this); } else if (index++ == inters.length) { // index++ ensure invoke action only one time - origin.invoke(); + try { + // Invoke the action + returnValue = action.getMethod().invoke(target, args); + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + if (t == null) { + t = e; + } + throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); + } } } @Override - public Method getMethod() { - return origin.getMethod(); + public Object getArg(int index) { + if (index >= args.length) { + throw new ArrayIndexOutOfBoundsException(); + } + return args[index]; } + @Override - public Controller getController() { - return origin.getController(); + public void setArg(int index, Object value) { + if (index >= args.length) { + throw new ArrayIndexOutOfBoundsException(); + } + args[index] = value; } + @Override - public String getActionKey() { - return origin.getActionKey(); + public Object[] getArgs() { + return args; } + @Override - public String getControllerKey() { - return origin.getControllerKey(); + public T getTarget() { + return (T) target; } + /** + * Return the method of this action. + *

+ * You can getMethod.getAnnotations() to get annotation on action method to do more things + */ @Override - public String getMethodName() { - return origin.getMethodName(); + public Method getMethod() { + return action.getMethod(); } + /** + * Return the method name of this action's method. + */ @Override - public boolean isActionInvocation() { - return true; + public String getMethodName() { + return action.getMethodName(); } + /** + * Get the return value of the target method + */ @Override - public Object getArg(int index) { - return origin.getArg(index); + public T getReturnValue() { + return (T) returnValue; } + @Override - public void setArg(int index, Object value) { - origin.setArg(index, value); + public void setReturnValue(Object returnValue) { + this.returnValue = returnValue; } + // --------- + /** + * Return the controller of this action. + */ @Override - public Object[] getArgs() { - return origin.getArgs(); + public Controller getController() { + return (Controller) target; } + /** + * Return the action key. + * actionKey = controllerPath + methodName + */ @Override - public T getTarget() { - return origin.getTarget(); + public String getActionKey() { + return action.getActionKey(); } + /** + * Return the controller path. + */ @Override - public T getReturnValue() { - return origin.getReturnValue(); + public String getControllerPath() { + return action.getControllerPath(); } + /** + * 该方法已改名为 getControllerPath() + */ @Override - public void setReturnValue(Object returnValue) { - origin.setReturnValue(returnValue); + @Deprecated + public String getControllerKey() { + return getControllerPath(); } + /** + * Return view path of this controller. + */ @Override public String getViewPath() { - return origin.getViewPath(); + return action.getViewPath(); } - public Invocation getOrigin() { - return origin; + /** + * return true if it is action invocation. + */ + @Override + public boolean isActionInvocation() { + return action != null; } - } diff --git a/src/main/java/io/jboot/web/handler/JbootActionReporter.java b/src/main/java/io/jboot/web/handler/JbootActionReporter.java new file mode 100644 index 0000000000000000000000000000000000000000..54460879013221d61940f308f2cf58686406ddaa --- /dev/null +++ b/src/main/java/io/jboot/web/handler/JbootActionReporter.java @@ -0,0 +1,321 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.handler; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.core.Action; +import com.jfinal.core.ActionReporter; +import com.jfinal.core.Controller; +import com.jfinal.core.JFinal; +import com.jfinal.kit.JsonKit; +import com.jfinal.render.*; +import io.jboot.Jboot; +import io.jboot.JbootConsts; +import io.jboot.support.jwt.JwtInterceptor; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.ReflectUtil; +import io.jboot.utils.RequestUtil; +import io.jboot.utils.StrUtil; +import io.jboot.web.controller.JbootController; +import io.jboot.web.render.JbootReturnValueRender; +import javassist.*; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; + + +/** + * JbootActionReporter 参考 ActionReporter + */ +public class JbootActionReporter { + + private static final String title = "\nJboot-" + JbootConsts.VERSION + " action report -------- "; + private static final String interceptMethodDesc = "(Lcom/jfinal/aop/Invocation;)V"; + private static int maxOutputLengthOfParaValue = 512; + private static Writer writer = new SystemOutWriter(); + private static ActionReporter actionReporter = JFinal.me().getConstants().getActionReporter(); + private static boolean reportEnable = Jboot.isDevMode(); + private static boolean colorRenderEnable = true; + private static boolean reportAllText = false; + + private static final ThreadLocal sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + + + public static void setMaxOutputLengthOfParaValue(int maxOutputLengthOfParaValue) { + if (maxOutputLengthOfParaValue < 16) { + throw new IllegalArgumentException("maxOutputLengthOfParaValue must more than 16"); + } + JbootActionReporter.maxOutputLengthOfParaValue = maxOutputLengthOfParaValue; + } + + public static void setWriter(Writer writer) { + if (writer == null) { + throw new IllegalArgumentException("writer can not be null"); + } + JbootActionReporter.writer = writer; + } + + public static Writer getWriter() { + return writer; + } + + public static boolean isReportEnable() { + return reportEnable; + } + + public static void setReportEnable(boolean reportEnable) { + JbootActionReporter.reportEnable = reportEnable; + } + + public static boolean isReportAllText() { + return reportAllText; + } + + public static void setReportAllText(boolean reportAllText) { + JbootActionReporter.reportAllText = reportAllText; + } + + public static boolean isColorRenderEnable() { + return colorRenderEnable; + } + + public static void setColorRenderEnable(boolean colorRenderEnable) { + JbootActionReporter.colorRenderEnable = colorRenderEnable; + } + + /** + * Report the action + */ + public static void report(String target, Controller controller, Action action, Invocation invocation, long time) { + try { + doReport(target, controller, action, invocation, time); + } + // 在 tomcat 或者自定义 classloader 的情况下, + // 可能会出现 NotFoundException 错误 + catch (NotFoundException e) { + ClassPool.getDefault().insertClassPath(new ClassClassPath(controller.getClass())); + try { + doReport(target, controller, action, invocation, time); + } catch (Exception exception) { + actionReporter.report(target, controller, action); + } + } catch (Exception ex) { + actionReporter.report(target, controller, action); + } finally { + JbootActionReporterInvocation.clear(); + } + } + + + private static void doReport(String target, Controller controller, Action action, Invocation invocation, long time) throws Exception { + CtClass ctClass = ClassPool.getDefault().get(ClassUtil.getUsefulClass(action.getControllerClass()).getName()); + ClassPool.getDefault().get(ClassUtil.getUsefulClass(action.getControllerClass()).getName()); + String desc = JbootActionReporterUtil.getMethodDescWithoutName(action.getMethod()); + CtMethod ctMethod = ctClass.getMethod(action.getMethodName(), desc); + int lineNumber = ctMethod.getMethodInfo().getLineNumber(0); + + StringBuilder sb = new StringBuilder(title).append(sdf.get().format(new Date(time))).append(" -------------------------\n"); + sb.append("Request : ").append(controller.getRequest().getMethod()).append(" ").append(target).append("\n"); + Class cc = action.getMethod().getDeclaringClass(); + sb.append("Controller : ").append(cc.getName()).append(".(").append(getClassFileName(cc)).append(".java:" + lineNumber + ")"); + if (JbootActionReporterInvocation.isControllerInvoked()) { + sb.append((colorRenderEnable ? ConsoleColor.GREEN_BRIGHT : "") + " ---> invoked √" + (colorRenderEnable ? ConsoleColor.RESET : "")); + } else { + sb.append((colorRenderEnable ? ConsoleColor.RED_BRIGHT : "") + " ---> skipped ×" + (colorRenderEnable ? ConsoleColor.RESET : "")); + } + sb.append("\nMethod : ").append(JbootActionReporterUtil.getMethodString(action.getMethod())).append("\n"); + + + String urlParas = controller.getPara(); + if (urlParas != null) { + sb.append("UrlPara : ").append(urlParas).append("\n"); + } + + Interceptor[] inters = invocation instanceof JbootActionReporterInvocation ? ((JbootActionReporterInvocation) invocation).getInters() : action.getInterceptors(); + List invokedInterceptors = JbootActionReporterInvocation.getInvokedInterceptor(); + + boolean printJwt = false; + + if (inters.length > 0) { + sb.append("Interceptor : "); + for (int i = 0; i < inters.length; i++) { + if (i > 0) { + sb.append("\n "); + } + Interceptor inter = inters[i]; + Class interClass = ClassUtil.getUsefulClass(inter.getClass()); + + if (interClass == JwtInterceptor.class) { + printJwt = true; + } + + CtClass icClass = ClassPool.getDefault().get(interClass.getName()); + CtMethod icMethod = icClass.getMethod("intercept", interceptMethodDesc); + int icLineNumber = icMethod.getMethodInfo().getLineNumber(0); + sb.append(icMethod.getDeclaringClass().getName()).append(".(").append(getClassFileName(interClass)).append(".java:" + icLineNumber + ")"); + + if (invokedInterceptors.contains(inter)) { + sb.append((colorRenderEnable ? ConsoleColor.GREEN_BRIGHT : "") + " ---> invoked √" + (colorRenderEnable ? ConsoleColor.RESET : "")); + } else { + sb.append((colorRenderEnable ? ConsoleColor.RED_BRIGHT : "") + " ---> skipped ×" + (colorRenderEnable ? ConsoleColor.RESET : "")); + } + } + sb.append("\n"); + } + + // print all parameters + HttpServletRequest request = controller.getRequest(); + Enumeration e = request.getParameterNames(); + if (e.hasMoreElements()) { + sb.append("Parameter : "); + while (e.hasMoreElements()) { + String name = e.nextElement(); + String[] values = request.getParameterValues(name); + if (values.length == 1) { + sb.append(name).append("="); + if (values[0] != null && values[0].length() > maxOutputLengthOfParaValue) { + sb.append(values[0], 0, maxOutputLengthOfParaValue).append("..."); + } else { + sb.append(values[0]); + } + } else { + sb.append(name).append("[]={"); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + sb.append(","); + } + sb.append(values[i]); + } + sb.append("}"); + } + sb.append(" "); + } + sb.append("\n"); + } + + + if (!"GET".equalsIgnoreCase(controller.getRequest().getMethod()) + && !RequestUtil.isMultipartRequest(controller.getRequest()) + && StrUtil.isNotBlank(controller.getRawData())) { + sb.append("RawData : ").append(controller.getRawData()); + sb.append("\n"); + } + + if (printJwt && controller instanceof JbootController) { + String jwtString = JsonKit.toJson(((JbootController) controller).getJwtParas()); + if (StrUtil.isNotBlank(jwtString)) { + sb.append("Jwt : ").append(jwtString.replace("\n", "")); + sb.append("\n"); + } + } + + appendRenderMessage(controller.getRender(), sb); + + sb.append("----------------------------------- took " + (System.currentTimeMillis() - time) + " ms --------------------------------\n\n\n"); + + writer.write(sb.toString()); + } + + private static void appendRenderMessage(Render render, StringBuilder sb) { + if (render == null) { + return; + } + String view = render.getView(); + if (StrUtil.isNotBlank(view)) { + sb.append("Render : ").append(view); + } else if (render instanceof JsonRender) { + String jsontext = ((JsonRender) render).getJsonText(); + if (jsontext == null) { + jsontext = ""; + } + jsontext = jsontext.replace("\n", ""); + sb.append("Render : ").append(getRenderText(jsontext)); + } else if (render instanceof TextRender) { + String text = ((TextRender) render).getText(); + if (text == null) { + text = ""; + } + text = text.replace("\n", ""); + sb.append("Render : ").append(getRenderText(text)); + } else if (render instanceof FileRender) { + File file = ReflectUtil.getFieldValue(render, "file"); + sb.append("Render : ").append(file); + } else if (render instanceof RedirectRender) { + String url = ReflectUtil.getFieldValue(render, "url"); + sb.append("Redirect : ").append(url); + } else if (render instanceof NullRender) { + sb.append("Render : null"); + } else if (render instanceof JbootReturnValueRender) { + appendRenderMessage(((JbootReturnValueRender) render).getRealRender(), sb); + } else { + sb.append("Render : ").append(ClassUtil.getUsefulClass(render.getClass()).getName()); + } + sb.append("\n"); + } + + + private static String getRenderText(String orignalText) { + if (StrUtil.isBlank(orignalText)) { + return ""; + } + + if (!reportAllText && orignalText.length() > 100) { + return orignalText.substring(0, 100) + "..."; + } + + return orignalText; + } + + + private static String getClassFileName(Class clazz) { + String classFileName = clazz.getName(); + if (classFileName.contains("$")) { + int indexOf = classFileName.contains(".") ? classFileName.lastIndexOf(".") + 1 : 0; + return classFileName.substring(indexOf, classFileName.indexOf("$")); + } else { + return clazz.getSimpleName(); + } + } + + + private static class SystemOutWriter extends Writer { + @Override + public void write(String str) throws IOException { + System.out.print(str); + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + } + + @Override + public void flush() throws IOException { + } + + @Override + public void close() throws IOException { + } + } +} + + diff --git a/src/main/java/io/jboot/web/handler/JbootActionReporterInvocation.java b/src/main/java/io/jboot/web/handler/JbootActionReporterInvocation.java new file mode 100644 index 0000000000000000000000000000000000000000..15b9fc27aa351b077b9f43e667ed6f23fc2ef9ab --- /dev/null +++ b/src/main/java/io/jboot/web/handler/JbootActionReporterInvocation.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.handler; + +import com.jfinal.aop.Interceptor; +import com.jfinal.core.Action; +import com.jfinal.core.Controller; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class JbootActionReporterInvocation extends JbootActionInvocation { + + + protected static ThreadLocal> invokedInterceptors = ThreadLocal.withInitial(ArrayList::new); + protected static ThreadLocal controllerInvokeFlag = ThreadLocal.withInitial(() -> Boolean.FALSE); + + public JbootActionReporterInvocation(Action action, Controller controller) { + super(action, controller); + } + + + @Override + public void invoke() { + if (index < inters.length) { + Interceptor interceptor = inters[index++]; + invokedInterceptors.get().add(interceptor); + interceptor.intercept(this); + } else if (index++ == inters.length) { // index++ ensure invoke action only one time + try { + // Invoke the action + controllerInvokeFlag.set(true); + returnValue = action.getMethod().invoke(target, args); + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + if (t == null) { + t = e; + } + throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t); + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + } + + + public static List getInvokedInterceptor() { + return invokedInterceptors.get(); + } + + public static boolean isControllerInvoked() { + return controllerInvokeFlag.get(); + } + + + public static void clear() { + invokedInterceptors.get().clear(); + invokedInterceptors.remove(); + controllerInvokeFlag.remove(); + } + + public Interceptor[] getInters() { + return inters; + } + + +} diff --git a/src/main/java/io/jboot/web/handler/JbootActionReporterUtil.java b/src/main/java/io/jboot/web/handler/JbootActionReporterUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..1916e3096d127d86a29d734b09e79075553b8047 --- /dev/null +++ b/src/main/java/io/jboot/web/handler/JbootActionReporterUtil.java @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jboot.web.handler; + + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + + +/** + * 参考 https://github.com/apache/dubbo/blob/master/dubbo-common/src/main/java/org/apache/dubbo/common/utils/ReflectUtils.java + */ +public final class JbootActionReporterUtil { + + /** + * void(V). + */ + public static final char JVM_VOID = 'V'; + + /** + * boolean(Z). + */ + public static final char JVM_BOOLEAN = 'Z'; + + /** + * byte(B). + */ + public static final char JVM_BYTE = 'B'; + + /** + * char(C). + */ + public static final char JVM_CHAR = 'C'; + + /** + * double(D). + */ + public static final char JVM_DOUBLE = 'D'; + + /** + * float(F). + */ + public static final char JVM_FLOAT = 'F'; + + /** + * int(I). + */ + public static final char JVM_INT = 'I'; + + /** + * long(J). + */ + public static final char JVM_LONG = 'J'; + + /** + * short(S). + */ + public static final char JVM_SHORT = 'S'; + + + /** + * get class desc. + * boolean[].class => "[Z" + * Object.class => "Ljava/lang/Object;" + * + * @param c class. + * @return desc. + */ + public static String getDesc(Class c) { + StringBuilder ret = new StringBuilder(); + + while (c.isArray()) { + ret.append('['); + c = c.getComponentType(); + } + + if (c.isPrimitive()) { + String t = c.getName(); + if ("void".equals(t)) { + ret.append(JVM_VOID); + } else if ("boolean".equals(t)) { + ret.append(JVM_BOOLEAN); + } else if ("byte".equals(t)) { + ret.append(JVM_BYTE); + } else if ("char".equals(t)) { + ret.append(JVM_CHAR); + } else if ("double".equals(t)) { + ret.append(JVM_DOUBLE); + } else if ("float".equals(t)) { + ret.append(JVM_FLOAT); + } else if ("int".equals(t)) { + ret.append(JVM_INT); + } else if ("long".equals(t)) { + ret.append(JVM_LONG); + } else if ("short".equals(t)) { + ret.append(JVM_SHORT); + } + } else { + ret.append('L'); + ret.append(c.getName().replace('.', '/')); + ret.append(';'); + } + return ret.toString(); + } + + + /** + * get method desc. + * "(I)I", "()V", "(Ljava/lang/String;Z)V" + * + * @param m method. + * @return desc. + */ + public static String getMethodDescWithoutName(Method m) { + StringBuilder ret = new StringBuilder(); + ret.append('('); + Class[] parameterTypes = m.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + ret.append(getDesc(parameterTypes[i])); + } + ret.append(')').append(getDesc(m.getReturnType())); + return ret.toString(); + } + + + /** + * get method string + * + * @param m + * @return + */ + public static String getMethodString(Method m) { + StringBuilder ret = new StringBuilder(m.getName()); + ret.append('('); + Parameter[] parameters = m.getParameters(); + int index = 0; + for (Parameter p : parameters) { + if (!p.isNamePresent()) { + // 必须通过添加 -parameters 进行编译,才可以获取 Parameter 的编译前的名字 + throw new RuntimeException(" Maven or IDE config is error. see https://jfinal.com/doc/3-3 "); + } + ret.append(p.getType().getSimpleName()); + ret.append(" ").append(p.getName()); + if (index++ < parameters.length - 1) { + ret.append(", "); + } + } + + ret.append(')'); + return ret.toString(); + } + + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/handler/JbootHandler.java b/src/main/java/io/jboot/web/handler/JbootHandler.java index 078a9a9d3dc7da4aa72047eaee7f4f9a6fc95994..fe63c5744060544c1474260665c34e6fd59ef067 100644 --- a/src/main/java/io/jboot/web/handler/JbootHandler.java +++ b/src/main/java/io/jboot/web/handler/JbootHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,8 +40,8 @@ public class JbootHandler extends Handler { } } + private void doHandle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { - request.setAttribute(JbootConsts.ATTR_REQUEST, request); request.setAttribute(JbootConsts.ATTR_CONTEXT_PATH, request.getContextPath()); next.handle(target, request, response, isHandled); } diff --git a/src/main/java/io/jboot/web/handler/PathVariableActionHandler.java b/src/main/java/io/jboot/web/handler/PathVariableActionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..cafe31287385fcb94caccf374682fe9105f0623e --- /dev/null +++ b/src/main/java/io/jboot/web/handler/PathVariableActionHandler.java @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.handler; + +import com.google.common.base.Splitter; +import com.jfinal.aop.Invocation; +import com.jfinal.core.Action; +import com.jfinal.core.ActionException; +import com.jfinal.core.CPI; +import com.jfinal.core.Controller; +import com.jfinal.log.Log; +import com.jfinal.render.RenderException; +import io.jboot.components.valid.ValidException; +import io.jboot.web.controller.JbootControllerContext; +import io.jboot.web.session.JbootServletRequestWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +/** + * @author 没牙的小朋友 (mjl@nxu.edu.cn) + * @version V1.0 + */ +public class PathVariableActionHandler extends JbootActionHandler { + + private static final Log LOG = Log.getLog(PathVariableActionHandler.class); + + /** + * handle + * 1: Action action = actionMapping.getAction(target) + * 2: new Invocation(...).invoke() + * 3: render(...) + */ + @Override + public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { + if (target.lastIndexOf('.') != -1) { + return; + } + + isHandled[0] = true; + //urlPara数组增加第2个元素存储路径参数 + String[] urlPara = {null, null}; + Action action = getAction(target, urlPara, request); + + if (action == null) { + if (LOG.isWarnEnabled()) { + String qs = request.getQueryString(); + LOG.warn("404 Action Not Found: " + (qs == null ? target : target + "?" + qs)); + } + renderManager.getRenderFactory().getErrorRender(404).setContext(request, response).render(); + return; + } + + + Controller controller = null; + try { + controller = controllerFactory.getController(action.getControllerClass()); + //controller.init(request, response, urlPara[0]); + //存在封装的路径参数 + if (urlPara[1] != null) { + Map params = Splitter.on("&").withKeyValueSeparator("=").split(urlPara[1]); + PathVariableWrappedRequest wrappedRequest = new PathVariableWrappedRequest(request, response, params); + CPI._init_(controller, action, wrappedRequest, response, urlPara[0]); + } else { + CPI._init_(controller, action, request, response, urlPara[0]); + } + JbootControllerContext.hold(controller); + + //Invocation invocation = new Invocation(action, controller); + Invocation invocation = getInvocation(action, controller); + + if (JbootActionReporter.isReportEnable()) { + long time = System.currentTimeMillis(); + try { + doStartRender(target, action, controller, invocation, isHandled); + } finally { + JbootActionReporter.report(target, controller, action, invocation, time); + } + } else { + doStartRender(target, action, controller, invocation, isHandled); + } + + doAfterRender(action, controller); + + } catch (RenderException e) { + if (LOG.isErrorEnabled()) { + String qs = request.getQueryString(); + LOG.error(qs == null ? target : target + "?" + qs, e); + } + } catch (ActionException e) { + handleActionException(target, request, response, action, e); + } catch (ValidException e) { + handleValidException(target, request, response, action, e); + } catch (Exception e) { + handleException(target, request, response, action, e); + } finally { + JbootControllerContext.release(); + controllerFactory.recycle(controller); + } + } + + +// private void doStartRender(String target +// , HttpServletRequest request +// , HttpServletResponse response +// , boolean[] isHandled +// , Action action +// , Controller controller +// , Invocation invocation) { +// +// invocation.invoke(); +// +// Render render = controller.getRender(); +// if (render instanceof ForwardActionRender) { +// String actionUrl = ((ForwardActionRender) render).getActionUrl(); +// if (target.equals(actionUrl)) { +// throw new RuntimeException("The forward action url is the same as before."); +// } else { +// handle(actionUrl, request, response, isHandled); +// } +// } else { +// if (render == null && void.class != action.getMethod().getReturnType() +// && renderManager.getRenderFactory() instanceof JbootRenderFactory) { +// +// JbootRenderFactory factory = (JbootRenderFactory) renderManager.getRenderFactory(); +// JbootReturnValueRender returnValueRender = factory.getReturnValueRender(action, invocation.getReturnValue()); +// String forwardTo = returnValueRender.getForwardTo(); +// if (forwardTo != null) { +// handle(getRealForwrdTo(forwardTo, target, action), request, response, isHandled); +// return; +// } else { +// render = returnValueRender; +// } +// } +// +// if (render == null) { +// render = renderManager.getRenderFactory().getDefaultRender(action.getViewPath() + action.getMethodName()); +// } +// +// render.setContext(request, response, action.getViewPath()).render(); +// } +// } + + /** + * 请求包装类用于将路径变量的URL中的额外参数加入request中 + */ + private class PathVariableWrappedRequest extends JbootServletRequestWrapper { + private final Map modifiableParameters; + private Map allParameters = null; + + public PathVariableWrappedRequest(HttpServletRequest request, HttpServletResponse response, + Map params) { + super(request, response); + modifiableParameters = new TreeMap<>(); + params.keySet().forEach(k -> { + modifiableParameters.put(k, new String[]{params.get(k)}); + }); + } + + @Override + public Map getParameterMap() { + if (allParameters == null) { + allParameters = new TreeMap(); + allParameters.putAll(super.getParameterMap()); + allParameters.putAll(modifiableParameters); + } + return Collections.unmodifiableMap(allParameters); + } + + @Override + public String getParameter(final String name) { + String[] strings = getParameterMap().get(name); + if (strings != null) { + return strings[0]; + } + return super.getParameter(name); + } + } +} diff --git a/src/main/java/io/jboot/web/json/JbootJson.java b/src/main/java/io/jboot/web/json/JbootJson.java new file mode 100644 index 0000000000000000000000000000000000000000..d09cb55a5b3cacd4d97d7f5cc1b2be6555fb90bc --- /dev/null +++ b/src/main/java/io/jboot/web/json/JbootJson.java @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.json; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.annotation.JSONField; +import com.jfinal.json.JFinalJson; +import com.jfinal.json.JFinalJsonKit; +import com.jfinal.kit.LogKit; +import com.jfinal.kit.StrKit; +import com.jfinal.plugin.activerecord.CPI; +import com.jfinal.plugin.activerecord.Model; +import io.jboot.Jboot; +import io.jboot.db.model.JbootModel; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.StrUtil; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + + +public class JbootJson extends JFinalJson { + + private JbootJsonConfig config = Jboot.config(JbootJsonConfig.class); + protected static Map, MethodsAndFieldsWrapper> methodAndFieldsCache = new HashMap<>(); + + public JbootJson() { + + //跳过 null 值输出到浏览器,提高传输性能 + setSkipNullValueField(config.isSkipNullValueField()); + + //设置转换层级 + setConvertDepth(config.getDepth()); + + //默认设置为 CamelCase 的属性模式 + if (config.isCamelCaseJsonStyleEnable()) { + setModelAndRecordFieldNameConverter((fieldName) -> StrKit.toCamelCase(fieldName, config.isCamelCaseToLowerCaseAnyway())); + } + + setToJsonFactory(o -> o instanceof Model ? jbootModelToJson : null); + + if (StrUtil.isNotBlank(config.getTimestampPattern())) { + setTimestampPattern(config.getTimestampPattern()); + } + } + + + protected JFinalJsonKit.ToJson> jbootModelToJson = (model, depth, ret) -> { + if (JFinalJsonKit.checkDepth(depth--, ret)) { + return; + } + + Map map = new HashMap<>(); + + if (!config.isSkipModelAttrs()) { + fillModelAttrsToMap(CPI.getAttrs(model), map); + } + + if (!config.isSkipBeanGetters()) { + fillBeanToMap(model, map); + } + + optimizeMapAttrs(map); + + JFinalJsonKit.mapToJson(map, depth, ret); + }; + + + protected void fillModelAttrsToMap(Map attrs, Map toMap) { + if (attrs != null && !attrs.isEmpty()) { + for (Map.Entry entry : attrs.entrySet()) { + String fieldName = entry.getKey(); + if (config.isCamelCaseJsonStyleEnable()) { + fieldName = StrKit.toCamelCase(fieldName, config.isCamelCaseToLowerCaseAnyway()); + } + toMap.put(fieldName, entry.getValue()); + } + } + } + + + protected void fillBeanToMap(Object bean, Map toMap) { + + MethodsAndFieldsWrapper wrapper = methodAndFieldsCache.get(bean.getClass()); + if (wrapper == null) { + synchronized (this) { + if (wrapper == null) { + wrapper = new MethodsAndFieldsWrapper(bean.getClass()); + } else { + wrapper = methodAndFieldsCache.get(bean.getClass()); + } + } + } + + for (String ignoreField : wrapper.ignoreFields) { + toMap.remove(ignoreField); + } + + + for (int i = 0; i < wrapper.fields.size(); i++) { + String originalField = wrapper.originalFields.get(i); + toMap.remove(originalField); + + Object value = invokeGetterMethod(wrapper.getterMethods.get(i), bean); + String field = wrapper.fields.get(i); + toMap.put(field, value); + } + + } + + + protected void optimizeMapAttrs(Map map) { + } + + protected Object invokeGetterMethod(Method method, Object bean) { + try { + return method.invoke(bean); + } catch (Exception ex) { + LogKit.error("can not invoke method: " + ClassUtil.buildMethodString(method), ex); + return null; + } + } + + + @Override + public T parse(String jsonString, Class type) { + return JSON.parseObject(jsonString, type); + } + + + public static class MethodsAndFieldsWrapper { + + + private static boolean hasFastJson = ClassUtil.hasClass("com.alibaba.fastjson.JSON"); + + private List fields = new LinkedList<>(); + private List getterMethods = new LinkedList<>(); + private List originalFields = new LinkedList<>(); + + //需要忽略的字段 + private List ignoreFields = new ArrayList<>(); + + public MethodsAndFieldsWrapper(Class reflectiveClass) { + + Method[] methodArray = reflectiveClass.getMethods(); + for (Method method : methodArray) { + if (method.getParameterCount() != 0 + || method.getReturnType() == void.class + || !Modifier.isPublic(method.getModifiers()) + || "getClass".equals(method.getName()) + || method.getDeclaringClass() == JbootModel.class + || method.getDeclaringClass() == Model.class + || method.getDeclaringClass() == Object.class + ) { + continue; + } + + + String fieldName = getGetterMethodField(method.getName()); + if (fieldName != null) { + String attrName = StrKit.firstCharToLowerCase(fieldName); + if (isIgnoreFiled(method)) { + ignoreFields.add(attrName); + } else { + originalFields.add(attrName); + fields.add(getDefineName(method, attrName)); + getterMethods.add(method); + } + } + + } + } + + private String getGetterMethodField(String methodName) { + if (methodName.startsWith("get") && methodName.length() > 3) { + return methodName.substring(3); + } else if (methodName.startsWith("is") && methodName.length() > 2) { + return methodName.substring(2); + } + return null; + } + + + private String getDefineName(Method method, String orginalName) { + if (hasFastJson) { + JSONField jsonField = method.getAnnotation(JSONField.class); + if (jsonField != null && StrUtil.isNotBlank(jsonField.name())) { + return jsonField.name(); + } + } + return orginalName; + } + + private boolean isIgnoreFiled(Method method) { + if (hasFastJson) { + JSONField jsonField = method.getAnnotation(JSONField.class); + if (jsonField != null && !jsonField.serialize()) { + return true; + } + } + return method.getAnnotation(JsonIgnore.class) != null; + } + } +} diff --git a/src/main/java/io/jboot/web/json/JbootJsonConfig.java b/src/main/java/io/jboot/web/json/JbootJsonConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..738c10c541f774103ba37663b094e7cf79811f0c --- /dev/null +++ b/src/main/java/io/jboot/web/json/JbootJsonConfig.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.json; + +import io.jboot.app.config.annotation.ConfigModel; + +/** + * @author Michael Yang 杨福海 (fuhai999@gmail.com) + * @version V1.0 + */ +@ConfigModel(prefix = "jboot.json") +public class JbootJsonConfig { + + //model 和 record 是否自动转为驼峰格式 + //当配置此项为 false 的时候,需要配置 skipBeanGetters 为 true,否则出现个别相同数据库字段输出两次的情况 + private boolean camelCaseJsonStyleEnable = true; + + //是否把所有的信息都转为驼峰格式,包括 map + private boolean camelCaseToLowerCaseAnyway = false; + + //是否跳过 null 值 + private boolean skipNullValueField = true; + + //是否跳过 model attrs,而只有使用 getter 来生成 + private boolean skipModelAttrs = false; + + //是否跳过 getter 方法 + private boolean skipBeanGetters = false; + + //时间格式化 + private String timestampPattern; + + //转换深度,防止且套引用导致死循环转换 + private int depth = 16; + + public boolean isCamelCaseJsonStyleEnable() { + return camelCaseJsonStyleEnable; + } + + public void setCamelCaseJsonStyleEnable(boolean camelCaseJsonStyleEnable) { + this.camelCaseJsonStyleEnable = camelCaseJsonStyleEnable; + } + + public boolean isCamelCaseToLowerCaseAnyway() { + return camelCaseToLowerCaseAnyway; + } + + public void setCamelCaseToLowerCaseAnyway(boolean camelCaseToLowerCaseAnyway) { + this.camelCaseToLowerCaseAnyway = camelCaseToLowerCaseAnyway; + } + + public boolean isSkipNullValueField() { + return skipNullValueField; + } + + public void setSkipNullValueField(boolean skipNullValueField) { + this.skipNullValueField = skipNullValueField; + } + + public boolean isSkipModelAttrs() { + return skipModelAttrs; + } + + public void setSkipModelAttrs(boolean skipModelAttrs) { + this.skipModelAttrs = skipModelAttrs; + } + + public boolean isSkipBeanGetters() { + return skipBeanGetters; + } + + public void setSkipBeanGetters(boolean skipBeanGetters) { + this.skipBeanGetters = skipBeanGetters; + } + + public String getTimestampPattern() { + return timestampPattern; + } + + public void setTimestampPattern(String timestampPattern) { + this.timestampPattern = timestampPattern; + } + + public int getDepth() { + return depth; + } + + public void setDepth(int depth) { + this.depth = depth; + } +} diff --git a/src/main/java/io/jboot/web/json/JsonBody.java b/src/main/java/io/jboot/web/json/JsonBody.java new file mode 100644 index 0000000000000000000000000000000000000000..efc9d73e5c47ed895b7a0e95eea66c5aff451005 --- /dev/null +++ b/src/main/java/io/jboot/web/json/JsonBody.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.json; + +import java.lang.annotation.*; + +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +public @interface JsonBody { + + /** + * 自定义前缀 + */ + String value() default ""; + + /** + * 是否跳过转换异常 + * @return + */ + boolean skipConvertError() default false; + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/json/JsonBodyParseInterceptor.java b/src/main/java/io/jboot/web/json/JsonBodyParseInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..87ad702351f987f4104cdca6fa970eb4d6296ca2 --- /dev/null +++ b/src/main/java/io/jboot/web/json/JsonBodyParseInterceptor.java @@ -0,0 +1,309 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.json; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.core.ActionException; +import com.jfinal.core.Controller; +import com.jfinal.kit.LogKit; +import com.jfinal.render.RenderManager; +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.utils.ClassUtil; +import io.jboot.utils.ObjectUtil; +import io.jboot.utils.StrUtil; +import io.jboot.web.controller.JbootController; + +import java.lang.reflect.*; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@AutoLoad +public class JsonBodyParseInterceptor implements Interceptor, InterceptorBuilder { + + private static final String startOfArray = "["; + private static final String endOfArray = "]"; + + @Override + public void intercept(Invocation inv) { + + Controller controller = inv.getController(); + Method method = inv.getMethod(); + Parameter[] parameters = method.getParameters(); + Type[] paraTypes = method.getGenericParameterTypes(); + + Object jsonObjectOrArray = StrUtil.isBlank(controller.getRawData()) ? null : JSON.parse(controller.getRawData()); + + for (int index = 0; index < parameters.length; index++) { + JsonBody jsonBody = parameters[index].getAnnotation(JsonBody.class); + if (jsonBody != null) { + Class paraClass = parameters[index].getType(); + Object result = null; + try { + Type paraType = paraTypes[index]; + if (paraType instanceof TypeVariable) { + Type variableRawType = getTypeVariableRawType(controller.getClass(), ((TypeVariable) paraType)); + if (variableRawType != null) { + paraClass = (Class) variableRawType; + paraType = variableRawType; + } + } + result = parseJsonBody(jsonObjectOrArray, paraClass, paraType, jsonBody.value()); + } catch (Exception e) { + String message = "Can not parse \"" + parameters[index].getType() + + "\" in method " + ClassUtil.buildMethodString(method) + ", Cause: " + e.getMessage(); + if (jsonBody.skipConvertError()) { + LogKit.error(message); + } else { + throw new ActionException(400, RenderManager.me().getRenderFactory().getErrorRender(400), message); + } + } + + inv.setArg(index, result); + } + } + + inv.invoke(); + } + + + /** + * 获取方法里的泛型参数 T 对于的真实的 Class 类 + * + * @param defClass + * @param typeVariable + * @return + */ + private static Type getTypeVariableRawType(Class defClass, TypeVariable typeVariable) { + Type type = defClass.getGenericSuperclass(); + if (type instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + if (typeArguments.length == 1) { + return typeArguments[0]; + } else if (typeArguments.length > 1) { + TypeVariable[] typeVariables = typeVariable.getGenericDeclaration().getTypeParameters(); + for (int i = 0; i < typeVariables.length; i++) { + if (typeVariable.getName().equals(typeVariables[i].getName())) { + return typeArguments[i]; + } + } + } + } + return null; + } + + + public static Object parseJsonBody(Object jsonObjectOrArray, Class paraClass, Type paraType, String jsonKey) throws InstantiationException, IllegalAccessException { + if (jsonObjectOrArray == null) { + return paraClass.isPrimitive() ? ObjectUtil.getPrimitiveDefaultValue(paraClass) : null; + } + if (Collection.class.isAssignableFrom(paraClass) || paraClass.isArray()) { + return parseArray(jsonObjectOrArray, paraClass, paraType, jsonKey); + } else { + return parseObject((JSONObject) jsonObjectOrArray, paraClass, paraType, jsonKey); + } + } + + + private static Object parseObject(JSONObject rawObject, Class paraClass, Type paraType, String jsonKey) throws IllegalAccessException, InstantiationException { + if (StrUtil.isBlank(jsonKey)) { + return toJavaObject(rawObject, paraClass, paraType); + } + + Object result = null; + String[] keys = jsonKey.split("\\."); + for (int i = 0; i < keys.length; i++) { + if (rawObject != null && !rawObject.isEmpty()) { + String key = keys[i].trim(); + if (StrUtil.isNotBlank(key)) { + //the last + if (i == keys.length - 1) { + if (key.endsWith(endOfArray) && key.contains(startOfArray)) { + String realKey = key.substring(0, key.indexOf(startOfArray)); + JSONArray jarray = rawObject.getJSONArray(realKey.trim()); + if (jarray != null && jarray.size() > 0) { + String arrayString = key.substring(key.indexOf(startOfArray) + 1, key.length() - 1); + int arrayIndex = StrUtil.isBlank(arrayString) ? 0 : Integer.parseInt(arrayString.trim()); + result = arrayIndex >= jarray.size() ? null : jarray.get(arrayIndex); + } + } else { + result = rawObject.get(key); + } + } + //not last + else { + rawObject = getJSONObjectByKey(rawObject, key); + } + } + } + } + + if (result == null || StrUtil.EMPTY.equals(result)) { + return paraClass.isPrimitive() ? ObjectUtil.getPrimitiveDefaultValue(paraClass) : null; + } + + if (paraClass == String.class && paraClass == paraType) { + return result.toString(); + } + + // JSONObject 类型 + if (result instanceof JSONObject) { + return toJavaObject((JSONObject) result, paraClass, paraType); + } + + return ObjectUtil.convert(result, paraClass); + } + + + private static Object parseArray(Object rawJsonObjectOrArray, Class typeClass, Type type, String jsonKey) { + JSONArray jsonArray = null; + if (StrUtil.isBlank(jsonKey)) { + if (rawJsonObjectOrArray instanceof JSONArray) { + jsonArray = (JSONArray) rawJsonObjectOrArray; + } + } else { + if (rawJsonObjectOrArray instanceof JSONObject) { + JSONObject rawObject = (JSONObject) rawJsonObjectOrArray; + String[] keys = jsonKey.split("\\."); + for (int i = 0; i < keys.length; i++) { + if (rawObject == null || rawObject.isEmpty()) { + break; + } + String key = keys[i].trim(); + if (StrUtil.isNotBlank(key)) { + //the last + if (i == keys.length - 1) { + if (key.endsWith(endOfArray) && key.contains(startOfArray)) { + String realKey = key.substring(0, key.indexOf(startOfArray)); + JSONArray jarray = rawObject.getJSONArray(realKey.trim()); + if (jarray == null || jarray.isEmpty()) { + return null; + } + String subKey = key.substring(key.indexOf(startOfArray) + 1, key.length() - 1).trim(); + if (StrUtil.isBlank(subKey)) { + throw new IllegalStateException("Sub key can not empty: " + jsonKey); + } + + JSONArray newJsonArray = new JSONArray(); + for (int j = 0; j < jarray.size(); j++) { + Object value = jarray.getJSONObject(j).get(subKey); + if (value != null) { + newJsonArray.add(value); + } + } + jsonArray = newJsonArray; + } else { + jsonArray = rawObject.getJSONArray(key); + } + } + //not last + else { + rawObject = getJSONObjectByKey(rawObject, key); + } + } + } + } + } + + if (jsonArray == null || jsonArray.isEmpty()) { + return null; + } + + //非泛型 set + if ((typeClass == Set.class || typeClass == HashSet.class) && typeClass == type) { + return new HashSet<>(jsonArray); + } + + //直接获取 JsonArray + if (typeClass == type && typeClass == JSONArray.class) { + return jsonArray; + } + + return jsonArray.toJavaObject(type); + } + + + private static JSONObject getJSONObjectByKey(JSONObject jsonObject, String key) { + if (key.endsWith(endOfArray) && key.contains(startOfArray)) { + String realKey = key.substring(0, key.indexOf(startOfArray)); + JSONArray jarray = jsonObject.getJSONArray(realKey.trim()); + if (jarray == null || jarray.isEmpty()) { + return null; + } + String arrayString = key.substring(key.indexOf(startOfArray) + 1, key.length() - 1); + int arrayIndex = StrUtil.isBlank(arrayString) ? 0 : Integer.parseInt(arrayString.trim()); + return arrayIndex >= jarray.size() ? null : jarray.getJSONObject(arrayIndex); + } else { + return jsonObject.getJSONObject(key); + } + } + + + private static Object toJavaObject(JSONObject rawObject, Class paraClass, Type paraType) throws IllegalAccessException, InstantiationException { + if (rawObject.isEmpty()) { + return paraClass.isPrimitive() ? ObjectUtil.getPrimitiveDefaultValue(paraClass) : null; + } + + //非泛型 的 map + if ((paraClass == Map.class || paraClass == JSONObject.class) && paraClass == paraType) { + return rawObject; + } + + //非泛型 的 map + if (Map.class.isAssignableFrom(paraClass) && paraClass == paraType && canNewInstance(paraClass)) { + Map map = (Map) paraClass.newInstance(); + map.putAll(rawObject); + return map; + } + + return rawObject.toJavaObject(paraType); + } + + + private static boolean canNewInstance(Class clazz) { + int modifiers = clazz.getModifiers(); + return !Modifier.isAbstract(modifiers) && !Modifier.isInterface(modifiers); + } + + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.isController(targetClass)) { + Parameter[] parameters = method.getParameters(); + if (parameters != null && parameters.length > 0) { + for (Parameter p : parameters) { + if (p.getAnnotation(JsonBody.class) != null) { + Class typeClass = p.getType(); + if ((Map.class.isAssignableFrom(typeClass) || Collection.class.isAssignableFrom(typeClass) || typeClass.isArray()) + && !JbootController.class.isAssignableFrom(targetClass)) { + throw new IllegalArgumentException("Can not use @JsonBody for Map/List(Collection)/Array type if your controller not extends JbootController, method: " + ClassUtil.buildMethodString(method)); + } + + interceptors.addIfNotExist(this); + return; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/json/JsonIgnore.java b/src/main/java/io/jboot/web/json/JsonIgnore.java new file mode 100644 index 0000000000000000000000000000000000000000..944028e86969508b8faf51c9dfd01dfa2550a431 --- /dev/null +++ b/src/main/java/io/jboot/web/json/JsonIgnore.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.json; + +import java.lang.annotation.*; + +/** + * @author michael yang + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface JsonIgnore { + +} \ No newline at end of file diff --git a/src/main/java/io/jboot/web/render/JbootCaptchaRender.java b/src/main/java/io/jboot/web/render/JbootCaptchaRender.java new file mode 100644 index 0000000000000000000000000000000000000000..91c9be3a9e8fa94b09aa456230349d2df3e72d79 --- /dev/null +++ b/src/main/java/io/jboot/web/render/JbootCaptchaRender.java @@ -0,0 +1,99 @@ +package io.jboot.web.render; + +import com.jfinal.captcha.CaptchaRender; +import com.jfinal.kit.StrKit; + +import java.awt.*; +import java.awt.geom.QuadCurve2D; +import java.awt.image.BufferedImage; +import java.util.concurrent.ThreadLocalRandom; + +public class JbootCaptchaRender extends CaptchaRender { + + // 验证码随机字符数组 + protected static char[] charArray = "1234567890ABCDEFGHJKMNPQRSTUVWXY".toCharArray(); + + + /** + * @param randomArrayString + */ + public static void setRandomArrayString(String randomArrayString) { + if (StrKit.isBlank(randomArrayString)) { + throw new IllegalArgumentException("randomArrayString can not be blank."); + } + charArray = randomArrayString.toCharArray(); + } + + + @Override + protected String getRandomString() { + char[] randomChars = new char[4]; + for (int i = 0; i < randomChars.length; i++) { + randomChars[i] = charArray[ThreadLocalRandom.current().nextInt(charArray.length)]; + } + return String.valueOf(randomChars); + } + + + @Override + protected void drawGraphic(String randomString, BufferedImage image) { + // 获取图形上下文 + Graphics2D g = image.createGraphics(); + + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + // 图形抗锯齿 + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + // 字体抗锯齿 + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + ThreadLocalRandom random = ThreadLocalRandom.current(); + + // 设定背景色 + g.setColor(getRandomColor(210, 250, random)); + g.fillRect(0, 0, WIDTH, HEIGHT); + + //绘制小字符背景 + Color color = null; + for (int i = 0; i < 20; i++) { + color = getRandomColor(120, 200, random); + g.setColor(color); + String rand = String.valueOf(charArray[random.nextInt(charArray.length)]); + g.drawString(rand, random.nextInt(WIDTH), random.nextInt(HEIGHT)); + color = null; + } + + //设定字体 + g.setFont(RANDOM_FONT[random.nextInt(RANDOM_FONT.length)]); + // 绘制验证码 + for (int i = 0; i < randomString.length(); i++) { + //旋转度数 最好小于45度 + int degree = random.nextInt(28); + if (i % 2 == 0) { + degree = degree * (-1); + } + //定义坐标 + int x = 22 * i, y = 21; + //旋转区域 + g.rotate(Math.toRadians(degree), x, y); + //设定字体颜色 + color = getRandomColor(20, 130, random); + g.setColor(color); + //将认证码显示到图象中 + g.drawString(String.valueOf(randomString.charAt(i)), x + 8, y + 10); + //旋转之后,必须旋转回来 + g.rotate(-Math.toRadians(degree), x, y); + } + //图片中间曲线,使用上面缓存的color + g.setColor(color); + //width是线宽,float型 + BasicStroke bs = new BasicStroke(3); + g.setStroke(bs); + //画出曲线 + QuadCurve2D.Double curve = new QuadCurve2D.Double(0d, random.nextInt(HEIGHT - 8) + 4, WIDTH / 2, HEIGHT / 2, WIDTH, random.nextInt(HEIGHT - 8) + 4); + g.draw(curve); + // 销毁图像 + g.dispose(); + } + + +} diff --git a/src/main/java/io/jboot/web/render/JbootErrorRender.java b/src/main/java/io/jboot/web/render/JbootErrorRender.java index 988225a919e0a1b9004e4d258b5611671a3a9549..fef1907da4d45cf0442f5f57ea8291d14a430411 100644 --- a/src/main/java/io/jboot/web/render/JbootErrorRender.java +++ b/src/main/java/io/jboot/web/render/JbootErrorRender.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,16 @@ */ package io.jboot.web.render; +import com.jfinal.kit.JsonKit; +import com.jfinal.kit.Ret; import com.jfinal.render.Render; import com.jfinal.render.RenderException; import com.jfinal.render.RenderManager; import io.jboot.exception.JbootExceptionHolder; +import io.jboot.utils.RequestUtil; +import io.jboot.utils.StrUtil; +import io.jboot.components.valid.ValidException; -import java.io.IOException; import java.io.PrintWriter; import java.util.List; @@ -29,7 +33,8 @@ import java.util.List; */ public class JbootErrorRender extends Render { - protected static final String contentType = "text/html; charset=" + getEncoding(); + protected static final String htmlContentType = "text/html;charset=" + getEncoding(); + protected static final String jsonContentType = "application/json;charset=" + getEncoding(); protected static final String poweredBy = "

Powered by Jboot
"; @@ -40,10 +45,45 @@ public class JbootErrorRender extends Render { protected static final String html500_header = "500 Internal Server Error" + "

500 Internal Server Error

" + "
"; + protected static final String html400_header = "400 Internal Server Error" + + "

400 Internal Server Error

" + + "
"; protected static final String html500_footer = "
" + poweredBy + ""; + + protected static final String json401 = JsonKit.toJson(Ret.fail().set("errorCode", 401).set("message", "401 Unauthorized")); + protected static final String json403 = JsonKit.toJson(Ret.fail().set("errorCode", 403).set("message", "403 Forbidden")); + protected static final String json404 = JsonKit.toJson(Ret.fail().set("errorCode", 404).set("message", "404 Not Found")); + + protected int errorCode; + protected String message; + protected Throwable throwable; + + public int getErrorCode() { + return errorCode; + } + + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Throwable getThrowable() { + return throwable; + } + + public void setThrowable(Throwable throwable) { + this.throwable = throwable; + } public JbootErrorRender(int errorCode, String view) { this.errorCode = errorCode; @@ -65,38 +105,38 @@ public class JbootErrorRender extends Render { } try { - response.setContentType(contentType); + boolean needRenderJson = RequestUtil.isJsonContentType(request) || RequestUtil.isAjaxRequest(request); + response.setContentType(needRenderJson ? jsonContentType : htmlContentType); PrintWriter writer = response.getWriter(); - writer.write(getErrorHtml()); - } catch (IOException e) { - throw new RenderException(e); + writer.write(needRenderJson ? getErrorJson() : getErrorHtml()); + } catch (Exception ex) { + throw new RenderException(ex); } + } + public String getErrorHtml() { int errorCode = getErrorCode(); - if (errorCode == 404) { + if (throwable instanceof ValidException) { + return buildErrorInfo(html400_header); + } else if (errorCode == 404) { return html404; - } - if (errorCode == 401) { + } else if (errorCode == 401) { return html401; - } - if (errorCode == 403) { + } else if (errorCode == 403) { return html403; - } - if (errorCode == 500) { - return build500ErrorInfo(); + } else if (errorCode == 400) { + return buildErrorInfo(html400_header); + } else if (errorCode == 500) { + return buildErrorInfo(html500_header); } return "" + errorCode + " Error

" + errorCode + " Error


" + poweredBy + ""; } - public int getErrorCode() { - return errorCode; - } - - public String build500ErrorInfo() { - StringBuilder stringBuilder = new StringBuilder(html500_header); + public String buildErrorInfo(String headerHtml) { + StringBuilder stringBuilder = new StringBuilder(headerHtml); List messages = JbootExceptionHolder.getMessages(); for (String message : messages) { @@ -116,6 +156,57 @@ public class JbootErrorRender extends Render { return stringBuilder.append(html500_footer).toString(); } + + + public String getErrorJson() { + int errorCode = getErrorCode(); + if (throwable instanceof ValidException || errorCode == 500 || errorCode == 400) { + return buildErrorJson(); + } else if (errorCode == 404) { + return json404; + } else if (errorCode == 401) { + return json401; + } else if (errorCode == 403) { + return json403; + } + return JsonKit.toJson(Ret.fail().set("errorCode", errorCode).set("message", errorCode + " Error")); + } + + + public String buildErrorJson() { + + Ret ret = Ret.fail().set("errorCode", getErrorCode()).set("message", getErrorCode() + " Internal Server Error"); + + StringBuilder errorMsgBuilder = new StringBuilder(); + List messages = JbootExceptionHolder.getMessages(); + for (String message : messages) { + errorMsgBuilder.append(message); + } + + ret.set("errorMessage", errorMsgBuilder.toString()); + + List throwables = JbootExceptionHolder.getThrowables(); + if (throwables.size() > 0) { + Throwable throwable = throwables.get(0); + ret.set("throwable", throwable.getClass().getName() + ": " + throwable.getMessage()); + ret.set("message", throwable.getMessage()); + } + + if (this.throwable != null) { + ret.set("throwable", this.throwable.getClass().getName() + ": " + this.throwable.getMessage()); + ret.set("message", throwable.getMessage()); + + if (throwable instanceof ValidException) { + ret.set("errorMessage", ((ValidException) throwable).getReason()); + } + } + + if (StrUtil.isNotBlank(this.message)) { + ret.set("message", this.message); + } + + return JsonKit.toJson(ret); + } } diff --git a/src/main/java/io/jboot/web/render/JbootHtmlRender.java b/src/main/java/io/jboot/web/render/JbootHtmlRender.java index caf3e503efd38b1aae420b57f9df0bfa79ba20d2..1595a913cfee2f91c9ff54c308e8c0e30092f1e0 100644 --- a/src/main/java/io/jboot/web/render/JbootHtmlRender.java +++ b/src/main/java/io/jboot/web/render/JbootHtmlRender.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.jfinal.render.ContentType; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.render */ public class JbootHtmlRender extends JbootTextRender { public JbootHtmlRender(String text) { diff --git a/src/main/java/io/jboot/web/render/JbootJavascriptRender.java b/src/main/java/io/jboot/web/render/JbootJavascriptRender.java index 12e2e059ae7669c1301b2e38c60f296d57002166..799b026c0e0b3b20e7318098f2b0a8fd7640d154 100644 --- a/src/main/java/io/jboot/web/render/JbootJavascriptRender.java +++ b/src/main/java/io/jboot/web/render/JbootJavascriptRender.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.jfinal.render.ContentType; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.render */ public class JbootJavascriptRender extends JbootTextRender { public JbootJavascriptRender(String text) { diff --git a/src/main/java/io/jboot/web/render/JbootJsonRender.java b/src/main/java/io/jboot/web/render/JbootJsonRender.java index 8f3adc0bec1146cd094cdbffc3f541def555feef..e7dc1cdbc0cab22c6db60ee7f21f5843c974da8e 100644 --- a/src/main/java/io/jboot/web/render/JbootJsonRender.java +++ b/src/main/java/io/jboot/web/render/JbootJsonRender.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,10 @@ import io.jboot.JbootConsts; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.render */ public class JbootJsonRender extends JsonRender { static { - excludedAttrs.add(JbootConsts.ATTR_REQUEST); excludedAttrs.add(JbootConsts.ATTR_CONTEXT_PATH); } diff --git a/src/main/java/io/jboot/web/render/JbootRedirect301Render.java b/src/main/java/io/jboot/web/render/JbootRedirect301Render.java new file mode 100644 index 0000000000000000000000000000000000000000..5fbc98a62098b6c26cc6471dc53410b23fc4f007 --- /dev/null +++ b/src/main/java/io/jboot/web/render/JbootRedirect301Render.java @@ -0,0 +1,31 @@ +package io.jboot.web.render; + +import javax.servlet.http.HttpServletResponse; + +/** + * Redirect301Render. + */ +public class JbootRedirect301Render extends JbootRedirectRender { + + public JbootRedirect301Render(String url) { + super(url); + } + + public JbootRedirect301Render(String url, boolean withQueryString) { + super(url, withQueryString); + } + + @Override + public void render() { + String finalUrl = buildFinalUrl(); + + response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); + response.setHeader("Location", finalUrl); + } +} + + + + + + diff --git a/src/main/java/io/jboot/web/render/JbootRedirectRender.java b/src/main/java/io/jboot/web/render/JbootRedirectRender.java new file mode 100644 index 0000000000000000000000000000000000000000..bdbda9ff17c5250874fbd60c5215c7275d24481f --- /dev/null +++ b/src/main/java/io/jboot/web/render/JbootRedirectRender.java @@ -0,0 +1,92 @@ +package io.jboot.web.render; + +import com.jfinal.kit.StrKit; +import com.jfinal.render.RedirectRender; + +/** + * RedirectRender with status: 302 Found. + * + * + * 注意:使用 nginx 代理实现 https 的场景,解决 https 被重定向到了 http 的问题,需要在 nginx 中添加如下配置: + * proxy_set_header X-Forwarded-Proto $scheme; + * proxy_set_header X-Forwarded-Port $server_port; + * + * + * PS:nginx 将 http 重定向到 https 的配置为: + * proxy_redirect http:// https://; + * 注意: 需要同时支持 http 与 https 的场景不能使用该配置 + * + */ +public class JbootRedirectRender extends RedirectRender { + + public JbootRedirectRender(String url) { + super(url); + } + + public JbootRedirectRender(String url, boolean withQueryString) { + super(url, withQueryString); + } + + @Override + public String buildFinalUrl() { + String ret; + // 如果一个url为/login/connect?goto=http://www.jfinal.com,则有错误 + // ^((https|http|ftp|rtsp|mms)?://)$ ==> indexOf 取值为 (3, 5) + if (contextPath != null && (url.indexOf("://") == -1 || url.indexOf("://") > 5)) { + ret = contextPath + url; + } else { + ret = url; + } + + if (withQueryString) { + String queryString = request.getQueryString(); + if (queryString != null) { + if (ret.indexOf('?') == -1) { + ret = ret + "?" + queryString; + } else { + ret = ret + "&" + queryString; + } + } + } + + // 跳过 http/https 已指定过协议类型的 url,用于支持跨域名重定向 + if (ret.toLowerCase().startsWith("http")) { + return ret; + } + + /** + * 注意:nginx 代理 https 的场景,需要使用如下配置: + * proxy_set_header X-Forwarded-Proto $scheme; + * proxy_set_header X-Forwarded-Port $server_port; + */ + if ("https".equalsIgnoreCase(request.getHeader("X-Forwarded-Proto"))) { + String serverName = request.getServerName(); + + /** + * 获取 nginx 端通过配置 proxy_set_header X-Forwarded-Port $server_port; + * 传递过来的端口号,保障重定向时端口号是正确的 + */ + String port = request.getHeader("X-Forwarded-Port"); + if (StrKit.notBlank(port)) { + serverName = serverName + ":" + port; + } + + if (ret.charAt(0) != '/') { + return "https://" + serverName + "/" + ret; + } else { + return "https://" + serverName + ret; + } + + } else { + return ret; + } + } +} + + + + + + + + diff --git a/src/main/java/io/jboot/web/render/JbootRender.java b/src/main/java/io/jboot/web/render/JbootRender.java index cf8d9e184524e4a5b38913d24bcc6aac10601513..daedc14b2a10de0992d75c31a26e9b5942b55db2 100644 --- a/src/main/java/io/jboot/web/render/JbootRender.java +++ b/src/main/java/io/jboot/web/render/JbootRender.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,26 @@ package io.jboot.web.render; import com.jfinal.render.Render; +import com.jfinal.render.RenderException; import com.jfinal.render.RenderManager; import com.jfinal.template.Engine; import io.jboot.Jboot; +import io.jboot.utils.StrUtil; +import io.jboot.web.render.cdn.JbootWebCdnConfig; +import io.jboot.ext.MixedByteArrayOutputStream; +import io.jboot.web.render.cdn.CdnUtil; +import java.io.IOException; +import java.io.PrintWriter; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; public class JbootRender extends Render { - private static Engine engine; - - private static final String contentType = "text/html; charset=" + getEncoding(); + private static String contentType = "text/html; charset=" + getEncoding(); + private static JbootWebCdnConfig cdnConfig = Jboot.config(JbootWebCdnConfig.class); private Engine getEngine() { if (engine == null) { @@ -38,11 +44,11 @@ public class JbootRender extends Render { return engine; } - private JbootWebCdnConfig config; - public JbootRender(String view) { + if (StrUtil.isBlank(view)){ + throw new IllegalArgumentException("view cannot be null or empty."); + } this.view = view; - this.config = Jboot.config(JbootWebCdnConfig.class); } public String getContentType() { @@ -59,10 +65,32 @@ public class JbootRender extends Render { data.put(attrName, request.getAttribute(attrName)); } - String html = getEngine().getTemplate(view).renderToString(data); - html = config.isEnable() ? RenderHelpler.processCDN(html, config.getDomain()) : html; + try { + if (cdnConfig.isEnable()) { + renderWithCdn(data); + } else { + getEngine().getTemplate(view).render(data, response.getWriter()); + } + } catch (RuntimeException e) { // 捕获 ByteWriter.close() 抛出的 RuntimeException + Throwable cause = e.getCause(); + if (cause instanceof IOException) { // ClientAbortException、EofException 直接或间接继承自 IOException + String name = cause.getClass().getSimpleName(); + if ("ClientAbortException".equals(name) || "EofException".equals(name)) { + return; + } + } + throw e; + } catch (IOException e) { + throw new RenderException(e); + } + } + + private void renderWithCdn(Map data) throws IOException { + MixedByteArrayOutputStream baos = new MixedByteArrayOutputStream(); + getEngine().getTemplate(view).render(data, baos); - RenderHelpler.renderHtml(response, html, contentType); + PrintWriter responseWriter = response.getWriter(); + responseWriter.write(CdnUtil.toHtml(baos.getInputStream(), cdnConfig.getDomain())); } diff --git a/src/main/java/io/jboot/web/render/JbootRenderFactory.java b/src/main/java/io/jboot/web/render/JbootRenderFactory.java index 12eb63b8228b650a775d9d6e1927e9a0dbe14002..231fe122237d4140ae689f69d1aca3fa3b1d5936 100644 --- a/src/main/java/io/jboot/web/render/JbootRenderFactory.java +++ b/src/main/java/io/jboot/web/render/JbootRenderFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,12 @@ package io.jboot.web.render; import com.jfinal.render.ContentType; +import com.jfinal.render.ErrorRender; import com.jfinal.render.Render; import com.jfinal.render.RenderFactory; +import com.jfinal.template.TemplateException; +import io.jboot.components.valid.ValidErrorRender; +import io.jboot.components.valid.ValidException; public class JbootRenderFactory extends RenderFactory { @@ -60,7 +64,7 @@ public class JbootRenderFactory extends RenderFactory { @Override public Render getErrorRender(int errorCode) { - return new JbootErrorRender(errorCode, constants.getErrorView(errorCode)); + return new JbootErrorRender(errorCode, ErrorRender.getErrorView(errorCode)); } @Override @@ -103,4 +107,43 @@ public class JbootRenderFactory extends RenderFactory { return new JbootXmlRender(view); } + + @Override + public Render getRedirectRender(String url) { + return new JbootRedirectRender(url); + } + + @Override + public Render getRedirectRender(String url, boolean withQueryString) { + return new JbootRedirectRender(url, withQueryString); + } + + @Override + public Render getRedirect301Render(String url) { + return new JbootRedirect301Render(url); + } + + @Override + public Render getRedirect301Render(String url, boolean withQueryString) { + return new JbootRedirect301Render(url, withQueryString); + } + + @Override + public Render getCaptchaRender() { + return new JbootCaptchaRender(); + } + + public JbootReturnValueRender getReturnValueRender(Object returnValue) { + return new JbootReturnValueRender(returnValue); + } + + public ValidErrorRender getValidErrorRender(ValidException validException) { + return new ValidErrorRender(validException); + } + + public TemplateErrorRender getTemplateErrorRender(TemplateException e) { + return new TemplateErrorRender(e); + } + + } diff --git a/src/main/java/io/jboot/web/render/JbootResponseEntityRender.java b/src/main/java/io/jboot/web/render/JbootResponseEntityRender.java new file mode 100644 index 0000000000000000000000000000000000000000..a8f90968996e949e860d54e70f145d90a9b8a359 --- /dev/null +++ b/src/main/java/io/jboot/web/render/JbootResponseEntityRender.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.render; + +import com.jfinal.kit.JsonKit; +import com.jfinal.render.Render; +import com.jfinal.render.RenderException; +import io.jboot.utils.DateUtil; +import io.jboot.web.ResponseEntity; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Date; +import java.util.Map; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/4/7 + */ +public class JbootResponseEntityRender extends Render { + + private ResponseEntity responseEntity; + + public JbootResponseEntityRender(ResponseEntity responseEntity) { + this.responseEntity = responseEntity; + } + + @Override + public void render() { + + PrintWriter writer = null; + try { + //默认输出 json,但是 responseEntity 可以配置 Header 覆盖这个输出 + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Content-Type", "application/json; charset=utf-8"); + response.setStatus(responseEntity.getHttpStatus().value()); + + Map headers = responseEntity.getHeaders(); + if (headers != null && !headers.isEmpty()) { + for (Map.Entry entry : headers.entrySet()) { + response.setHeader(entry.getKey(), entry.getValue()); + } + } + + Object body = responseEntity.getBody(); + String bodyString = null; + if (body == null) { + bodyString = ""; + } else if (body instanceof String) { + bodyString = (String) body; + } else if (body instanceof Date) { + bodyString = DateUtil.toDateTimeString((Date) body); + } else { + JsonKit.toJson(body); + } + writer = response.getWriter(); + writer.write(bodyString); + // writer.flush(); + } catch (IOException e) { + throw new RenderException(e); + } + + } +} diff --git a/src/main/java/io/jboot/web/render/JbootReturnValueRender.java b/src/main/java/io/jboot/web/render/JbootReturnValueRender.java new file mode 100644 index 0000000000000000000000000000000000000000..f386a4f295a08101a4ea7b45b1b632922a8ebbf0 --- /dev/null +++ b/src/main/java/io/jboot/web/render/JbootReturnValueRender.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.render; + +import com.jfinal.kit.JsonKit; +import com.jfinal.render.IRenderFactory; +import com.jfinal.render.Render; +import com.jfinal.render.RenderManager; +import io.jboot.utils.DateUtil; +import io.jboot.utils.StrUtil; +import io.jboot.web.ResponseEntity; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +/** + * @author Michael Yang 杨福海 (fuhai999@gmail.com) + * @version V1.0 + */ +public class JbootReturnValueRender extends Render { + + protected IRenderFactory factory = RenderManager.me().getRenderFactory(); + protected Object value; + protected Render render; + + protected String forwardTo; + + public JbootReturnValueRender(Object returnValue) { + + if (returnValue == null) { + this.value = null; + } else if (isBaseType(returnValue)) { + this.value = String.valueOf(returnValue); + } else { + this.value = returnValue; + } + + initRealRender(); + } + + + protected void initRealRender() { + if (this.value == null) { + this.render = factory.getNullRender(); + } else if (this.value instanceof ResponseEntity) { + this.render = new JbootResponseEntityRender((ResponseEntity) value); + } else if (this.value instanceof String) { + String newVal = ((String) value).toLowerCase(); + + //template + if (newVal.endsWith(".html") && !newVal.contains(":")) { + this.render = factory.getTemplateRender((String) value); + } + + //error + else if (newVal.startsWith("error") && newVal.length() > 8) { + String trim = ((String) value).substring(5).trim(); + if (trim.startsWith(":")) { + String errorCodeStr = trim.substring(1).trim(); + if (StrUtil.isNumeric(errorCodeStr)) { + this.render = factory.getErrorRender(Integer.parseInt(errorCodeStr)); + } + } + if (this.render == null) { + this.render = factory.getTextRender((String) value); + } + } + + //forward + else if (newVal.startsWith("forward")) { + String trim = ((String) value).substring(7).trim(); + if (trim.startsWith(":")) { + this.forwardTo = trim.substring(1).trim(); + } else { + this.render = factory.getTextRender((String) value); + } + } + + //redirect + else if (newVal.startsWith("redirect")) { + String trim = ((String) value).substring(8).trim(); + if (trim.startsWith(":")) { + String redirectTo = trim.substring(1).trim(); + if (StrUtil.isNotBlank(redirectTo)) { + this.render = factory.getRedirectRender(redirectTo); + } + } + if (this.render == null) { + this.render = factory.getTextRender((String) value); + } + } + + //text + else { + this.render = factory.getTextRender((String) value); + } + } else if (this.value instanceof Date) { + this.render = factory.getTextRender(DateUtil.toDateTimeString((Date) value)); + } else if (this.value instanceof File) { + this.render = factory.getFileRender((File) value); + } else if (this.value instanceof Render) { + this.render = (Render) value; + } else { + this.render = factory.getJsonRender(JsonKit.toJson(value)); + } + } + + + @Override + public Render setContext(HttpServletRequest request, HttpServletResponse response) { + render.setContext(request, response); + return this; + } + + @Override + public Render setContext(HttpServletRequest request, HttpServletResponse response, String viewPath) { + render.setContext(request, response, viewPath); + return this; + } + + @Override + public void render() { + this.render.render(); + } + + + protected boolean isBaseType(Object value) { + Class c = value.getClass(); + return c == String.class || c == char.class + || c == Integer.class || c == int.class + || c == Long.class || c == long.class + || c == Double.class || c == double.class + || c == Float.class || c == float.class + || c == Boolean.class || c == boolean.class + || c == Short.class || c == short.class + || c == BigDecimal.class || c == BigInteger.class; + } + + public Render getRealRender(){ + return render; + } + + public String getForwardTo() { + return forwardTo; + } +} diff --git a/src/main/java/io/jboot/web/render/JbootTemplateRender.java b/src/main/java/io/jboot/web/render/JbootTemplateRender.java index 7c9302f84403a2097bb7d03963b178f37cc8e7d1..65ed97f08ed2e0d78d108eee7286fe84f8d9e089 100644 --- a/src/main/java/io/jboot/web/render/JbootTemplateRender.java +++ b/src/main/java/io/jboot/web/render/JbootTemplateRender.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package io.jboot.web.render; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.render */ public class JbootTemplateRender extends JbootRender { diff --git a/src/main/java/io/jboot/web/render/JbootTextRender.java b/src/main/java/io/jboot/web/render/JbootTextRender.java index e39289d6f8e3fd9b3d3617dfdbae5c4c9283f6cb..1e7510d252f51bcdd1699d3f49f2fc57aa97f838 100644 --- a/src/main/java/io/jboot/web/render/JbootTextRender.java +++ b/src/main/java/io/jboot/web/render/JbootTextRender.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import com.jfinal.render.TextRender; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.render */ public class JbootTextRender extends TextRender { public JbootTextRender(String text) { diff --git a/src/main/java/io/jboot/web/render/JbootXmlRender.java b/src/main/java/io/jboot/web/render/JbootXmlRender.java index a3d9d9c89f60488eb376094fa443ac451e19f758..c48aa1c398321f10d2f8cf4f851601809e985f2a 100644 --- a/src/main/java/io/jboot/web/render/JbootXmlRender.java +++ b/src/main/java/io/jboot/web/render/JbootXmlRender.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,8 @@ package io.jboot.web.render; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.render */ -public class JbootXmlRender extends JbootRender { +public class JbootXmlRender extends JbootTemplateRender { private static final String contentType = "text/xml; charset=" + getEncoding(); diff --git a/src/main/java/io/jboot/web/render/TemplateErrorRender.java b/src/main/java/io/jboot/web/render/TemplateErrorRender.java new file mode 100644 index 0000000000000000000000000000000000000000..242ee3eb119259dc4f58ac507f254e29a7b557f5 --- /dev/null +++ b/src/main/java/io/jboot/web/render/TemplateErrorRender.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.render; + +import com.jfinal.render.Render; +import com.jfinal.render.RenderException; +import com.jfinal.template.TemplateException; + +import java.io.IOException; +import java.io.PrintWriter; + +public class TemplateErrorRender extends Render { + + private final TemplateException exception; + + public TemplateErrorRender(TemplateException e) { + this.exception = e; + } + + @Override + public void render() { + try { + PrintWriter writer = response.getWriter(); + String message = exception.getMessage(); + if (message != null) { + message = message.replace("\n", "
"); + } + writer.write("TemplateException: " + message); + } catch (IOException e) { + throw new RenderException(e); + } + } +} diff --git a/src/main/java/io/jboot/web/render/RenderHelpler.java b/src/main/java/io/jboot/web/render/cdn/CdnUtil.java similarity index 64% rename from src/main/java/io/jboot/web/render/RenderHelpler.java rename to src/main/java/io/jboot/web/render/cdn/CdnUtil.java index 5131ddfd363aa81f8ecbfac08d2df536c9b85ca8..f6cacf29d79ae9f43a2c15baf2074d8eeb1cdee7 100644 --- a/src/main/java/io/jboot/web/render/RenderHelpler.java +++ b/src/main/java/io/jboot/web/render/cdn/CdnUtil.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,44 +13,47 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.web.render; +package io.jboot.web.render.cdn; -import com.jfinal.render.RenderException; +import com.jfinal.core.JFinal; +import io.jboot.Jboot; import io.jboot.utils.StrUtil; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; -import javax.servlet.http.HttpServletResponse; -import java.io.PrintWriter; +import java.io.IOException; +import java.io.InputStream; import java.util.Iterator; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.render */ -public class RenderHelpler { - - public static void renderHtml(HttpServletResponse response, String html, String contentType) { - response.setContentType(contentType); - try { - PrintWriter responseWriter = response.getWriter(); - responseWriter.write(html); - } catch (Exception e) { - throw new RenderException(e); - } - } +public class CdnUtil { + private static String charSet = JFinal.me().getConstants().getEncoding(); + private static JbootWebCdnConfig cdnConfig = Jboot.config(JbootWebCdnConfig.class); + + public static String appendCdnDomain(String path) { + if (StrUtil.isBlank(path)) { + return path; + } - public static String processCDN(String content, String domain) { - if (StrUtil.isBlank(content)) { - return content; + if (cdnConfig.isEnable() && StrUtil.isNotBlank(cdnConfig.getDomain())) { + if (!path.startsWith("/")) { + path = "/" + path; + } + return cdnConfig.getDomain() + path; } + return path; + } + - Document doc = Jsoup.parse(content); + public static String toHtml(InputStream content, String domain) throws IOException { + Document doc = Jsoup.parse(content, charSet, ""); Elements jsElements = doc.select("script[src]"); replace(jsElements, "src", domain); @@ -58,8 +61,7 @@ public class RenderHelpler { Elements imgElements = doc.select("img[src]"); replace(imgElements, "src", domain); - - Elements linkElements = doc.select("link[href]"); + Elements linkElements = doc.select("link"); replace(linkElements, "href", domain); return doc.toString(); diff --git a/src/main/java/io/jboot/web/render/JbootWebCdnConfig.java b/src/main/java/io/jboot/web/render/cdn/JbootWebCdnConfig.java similarity index 84% rename from src/main/java/io/jboot/web/render/JbootWebCdnConfig.java rename to src/main/java/io/jboot/web/render/cdn/JbootWebCdnConfig.java index de6493394b6820692295b6305671b261dad73cd6..99e24db566917d762be58b525c1230356ee43217 100644 --- a/src/main/java/io/jboot/web/render/JbootWebCdnConfig.java +++ b/src/main/java/io/jboot/web/render/cdn/JbootWebCdnConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.web.render; +package io.jboot.web.render.cdn; import io.jboot.app.config.annotation.ConfigModel; import io.jboot.utils.StrUtil; @@ -37,6 +37,10 @@ public class JbootWebCdnConfig { } public void setDomain(String domain) { + if (domain.endsWith("/")) { + domain = domain.substring(0, domain.length() - 1); + } this.domain = domain; } + } diff --git a/src/main/java/io/jboot/web/session/JbootHttpSession.java b/src/main/java/io/jboot/web/session/JbootHttpSession.java index cca8205161d8213b7c9b61c2fbbfd00fcd84b387..8aebe54dcad872388661392da446b9a13e6656b2 100644 --- a/src/main/java/io/jboot/web/session/JbootHttpSession.java +++ b/src/main/java/io/jboot/web/session/JbootHttpSession.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,7 @@ import com.google.common.collect.Sets; import javax.servlet.ServletContext; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionContext; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Map; -import java.util.Set; +import java.util.*; public class JbootHttpSession implements HttpSession { @@ -42,19 +39,24 @@ public class JbootHttpSession implements HttpSession { private final Set deleteAttribute = Sets.newHashSet(); private final Map sessionStore; - private volatile boolean invalid; - private volatile boolean dataChanged; - private volatile boolean empty; + private volatile boolean invalid = false; + private volatile boolean dataChanged = false; + private volatile boolean empty = false; - public JbootHttpSession(String id, ServletContext servletContext, Map sessionStore) { + private volatile HttpSession originSession; + + public JbootHttpSession(String id, ServletContext servletContext, Map sessionStore, HttpSession originSession) { this.id = id; this.servletContext = servletContext; this.sessionStore = sessionStore; this.createdAt = System.currentTimeMillis(); this.lastAccessedAt = createdAt; this.empty = sessionStore.isEmpty(); + this.originSession = originSession; } + + @Override public long getCreationTime() { return createdAt; @@ -85,6 +87,7 @@ public class JbootHttpSession implements HttpSession { return maxInactiveInterval; } + @Override @Deprecated public HttpSessionContext getSessionContext() { return null; @@ -137,6 +140,10 @@ public class JbootHttpSession implements HttpSession { empty = false; dataChanged = true; + if (originSession != null) { + originSession.setAttribute(name, value); + } + } @Override @@ -158,6 +165,10 @@ public class JbootHttpSession implements HttpSession { deleteAttribute.add(name); newAttributes.remove(name); dataChanged = true; + + if (originSession != null) { + originSession.removeAttribute(name); + } } @Override @@ -169,6 +180,10 @@ public class JbootHttpSession implements HttpSession { public void invalidate() { invalid = true; dataChanged = true; + + if (originSession != null) { + originSession.invalidate(); + } } @Override @@ -183,7 +198,7 @@ public class JbootHttpSession implements HttpSession { public Map snapshot() { - Map snap = Maps.newHashMap(); + Map snap = new HashMap<>(); snap.putAll(sessionStore); snap.putAll(newAttributes); for (String name : deleteAttribute) { diff --git a/src/main/java/io/jboot/web/session/JbootServletRequestWrapper.java b/src/main/java/io/jboot/web/session/JbootServletRequestWrapper.java index 96759d4171566f2b4e5aae5fc4f97aa878cf6dc6..6aab4b7c0f39ffd74e062661f4503fa0dce8db46 100644 --- a/src/main/java/io/jboot/web/session/JbootServletRequestWrapper.java +++ b/src/main/java/io/jboot/web/session/JbootServletRequestWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,7 @@ package io.jboot.web.session; import io.jboot.Jboot; import io.jboot.components.cache.JbootCache; -import io.jboot.components.cache.JbootCacheConfig; import io.jboot.components.cache.JbootCacheManager; -import io.jboot.utils.StrUtil; import javax.servlet.http.*; import java.util.Enumeration; @@ -38,28 +36,28 @@ public class JbootServletRequestWrapper extends HttpServletRequestWrapper { private static String cookieDomain = config.getCookieDomain(); private static int cookieMaxAge = config.getCookieMaxAge(); private static String cacheName = config.getCacheName(); - private static String cacheType = config.getCacheType(); + private static String useCacheName = config.getUseCacheName(); private static JbootCache jbootCache = JbootCacheManager.me() - .getCache(StrUtil.isBlank(cacheType) || JbootCacheConfig.TYPE_NONE.equals(cacheType) - ? JbootCacheConfig.TYPE_EHCACHE - : cacheType); + .getCache(useCacheName); private HttpServletResponse response; private HttpServletRequest originRequest; + private HttpSession originSession; private JbootHttpSession jbootSession; public JbootServletRequestWrapper(HttpServletRequest request, HttpServletResponse response) { super(request); - this.originRequest = request; this.response = response; + this.originRequest = request; + this.originSession = request.getSession(false); } @Override public HttpSession getSession() { - return getSession(false); + return getSession(true); } @@ -71,11 +69,11 @@ public class JbootServletRequestWrapper extends HttpServletRequestWrapper { String sessionId = getCookie(cookieName); if (sessionId != null) { - jbootSession = new JbootHttpSession(sessionId, originRequest.getServletContext(), createSessionStore(sessionId)); + jbootSession = new JbootHttpSession(sessionId, originRequest.getServletContext(), createSessionStore(sessionId), originSession); jbootSession.setMaxInactiveInterval(maxInactiveInterval); - } else if (create) { + } else if (create || originSession != null) { sessionId = UUID.randomUUID().toString().replace("-", ""); - jbootSession = new JbootHttpSession(sessionId, originRequest.getServletContext(), createSessionStore(sessionId)); + jbootSession = new JbootHttpSession(sessionId, originRequest.getServletContext(), createSessionStore(sessionId), originSession); jbootSession.setMaxInactiveInterval(maxInactiveInterval); setCookie(cookieName, sessionId, cookieMaxAge); } @@ -87,21 +85,30 @@ public class JbootServletRequestWrapper extends HttpServletRequestWrapper { Map store = jbootCache.get(cacheName, sessionId); if (store == null) { store = new HashMap<>(); - HttpSession originSession = originRequest.getSession(); - if (originSession != null) { - Enumeration names = originSession.getAttributeNames(); - if (names != null) { - while (names.hasMoreElements()) { - String name = names.nextElement(); - store.put(name, originSession.getAttribute(name)); - } - } - } + syncOriginSessionData(store); + jbootCache.put(cacheName, sessionId, store); } return store; } + /** + * 同步上层 session 到 sessionStore + * @// TODO: 2021/6/3 若上层动态修改了 上层自己的 session,会导致 Controller 的 session 和 上层 session 不同步的情况 + * @// TODO: 2021/6/3 临时的解决方案需要用户手动通过 Controller 来修改 session 数据 + * @param store + */ + private void syncOriginSessionData(Map store) { + if (this.originSession != null) { + Enumeration names = originSession.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + store.put(name, originSession.getAttribute(name)); + } + } + } + + /** * http请求结束时,更新session信息,包括:刷新session的存储时间,更新session数据,清空session数据等 */ @@ -110,12 +117,12 @@ public class JbootServletRequestWrapper extends HttpServletRequestWrapper { return; } - // 空的 jbootSession 数据 - // 或者 session 已经被整体删除,调用了session.invalidate() - if (jbootSession.isEmpty() || !jbootSession.isValid()) { + //session 已经被整体删除,用户调用了session.invalidate() + if (!jbootSession.isValid()) { jbootCache.remove(cacheName, jbootSession.getId()); setCookie(cookieName, null, 0); } + //session 已经被修改(session数据的增删改查) else if (jbootSession.isDataChanged()) { Map snapshot = jbootSession.snapshot(); @@ -127,6 +134,7 @@ public class JbootServletRequestWrapper extends HttpServletRequestWrapper { jbootCache.put(cacheName, jbootSession.getId(), snapshot, maxInactiveInterval); } } + //更新session存储时间 else { jbootCache.setTtl(cacheName, jbootSession.getId(), maxInactiveInterval); @@ -175,6 +183,7 @@ public class JbootServletRequestWrapper extends HttpServletRequestWrapper { } } + public HttpServletRequest getOriginRequest() { return originRequest; } diff --git a/src/main/java/io/jboot/web/session/JbootSessionConfig.java b/src/main/java/io/jboot/web/session/JbootSessionConfig.java index 0d4495c38daa7b4a5938f7f812156dbf3f9ca60e..58d20bbc91f24dd8bed0653d25127f9840f6a10e 100644 --- a/src/main/java/io/jboot/web/session/JbootSessionConfig.java +++ b/src/main/java/io/jboot/web/session/JbootSessionConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,7 @@ */ package io.jboot.web.session; -import io.jboot.Jboot; import io.jboot.app.config.annotation.ConfigModel; -import io.jboot.components.cache.JbootCacheConfig; @ConfigModel(prefix = "jboot.web.session") public class JbootSessionConfig { @@ -36,7 +34,8 @@ public class JbootSessionConfig { private int cookieMaxAge = DEFAULT_COOKIE_MAX_AGE; private String cacheName = DEFAULT_SESSION_CACHE_NAME; - private String cacheType = Jboot.config(JbootCacheConfig.class).getType(); + private String useCacheName = "default"; + public String getCookieName() { return cookieName; @@ -86,12 +85,12 @@ public class JbootSessionConfig { this.cacheName = cacheName; } - public String getCacheType() { - return cacheType; + public String getUseCacheName() { + return useCacheName; } - public void setCacheType(String cacheType) { - this.cacheType = cacheType; + public void setUseCacheName(String useCacheName) { + this.useCacheName = useCacheName; } } diff --git a/src/main/java/io/jboot/web/validate/CaptchaValidate.java b/src/main/java/io/jboot/web/validate/CaptchaValidate.java index c57189a3abaf0ab240027c0d4c2ca1807868cfcf..ae58c8fee108730cad98b0c72cf9763a0fc01d1f 100644 --- a/src/main/java/io/jboot/web/validate/CaptchaValidate.java +++ b/src/main/java/io/jboot/web/validate/CaptchaValidate.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,6 @@ public @interface CaptchaValidate { String renderType() default ValidateRenderType.DEFAULT; - int errorCode() default 1; + int errorCode() default 400; } diff --git a/src/main/java/io/jboot/web/validate/EmptyValidate.java b/src/main/java/io/jboot/web/validate/EmptyValidate.java index c1e021fcd7fccb4d53bdb8e11f5c4fa0a28ef74d..4bebc93a9c35e0cb41314d984de64241ea35e731 100644 --- a/src/main/java/io/jboot/web/validate/EmptyValidate.java +++ b/src/main/java/io/jboot/web/validate/EmptyValidate.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/web/validate/Form.java b/src/main/java/io/jboot/web/validate/Form.java index e129a2f9d42d5557dce63d560d0b7138c0ae3ba7..9727ab419be5435c253f73b1f708de86e305906f 100644 --- a/src/main/java/io/jboot/web/validate/Form.java +++ b/src/main/java/io/jboot/web/validate/Form.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,5 +33,5 @@ public @interface Form { String type() default FormType.FORM_DATA; - int errorCode() default 1; + int errorCode() default 400; } diff --git a/src/main/java/io/jboot/web/validate/FormType.java b/src/main/java/io/jboot/web/validate/FormType.java index 4926d9affeb6b009de1a926a0b7ba89ff64f5ca9..ce9964076ec7909c208fc4e5293343dff81528a3 100644 --- a/src/main/java/io/jboot/web/validate/FormType.java +++ b/src/main/java/io/jboot/web/validate/FormType.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package io.jboot.web.validate; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.controller.validate */ public class FormType { diff --git a/src/main/java/io/jboot/web/validate/Regex.java b/src/main/java/io/jboot/web/validate/Regex.java new file mode 100644 index 0000000000000000000000000000000000000000..fd64fef84993eb553f1c655642d45f4b96b93056 --- /dev/null +++ b/src/main/java/io/jboot/web/validate/Regex.java @@ -0,0 +1,296 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.validate; + +/** + * 正则表达式大全 + * @author michael yang (fuhai999@gmail.com) + */ +public class Regex { + + /** + * 汉字 + */ + public static final String CHINESE ="^[\\u4e00-\\u9fa5]+$"; + + /** + * 全部是英文 + */ + public static final String ENGLISH ="^[A-Za-z]+$"; + + + /** + * 英文或者数字 + */ + public static final String ENGLISH_NUMBERS ="^[A-Za-z0-9]+$"; + + + /** + * 英文、数字 或下划线 + */ + public static final String ENGLISH_NUMBERS_UNDERLINE ="^[A-Za-z0-9_]+$"; + + + + /** + * 中文、英文、数字、或下划线 + */ + public static final String CHINESE_ENGLISH_NUMBERS_UNDERLINE ="^[\\u4E00-\\u9FA5A-Za-z0-9_]+$"; + + /** + * 正数、负数 或 小数 + */ + public static final String DECIMAL ="^(\\-|\\+)?\\d+(\\.\\d+)?$"; + + + /** + * 密码长度 6~20 位数,字母、数字和下划线 + */ + public static final String CIPHER ="^[a-zA-Z0-9_]\\w{5,19}$"; + + + /** + * 邮件地址 + */ + public static final String EMAIL ="^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"; + + + /** + * 域名 + */ + public static final String DOMAIN ="^((http://)|(https://))?([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}"; + + /** + * url 地址 + */ + public static final String URL ="[a-zA-z]+://[^\\s]*"; + + /** + * 手机号码 + */ + public static final String MOBILE ="^(1[3,4,5,6,7,8,9])\\d{9}$"; + + /** + * 电话号码 + */ + public static final String TELEPHONE ="^(\\(\\d{3,4}\\)|\\d{3,4}-|\\s)?\\d{7,14}$"; + + /** + * 身份证号码 + */ + public static final String ID_CARD ="^[1-9]\\d{5}(18|19|20|(3\\d))\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$"; + + + /** + * 日期格式 2020-02-0 + */ + public static final String DATE ="^[0-9]{4}-(((0[13578]|(10|12))-(0[1-9]|[1-2][0-9]|3[0-1]))|(02-(0[1-9]|[1-2][0-9]))|((0[469]|11)-(0[1-9]|[1-2][0-9]|30)))$"; + + + /** + * 日期格式 2020-02-02 23:12:23 或者 2020-02-02 23:12 + */ + public static final String DATE_TIME ="^[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])\\s+(20|21|22|23|[0-1]\\d):[0-5]\\d(:[0-5]\\d)?$"; + + + /** + * 时间格式(没有秒) 2020-02-02 23:12 + */ + public static final String DATE_TIME_HM ="^[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])\\s+(20|21|22|23|[0-1]\\d):[0-5]\\d$"; + + + + /** + * 时间格式 2020-02-02 23:12:23 + */ + public static final String DATE_TIME_HMS ="^[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])\\s+(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d$"; + + + + /** + * ip地址 + */ + public static final String IP ="^((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)$"; + + + + public static void main(String[] args){ + + System.out.println("\"汉字\".matches(CHINESE) ---> " + ("汉字".matches(CHINESE))); + System.out.println("\"汉字123\".matches(CHINESE) ---> " + ("汉字123".matches(CHINESE))); + System.out.println(); + + + + System.out.println("\"abc\".matches(ENGLISH) ---> " + ("abc".matches(ENGLISH))); + System.out.println("\"abc123\".matches(ENGLISH) ---> " + ("abc123".matches(ENGLISH))); + System.out.println(); + + + + System.out.println("\"abc123\".matches(ENGLISH_NUMBERS) ---> " + ("abc123".matches(ENGLISH_NUMBERS))); + System.out.println("\"abc_123\".matches(ENGLISH_NUMBERS) ---> " + ("abc_123".matches(ENGLISH_NUMBERS))); + System.out.println("\"汉字abc123\".matches(ENGLISH_NUMBERS) ---> " + ("汉字汉字abc123".matches(ENGLISH_NUMBERS))); + System.out.println(); + + + + System.out.println("\"abc123\".matches(ENGLISH_NUMBERS_UNDERLINE) ---> " + ("abc123".matches(ENGLISH_NUMBERS_UNDERLINE))); + System.out.println("\"abc_123\".matches(ENGLISH_NUMBERS_UNDERLINE) ---> " + ("abc_123".matches(ENGLISH_NUMBERS_UNDERLINE))); + System.out.println("\"汉字abc123\".matches(ENGLISH_NUMBERS_UNDERLINE) ---> " + ("汉字汉字abc123".matches(ENGLISH_NUMBERS_UNDERLINE))); + System.out.println(); + + + + System.out.println("\"abc123\".matches(CHINESE_ENGLISH_NUMBERS_UNDERLINE) ---> " + ("abc123".matches(CHINESE_ENGLISH_NUMBERS_UNDERLINE))); + System.out.println("\"abc_123\".matches(CHINESE_ENGLISH_NUMBERS_UNDERLINE) ---> " + ("abc_123".matches(CHINESE_ENGLISH_NUMBERS_UNDERLINE))); + System.out.println("\"汉字abc123\".matches(CHINESE_ENGLISH_NUMBERS_UNDERLINE) ---> " + ("汉字汉字abc123".matches(CHINESE_ENGLISH_NUMBERS_UNDERLINE))); + System.out.println("\"汉字abc123#\".matches(CHINESE_ENGLISH_NUMBERS_UNDERLINE) ---> " + ("汉字abc123#".matches(CHINESE_ENGLISH_NUMBERS_UNDERLINE))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(DECIMAL) ---> " + ("汉字".matches(DECIMAL))); + System.out.println("\"123\".matches(DECIMAL) ---> " + ("123".matches(DECIMAL))); + System.out.println("\"123.12\".matches(DECIMAL) ---> " + ("123.12".matches(DECIMAL))); + System.out.println("\"-12\".matches(DECIMAL) ---> " + ("-12".matches(DECIMAL))); + System.out.println("\"-12.12\".matches(DECIMAL) ---> " + ("-12.12".matches(DECIMAL))); + System.out.println(); + + + + System.out.println("\"123\".matches(PASSWORD) ---> " + ("123".matches(CIPHER))); + System.out.println("\"123456\".matches(PASSWORD) ---> " + ("123456".matches(CIPHER))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(EMAIL) ---> " + ("汉字".matches(EMAIL))); + System.out.println("\"abc\".matches(EMAIL) ---> " + ("abc".matches(EMAIL))); + System.out.println("\"abc@gmail\".matches(EMAIL) ---> " + ("bc.abc@gmail".matches(EMAIL))); + System.out.println("\"abc.abc@gmail\".matches(EMAIL) ---> " + ("bc.abc@gmail".matches(EMAIL))); + System.out.println("\"abc@gmail.com\".matches(EMAIL) ---> " + ("bc.abc@gmail.com".matches(EMAIL))); + System.out.println("\"abc.abc@gmail.com\".matches(EMAIL) ---> " + ("bc.abc@gmail.com".matches(EMAIL))); + System.out.println(); + + + + System.out.println("\"yangfuhai\".matches(DOMAIN) ---> " + ("yangfuhai".matches(DOMAIN))); + System.out.println("\"yangfuhai.com\".matches(DOMAIN) ---> " + ("yangfuhai.com".matches(DOMAIN))); + System.out.println("\"www.yangfuhai.com\".matches(DOMAIN) ---> " + ("www.yangfuhai.com".matches(DOMAIN))); + System.out.println("\"http://www.yangfuhai.com\".matches(DOMAIN) ---> " + ("http://www.yangfuhai.com".matches(DOMAIN))); + System.out.println("\"http://www.yangfuhai.com/\".matches(DOMAIN) ---> " + ("http://www.yangfuhai.com/".matches(DOMAIN))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(URL) ---> " + ("汉字".matches(URL))); + System.out.println("\"abc\".matches(URL) ---> " + ("abc".matches(URL))); + System.out.println("\"http://www\".matches(URL) ---> " + ("http://www".matches(URL))); + System.out.println("\"http://www.yangfuhai.com\".matches(URL) ---> " + ("http://www.yangfuhai.com".matches(URL))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(MOBILE) ---> " + ("汉字".matches(MOBILE))); + System.out.println("\"12345\".matches(MOBILE) ---> " + ("12345".matches(MOBILE))); + System.out.println("\"11611223344\".matches(MOBILE) ---> " + ("11611223344".matches(MOBILE))); + System.out.println("\"12611223344\".matches(MOBILE) ---> " + ("12611223344".matches(MOBILE))); + System.out.println("\"13611223344\".matches(MOBILE) ---> " + ("13611223344".matches(MOBILE))); + System.out.println("\"14611223344\".matches(MOBILE) ---> " + ("14611223344".matches(MOBILE))); + System.out.println("\"15611223344\".matches(MOBILE) ---> " + ("15611223344".matches(MOBILE))); + System.out.println("\"16611223344\".matches(MOBILE) ---> " + ("16611223344".matches(MOBILE))); + System.out.println("\"17611223344\".matches(MOBILE) ---> " + ("17611223344".matches(MOBILE))); + System.out.println("\"18611223344\".matches(MOBILE) ---> " + ("18611223344".matches(MOBILE))); + System.out.println("\"19611223344\".matches(MOBILE) ---> " + ("19611223344".matches(MOBILE))); + System.out.println("\"196112233441\".matches(MOBILE) ---> " + ("196112233441".matches(MOBILE))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(TELEPHONE) ---> " + ("汉字".matches(TELEPHONE))); + System.out.println("\"021-1234567\".matches(TELEPHONE) ---> " + ("021-1234567".matches(TELEPHONE))); + System.out.println("\"0855-1234567\".matches(TELEPHONE) ---> " + ("0855-1234567".matches(TELEPHONE))); + System.out.println("\"1234567\".matches(TELEPHONE) ---> " + ("1234567".matches(TELEPHONE))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(ID_CARD) ---> " + ("汉字".matches(ID_CARD))); + System.out.println("\"522\".matches(ID_CARD) ---> " + ("522".matches(ID_CARD))); + System.out.println("\"522000000000000000\".matches(ID_CARD) ---> " + ("522000000000000000".matches(ID_CARD))); + System.out.println("\"52260119000125205x\".matches(ID_CARD) ---> " + ("52260119000125205x".matches(ID_CARD))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(DATE) ---> " + ("汉字".matches(DATE))); + System.out.println("\"abc\".matches(DATE) ---> " + ("abc".matches(DATE))); + System.out.println("\"123\".matches(DATE) ---> " + ("123".matches(DATE))); + System.out.println("\"2020-02-02\".matches(DATE) ---> " + ("2020-02-02".matches(DATE))); + System.out.println("\"2020-02-02 \".matches(DATE) ---> " + ("2020-02-02 ".matches(DATE))); + System.out.println("\"2020-02-02 23:32\".matches(DATE) ---> " + ("2020-02-02 23:32".matches(DATE))); + System.out.println("\"2020-02-02 23:32:21\".matches(DATE) ---> " + ("2020-02-02 23:32:21".matches(DATE))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(DATE_TIME) ---> " + ("汉字".matches(DATE_TIME))); + System.out.println("\"abc\".matches(DATE_TIME) ---> " + ("abc".matches(DATE_TIME))); + System.out.println("\"123\".matches(DATE_TIME) ---> " + ("123".matches(DATE_TIME))); + System.out.println("\"2020-02-02\".matches(DATE_TIME) ---> " + ("2020-02-02".matches(DATE_TIME))); + System.out.println("\"2020-02-02 \".matches(DATE_TIME) ---> " + ("2020-02-02 ".matches(DATE_TIME))); + System.out.println("\"2020-02-02 23:32\".matches(DATE_TIME) ---> " + ("2020-02-02 23:32".matches(DATE_TIME))); + System.out.println("\"2020-02-02 23:32:21\".matches(DATE_TIME) ---> " + ("2020-02-02 23:32:21".matches(DATE_TIME))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(DATE_TIME_HM) ---> " + ("汉字".matches(DATE_TIME_HM))); + System.out.println("\"abc\".matches(DATE_TIME_HM) ---> " + ("abc".matches(DATE_TIME_HM))); + System.out.println("\"123\".matches(DATE_TIME_HM) ---> " + ("123".matches(DATE_TIME_HM))); + System.out.println("\"2020-02-02\".matches(DATE_TIME_HM) ---> " + ("2020-02-02".matches(DATE_TIME_HM))); + System.out.println("\"2020-02-02 \".matches(DATE_TIME_HM) ---> " + ("2020-02-02 ".matches(DATE_TIME_HM))); + System.out.println("\"2020-02-02 23:32\".matches(DATE_TIME_HM) ---> " + ("2020-02-02 23:32".matches(DATE_TIME_HM))); + System.out.println("\"2020-02-02 23:32:21\".matches(DATE_TIME_HM) ---> " + ("2020-02-02 23:32:21".matches(DATE_TIME_HM))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(DATE_TIME_HMS) ---> " + ("汉字".matches(DATE_TIME_HMS))); + System.out.println("\"abc\".matches(DATE_TIME_HMS) ---> " + ("abc".matches(DATE_TIME_HMS))); + System.out.println("\"123\".matches(DATE_TIME_HMS) ---> " + ("123".matches(DATE_TIME_HMS))); + System.out.println("\"2020-02-02\".matches(DATE_TIME_HMS) ---> " + ("2020-02-02".matches(DATE_TIME_HMS))); + System.out.println("\"2020-02-02 \".matches(DATE_TIME_HMS) ---> " + ("2020-02-02 ".matches(DATE_TIME_HMS))); + System.out.println("\"2020-02-02 23:32\".matches(DATE_TIME_HMS) ---> " + ("2020-02-02 23:32".matches(DATE_TIME_HMS))); + System.out.println("\"2020-02-02 23:32:21\".matches(DATE_TIME_HMS) ---> " + ("2020-02-02 23:32:21".matches(DATE_TIME_HMS))); + System.out.println(); + + + + System.out.println("\"汉字\".matches(IP) ---> " + ("汉字".matches(IP))); + System.out.println("\"abc\".matches(IP) ---> " + ("abc".matches(IP))); + System.out.println("\"123\".matches(IP) ---> " + ("123".matches(IP))); + System.out.println("\"123.123.123\".matches(IP) ---> " + ("123.123.123".matches(IP))); + System.out.println("\"123.123.123.123\".matches(IP) ---> " + ("123.123.123.123".matches(IP))); + System.out.println("\"255.255.255.255\".matches(IP) ---> " + ("255.255.255.255".matches(IP))); + System.out.println("\"255.255.256.255\".matches(IP) ---> " + ("255.255.256.255".matches(IP))); + System.out.println("\"0.0.0.0\".matches(IP) ---> " + ("0.0.0.0".matches(IP))); + } + + + +} diff --git a/src/main/java/io/jboot/web/validate/RegexForm.java b/src/main/java/io/jboot/web/validate/RegexForm.java new file mode 100644 index 0000000000000000000000000000000000000000..2db227a5403c0c475b19aafac587397925b13195 --- /dev/null +++ b/src/main/java/io/jboot/web/validate/RegexForm.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.validate; + +import java.lang.annotation.*; + +/** + * 非空验证注解 + * @author michael yang + */ +@Documented +@Target(ElementType.METHOD) +@Inherited +@Retention(RetentionPolicy.RUNTIME) +public @interface RegexForm { + + String name(); + + String regex(); + + String message() default ""; + + String type() default FormType.FORM_DATA; + + int errorCode() default 400; +} diff --git a/src/main/java/io/jboot/web/validate/UrlParaValidate.java b/src/main/java/io/jboot/web/validate/RegexValidate.java similarity index 85% rename from src/main/java/io/jboot/web/validate/UrlParaValidate.java rename to src/main/java/io/jboot/web/validate/RegexValidate.java index 522bb5833f2d503c0fe1384408a4662e659d24ea..6bf4a3d70c6d5ca9235b858fe87efc3a87996713 100644 --- a/src/main/java/io/jboot/web/validate/UrlParaValidate.java +++ b/src/main/java/io/jboot/web/validate/RegexValidate.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,14 +18,16 @@ package io.jboot.web.validate; import java.lang.annotation.*; /** - * Url Para 非空验证 + * 正则验证注解 * @author michael yang */ @Documented @Target(ElementType.METHOD) @Inherited @Retention(RetentionPolicy.RUNTIME) -public @interface UrlParaValidate { +public @interface RegexValidate { + + RegexForm[] value(); String message() default ""; @@ -35,6 +37,4 @@ public @interface UrlParaValidate { String renderType() default ValidateRenderType.DEFAULT; - int errorCode() default 1; - } diff --git a/src/main/java/io/jboot/web/validate/ValidateRenderType.java b/src/main/java/io/jboot/web/validate/ValidateRenderType.java index 8b02f419c9997e7685342a740f19c14564e0ee83..90dc183bc6ef3c8ba393ea3a3decded3f56d1d09 100644 --- a/src/main/java/io/jboot/web/validate/ValidateRenderType.java +++ b/src/main/java/io/jboot/web/validate/ValidateRenderType.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package io.jboot.web.validate; /** * @author Michael Yang 杨福海 (fuhai999@gmail.com) * @version V1.0 - * @Package io.jboot.web.controller.validate */ public class ValidateRenderType { diff --git a/src/main/java/io/jboot/web/validate/interceptor/CaptchaValidateInterceptor.java b/src/main/java/io/jboot/web/validate/interceptor/CaptchaValidateInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..df18e571ccccaf17cdc784cbca436ceadd0ff739 --- /dev/null +++ b/src/main/java/io/jboot/web/validate/interceptor/CaptchaValidateInterceptor.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.validate.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.core.Controller; +import com.jfinal.log.Log; +import io.jboot.Jboot; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.StrUtil; +import io.jboot.web.validate.CaptchaValidate; + +/** + * 验证拦截器 + */ +public class CaptchaValidateInterceptor implements Interceptor { + + private static final Log LOG = Log.getLog("Validate"); + + @Override + public void intercept(Invocation inv) { + + CaptchaValidate captchaValidate = inv.getMethod().getAnnotation(CaptchaValidate.class); + if (captchaValidate != null && !validateCaptache(inv, captchaValidate)) { + if (Jboot.isDevMode()){ + LOG.error(ValidateInterceptorUtil.buildErrorMessage(inv,"@CaptchaValidate")); + } + return; + } + + inv.invoke(); + } + + + /** + * 对验证码进行验证 + * + * @param inv + * @param captchaValidate + * @return + */ + private boolean validateCaptache(Invocation inv, CaptchaValidate captchaValidate) { + String formName = AnnotationUtil.get(captchaValidate.form()); + if (StrUtil.isBlank(formName)) { + throw new IllegalArgumentException("@CaptchaValidate.form must not be empty in " + inv.getController().getClass().getName() + "." + inv.getMethodName()); + } + + + Controller controller = inv.getController(); + if (controller.validateCaptcha(formName)) { + return true; + } + + ValidateInterceptorUtil.renderValidException(inv.getController() + , AnnotationUtil.get(captchaValidate.renderType()) + , formName + , AnnotationUtil.get(captchaValidate.message()) + , AnnotationUtil.get(captchaValidate.redirectUrl()) + , AnnotationUtil.get(captchaValidate.htmlPath()) + , captchaValidate.errorCode() + ); + + return false; + } + + + + +} diff --git a/src/main/java/io/jboot/web/validate/ParaValidateInterceptor.java b/src/main/java/io/jboot/web/validate/interceptor/EmptyValidateInterceptor.java similarity index 34% rename from src/main/java/io/jboot/web/validate/ParaValidateInterceptor.java rename to src/main/java/io/jboot/web/validate/interceptor/EmptyValidateInterceptor.java index 1413e51bf87f3114fd42654adea109ee29a4576a..f03ed4ee5c8222a16d4f1d004982a4dbd895c295 100644 --- a/src/main/java/io/jboot/web/validate/ParaValidateInterceptor.java +++ b/src/main/java/io/jboot/web/validate/interceptor/EmptyValidateInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,102 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.jboot.web.validate; +package io.jboot.web.validate.interceptor; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONPath; +import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; -import com.jfinal.core.Controller; -import com.jfinal.kit.Ret; +import com.jfinal.log.Log; +import io.jboot.Jboot; import io.jboot.utils.AnnotationUtil; import io.jboot.utils.ArrayUtil; -import io.jboot.utils.RequestUtil; import io.jboot.utils.StrUtil; -import io.jboot.web.fixedinterceptor.FixedInterceptor; - -import java.lang.reflect.Method; +import io.jboot.web.validate.EmptyValidate; +import io.jboot.web.validate.Form; +import io.jboot.web.validate.FormType; /** * 验证拦截器 */ -public class ParaValidateInterceptor implements FixedInterceptor { +public class EmptyValidateInterceptor implements Interceptor { + + private static final Log LOG = Log.getLog("Validate"); @Override public void intercept(Invocation inv) { - Method method = inv.getMethod(); - - UrlParaValidate urlParaValidate = method.getAnnotation(UrlParaValidate.class); - if (urlParaValidate != null && !validateUrlPara(inv, urlParaValidate)) { - return; - } - - - EmptyValidate emptyParaValidate = method.getAnnotation(EmptyValidate.class); + EmptyValidate emptyParaValidate = inv.getMethod().getAnnotation(EmptyValidate.class); if (emptyParaValidate != null && !validateEmpty(inv, emptyParaValidate)) { - return; - } - - CaptchaValidate captchaValidate = method.getAnnotation(CaptchaValidate.class); - if (captchaValidate != null && !validateCaptache(inv, captchaValidate)) { + if (Jboot.isDevMode()){ + LOG.error(ValidateInterceptorUtil.buildErrorMessage(inv,"@EmptyValidate")); + } return; } inv.invoke(); - - } - - private boolean validateUrlPara(Invocation inv, UrlParaValidate urlParaValidate) { - Controller controller = inv.getController(); - if (controller.getPara() != null) { - return true; - } - - - renderError(inv.getController() - , AnnotationUtil.get(urlParaValidate.renderType()) - , null - , AnnotationUtil.get(urlParaValidate.message()) - , AnnotationUtil.get(urlParaValidate.redirectUrl()) - , AnnotationUtil.get(urlParaValidate.htmlPath()) - , urlParaValidate.errorCode() - ); - - - return false; - } - - - /** - * 对验证码进行验证 - * - * @param inv - * @param captchaValidate - * @return - */ - private boolean validateCaptache(Invocation inv, CaptchaValidate captchaValidate) { - String formName = AnnotationUtil.get(captchaValidate.form()); - if (StrUtil.isBlank(formName)) { - throw new IllegalArgumentException("@CaptchaValidate.form must not be empty in " + inv.getController().getClass().getName() + "." + inv.getMethodName()); - } - - - Controller controller = inv.getController(); - if (controller.validateCaptcha(formName)) { - return true; - } - - renderError(inv.getController() - , AnnotationUtil.get(captchaValidate.renderType()) - , formName - , AnnotationUtil.get(captchaValidate.message()) - , AnnotationUtil.get(captchaValidate.redirectUrl()) - , AnnotationUtil.get(captchaValidate.htmlPath()) - , captchaValidate.errorCode() - ); - - return false; } @@ -126,41 +65,42 @@ public class ParaValidateInterceptor implements FixedInterceptor { } - for (Form form : forms) { - String formName = AnnotationUtil.get(form.name()); - String formType = AnnotationUtil.get(form.type()); + for (Form formAnnotation : forms) { + String formName = AnnotationUtil.get(formAnnotation.name()); + String formType = AnnotationUtil.get(formAnnotation.type()); if (StrUtil.isBlank(formName)) { throw new IllegalArgumentException("@Form.value must not be empty in " + inv.getController().getClass().getName() + "." + inv.getMethodName()); } - String value = null; + String paraValue = null; if (FormType.FORM_DATA.equalsIgnoreCase(formType)) { - value = inv.getController().getPara(formName); + paraValue = inv.getController().getPara(formName); } else if (FormType.RAW_DATA.equalsIgnoreCase(formType)) { try { JSONObject json = JSON.parseObject(inv.getController().getRawData()); if (json != null) { Object tmp = JSONPath.eval(json, "$." + formName); if (tmp != null) { - value = tmp.toString(); + paraValue = tmp.toString(); } } } catch (Exception e) { - value = null; + paraValue = null; } } else { - throw new IllegalArgumentException("para validate not support form type : " + formType + ", " + - "see : io.jboot.web.controller.validate.FormType"); + throw new IllegalArgumentException("@EmptyValidate not support form type : " + formType + ", " + + "see: io.jboot.web.controller.validate.FormType"); } - if (value == null || value.trim().length() == 0) { - renderError(inv.getController() + if (paraValue == null || paraValue.trim().length() == 0) { + ValidateInterceptorUtil.renderValidException(inv.getController() , AnnotationUtil.get(emptyParaValidate.renderType()) , formName - , AnnotationUtil.get(form.message()) + , AnnotationUtil.get(formAnnotation.message()) , AnnotationUtil.get(emptyParaValidate.redirectUrl()) , AnnotationUtil.get(emptyParaValidate.htmlPath()) - , form.errorCode() + , formAnnotation.errorCode() ); + return false; } } @@ -169,39 +109,7 @@ public class ParaValidateInterceptor implements FixedInterceptor { } - private void renderError(Controller controller, String renderType, String formName, String message, String redirectUrl, String htmlPath, int errorCode) { - switch (renderType) { - case ValidateRenderType.DEFAULT: - if (RequestUtil.isAjaxRequest(controller.getRequest())) { - controller.renderJson( - Ret.fail("message", message) - .set("errorCode", errorCode) - .setIfNotNull("formName", formName) - ); - } else { - controller.renderError(404); - } - break; - case ValidateRenderType.JSON: - controller.renderJson( - Ret.fail("message", message) - .set("errorCode", errorCode) - .setIfNotNull("formName", formName) - ); - break; - case ValidateRenderType.REDIRECT: - controller.redirect(redirectUrl); - break; - case ValidateRenderType.HTML: - controller.render(htmlPath); - break; - case ValidateRenderType.TEXT: - controller.renderText(message); - break; - default: - throw new IllegalArgumentException("can not process render : " + renderType); - } - } + } diff --git a/src/main/java/io/jboot/web/validate/interceptor/RegexValidateInterceptor.java b/src/main/java/io/jboot/web/validate/interceptor/RegexValidateInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..036952aa8670819485da154437c79d34b6ecbf2e --- /dev/null +++ b/src/main/java/io/jboot/web/validate/interceptor/RegexValidateInterceptor.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.validate.interceptor; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.JSONPath; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.log.Log; +import io.jboot.Jboot; +import io.jboot.utils.AnnotationUtil; +import io.jboot.utils.ArrayUtil; +import io.jboot.utils.StrUtil; +import io.jboot.web.validate.FormType; +import io.jboot.web.validate.RegexForm; +import io.jboot.web.validate.RegexValidate; + +/** + * 验证拦截器 + */ +public class RegexValidateInterceptor implements Interceptor { + + private static final Log LOG = Log.getLog("Validate"); + + @Override + public void intercept(Invocation inv) { + + RegexValidate regexValidate = inv.getMethod().getAnnotation(RegexValidate.class); + if (regexValidate != null && !validateRegex(inv, regexValidate)) { + if (Jboot.isDevMode()){ + LOG.error(ValidateInterceptorUtil.buildErrorMessage(inv,"@RegexValidate")); + } + return; + } + + inv.invoke(); + } + + + + + /** + * 正则验证 + * + * @param inv + * @param regexValidate + * @return + */ + private boolean validateRegex(Invocation inv, RegexValidate regexValidate) { + RegexForm[] forms = regexValidate.value(); + if (ArrayUtil.isNullOrEmpty(forms)) { + return true; + } + + + for (RegexForm form : forms) { + String formName = AnnotationUtil.get(form.name()); + String formType = AnnotationUtil.get(form.type()); + if (StrUtil.isBlank(formName)) { + throw new IllegalArgumentException("@RegexForm.value must not be empty in " + inv.getController().getClass().getName() + "." + inv.getMethodName()); + } + String value = null; + if (FormType.FORM_DATA.equalsIgnoreCase(formType)) { + value = inv.getController().getPara(formName); + } else if (FormType.RAW_DATA.equalsIgnoreCase(formType)) { + try { + JSONObject json = JSON.parseObject(inv.getController().getRawData()); + if (json != null) { + Object tmp = JSONPath.eval(json, "$." + formName); + if (tmp != null) { + value = tmp.toString(); + } + } + } catch (Exception e) { + value = null; + } + } else { + throw new IllegalArgumentException("@RegexValidate not support form type : " + formType + ", " + + "see : io.jboot.web.controller.validate.FormType"); + } + + if (value == null || !value.trim().matches(form.regex())) { + ValidateInterceptorUtil.renderValidException(inv.getController() + , AnnotationUtil.get(regexValidate.renderType()) + , formName + , AnnotationUtil.get(form.message()) + , AnnotationUtil.get(regexValidate.redirectUrl()) + , AnnotationUtil.get(regexValidate.htmlPath()) + , form.errorCode() + ); + return false; + } + } + + return true; + } + + + + + +} diff --git a/src/main/java/io/jboot/web/validate/interceptor/ValidateInterceptorBuilder.java b/src/main/java/io/jboot/web/validate/interceptor/ValidateInterceptorBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..722bf5e6b9b05d783abd96c84aba65fbb799f697 --- /dev/null +++ b/src/main/java/io/jboot/web/validate/interceptor/ValidateInterceptorBuilder.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.validate.interceptor; + +import io.jboot.aop.InterceptorBuilder; +import io.jboot.aop.Interceptors; +import io.jboot.aop.annotation.AutoLoad; +import io.jboot.web.validate.CaptchaValidate; +import io.jboot.web.validate.EmptyValidate; +import io.jboot.web.validate.RegexValidate; + +import java.lang.reflect.Method; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +@AutoLoad +public class ValidateInterceptorBuilder implements InterceptorBuilder { + + @Override + public void build(Class targetClass, Method method, Interceptors interceptors) { + if (Util.isController(targetClass) && Util.hasAnnotation(method, EmptyValidate.class)) { + interceptors.add(EmptyValidateInterceptor.class); + } + + if (Util.isController(targetClass) && Util.hasAnnotation(method, RegexValidate.class)) { + interceptors.add(RegexValidateInterceptor.class); + } + + if (Util.isController(targetClass) && Util.hasAnnotation(method, CaptchaValidate.class)) { + interceptors.add(CaptchaValidateInterceptor.class); + } + } +} diff --git a/src/main/java/io/jboot/web/validate/interceptor/ValidateInterceptorUtil.java b/src/main/java/io/jboot/web/validate/interceptor/ValidateInterceptorUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..086d9f13b01aea1203ffeba4f253ae1c0d2a04cd --- /dev/null +++ b/src/main/java/io/jboot/web/validate/interceptor/ValidateInterceptorUtil.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.validate.interceptor; + +import com.jfinal.aop.Invocation; +import com.jfinal.core.Controller; +import com.jfinal.kit.Ret; +import io.jboot.utils.RequestUtil; +import io.jboot.utils.StrUtil; +import io.jboot.web.validate.ValidateRenderType; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class ValidateInterceptorUtil { + + + /** + * 通过自定义的 ValidExceptionRetBuilder,可以自定义验证错误输出的 json 内容 + */ + private static ValidExceptionRetBuilder validExceptionRetBuilder = ValidExceptionRetBuilder.DEFAULT_BUILDER; + + public static ValidExceptionRetBuilder getValidExceptionRetBuilder() { + return validExceptionRetBuilder; + } + + public static void setValidExceptionRetBuilder(ValidExceptionRetBuilder validExceptionRetBuilder) { + ValidateInterceptorUtil.validExceptionRetBuilder = validExceptionRetBuilder; + } + + + static String buildErrorMessage(Invocation inv, String annotation) { + StringBuilder sb = new StringBuilder(); + sb.append("method \"").append(inv.getController().getClass().getName()) + .append(".") + .append(inv.getMethodName()) + .append("()\"") + .append(" has intercepted by annotation ") + .append(annotation); + return sb.toString(); + } + + + static void renderValidException(Controller controller, String renderType, String formName, String message, String redirectUrl, String htmlPath, int errorCode) { + String reason = StrUtil.isNotBlank(message) ? (formName + " validate failed: " + message) : (formName + " validate failed."); + switch (renderType) { + case ValidateRenderType.DEFAULT: + if (RequestUtil.isAjaxRequest(controller.getRequest()) + || RequestUtil.isJsonContentType(controller.getRequest())) { + + Ret baseRet = Ret.fail("message", message) + .set("reason", reason) + .set("errorCode", errorCode) + .setIfNotNull("formName", formName); + + Ret ret = validExceptionRetBuilder.build(baseRet); + controller.renderJson(ret); + } else { + controller.renderText(reason); + } + break; + case ValidateRenderType.JSON: + Ret baseRet = Ret.fail("message", message) + .set("reason", reason) + .set("errorCode", errorCode) + .setIfNotNull("formName", formName); + Ret ret = validExceptionRetBuilder.build(baseRet); + controller.renderJson(ret); + break; + case ValidateRenderType.REDIRECT: + controller.redirect(redirectUrl); + break; + case ValidateRenderType.HTML: + controller.render(htmlPath); + break; + case ValidateRenderType.TEXT: + controller.renderText(message); + break; + default: + throw new IllegalArgumentException("Can not process render : " + renderType); + } + } + + + public static interface ValidExceptionRetBuilder { + + ValidExceptionRetBuilder DEFAULT_BUILDER = ret -> ret; + + Ret build(Ret baseRet); + } + + +} diff --git a/src/main/java/io/jboot/web/xss/XSSHandler.java b/src/main/java/io/jboot/web/xss/XSSHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..a6e29442772b1dd4a8f3104537bf215f2b255035 --- /dev/null +++ b/src/main/java/io/jboot/web/xss/XSSHandler.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.xss; + +import com.jfinal.handler.Handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class XSSHandler extends Handler { + + @Override + public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { + next.handle(target, new XSSHttpServletRequestWrapper(request), response, isHandled); + } + +} diff --git a/src/main/java/io/jboot/web/xss/XSSHttpServletRequestWrapper.java b/src/main/java/io/jboot/web/xss/XSSHttpServletRequestWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..21f3410066a945c5b3bffda776c573dc062b0c7b --- /dev/null +++ b/src/main/java/io/jboot/web/xss/XSSHttpServletRequestWrapper.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.web.xss; + +import io.jboot.utils.StrUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +public class XSSHttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper { + + public XSSHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public String getParameter(String name) { + return cleanXss(super.getParameter(name)); + + } + + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (null == values) { + return null; + } + for (int i = 0; i < values.length; i++) { + values[i] = cleanXss(values[i]); + } + return values; + } + + + @Override + public String getHeader(String name) { + return cleanXss(super.getHeader(name)); + } + + + @Override + public Map getParameterMap() { + Map paraMap = super.getParameterMap(); + if (null == paraMap || paraMap.isEmpty()) { + return paraMap; + } + + Map ret = new HashMap<>(paraMap.size()); + for (Map.Entry entry : paraMap.entrySet()) { + String[] values = entry.getValue(); + if (null == values || values.length == 0) { + ret.put(entry.getKey(),values); + }else { + String[] newValues = new String[values.length]; + for (int i = 0; i < values.length; i++) { + newValues[i] = cleanXss(values[i]); + } + ret.put(entry.getKey(),newValues); + } + } + return ret; + } + + private static String cleanXss(String para) { + return StrUtil.escapeHtml(para); + } +} \ No newline at end of file diff --git a/src/main/java/io/jboot/wechat/JbootWechatConfig.java b/src/main/java/io/jboot/wechat/JbootWechatConfig.java index c63619fed30d306677d6896822fd8c548214289c..aff4243bbf927325cbf3a4b715cc3fdf71cebb8e 100644 --- a/src/main/java/io/jboot/wechat/JbootWechatConfig.java +++ b/src/main/java/io/jboot/wechat/JbootWechatConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/wechat/WechatApis.java b/src/main/java/io/jboot/wechat/WechatApis.java index 332839ed86a8b2e8ba0d5ba0f3f37f8c93b3e580..4c8b3b6df305b094421e5b4dc00e711394ce26b3 100644 --- a/src/main/java/io/jboot/wechat/WechatApis.java +++ b/src/main/java/io/jboot/wechat/WechatApis.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/io/jboot/wechat/WechatSupport.java b/src/main/java/io/jboot/wechat/WechatSupport.java new file mode 100644 index 0000000000000000000000000000000000000000..d8a375dcae378c7d3abae90a067a1a240afcccc2 --- /dev/null +++ b/src/main/java/io/jboot/wechat/WechatSupport.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.wechat; + +import com.jfinal.weixin.sdk.api.ApiConfigKit; +import io.jboot.components.cache.support.WechatAccessTokenCache; + +public class WechatSupport { + + public void autoSupport() { + ApiConfigKit.setAccessTokenCache(new WechatAccessTokenCache()); + } +} diff --git a/src/main/java/io/jboot/wechat/controller/JbootWechatController.java b/src/main/java/io/jboot/wechat/controller/JbootWechatController.java index e496442af7b79756992609c1d3998ce690674b6a..4f51cc89d5d1818b77b970a9ea3e63c9d1dc125b 100644 --- a/src/main/java/io/jboot/wechat/controller/JbootWechatController.java +++ b/src/main/java/io/jboot/wechat/controller/JbootWechatController.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -192,7 +192,7 @@ public abstract class JbootWechatController extends JbootController { } @NotAction - public void doNotAlloVisitRedirect() { + public void doNotAllowVisitRedirect() { /** * 一般情况下,此方法是为了调整到其他页面,比如让用户扫描二维码之类的 * 由子类去实现 diff --git a/src/main/java/io/jboot/wechat/interceptor/WechatApiConfigInterceptor.java b/src/main/java/io/jboot/wechat/interceptor/WechatApiConfigInterceptor.java index 4dcc49768a4ce2cf4b9c4b29f07a8400f050a69b..f42b3391d907f26f2d7e2dba498fe8954a0c45ec 100644 --- a/src/main/java/io/jboot/wechat/interceptor/WechatApiConfigInterceptor.java +++ b/src/main/java/io/jboot/wechat/interceptor/WechatApiConfigInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,9 @@ public class WechatApiConfigInterceptor implements Interceptor { ApiConfig config = controller.getApiConfig(); if (config == null) { - inv.getController().renderText("error : cannot get apiconfig,please config jboot.properties"); + inv.getController().renderText("Error: Can not get apiconfig, please config jboot.properties"); return; } - ApiConfigKit.setThreadLocalAppId(config.getAppId()); inv.invoke(); } finally { diff --git a/src/main/java/io/jboot/wechat/interceptor/WechatUserInterceptor.java b/src/main/java/io/jboot/wechat/interceptor/WechatUserInterceptor.java index 0d122d4babd243dba604cddf2d0c9fe3d5cd6832..51e50e6e10f8368602168f4a7d8bd430998b270e 100644 --- a/src/main/java/io/jboot/wechat/interceptor/WechatUserInterceptor.java +++ b/src/main/java/io/jboot/wechat/interceptor/WechatUserInterceptor.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-2016, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ public class WechatUserInterceptor implements Interceptor { return; } - controller.doNotAlloVisitRedirect(); + controller.doNotAllowVisitRedirect(); } @@ -120,7 +120,7 @@ public class WechatUserInterceptor implements Interceptor { } - String controllerKey = inv.getControllerKey(); + String controllerKey = inv.getControllerPath(); String callbackControllerKey = controllerKey + "/wechatCallback"; if (!JFinal.me().getAllActionKeys().contains(callbackControllerKey)) { diff --git a/src/main/resources/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Filter b/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.Filter similarity index 97% rename from src/main/resources/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Filter rename to src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.Filter index ba82ef1efcc97f3b253a1a42bad84bc701cd225d..938d350fc5340498e335f285c3a2fae2ce81df77 100644 --- a/src/main/resources/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Filter +++ b/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.Filter @@ -1,2 +1,2 @@ -seata=io.jboot.support.seata.filter.TransactionPropagationFilter - +seata=io.jboot.support.seata.filter.TransactionPropagationFilter + diff --git a/src/main/resources/META-INF/seata/io.seata.core.model.ResourceManager b/src/main/resources/META-INF/seata/io.seata.core.model.ResourceManager new file mode 100644 index 0000000000000000000000000000000000000000..e6494fdd8e4303fa4453f6b0dd4c47432472441d --- /dev/null +++ b/src/main/resources/META-INF/seata/io.seata.core.model.ResourceManager @@ -0,0 +1 @@ +io.jboot.support.seata.tcc.JbootTCCResourceManager \ No newline at end of file diff --git a/src/test/java/io/jboot/test/ClassScannerTester.java b/src/test/java/io/jboot/test/ClassScannerTester.java new file mode 100644 index 0000000000000000000000000000000000000000..8d967cde080bf4c0b3085a981e1ed8eba94002fc --- /dev/null +++ b/src/test/java/io/jboot/test/ClassScannerTester.java @@ -0,0 +1,23 @@ +package io.jboot.test; + +import io.jboot.app.JbootApplication; +import io.jboot.core.listener.JbootAppListener; +import io.jboot.utils.ClassScanner; + +public class ClassScannerTester implements JbootAppListener { + + public static void main(String[] args) { + +// ClassScanner.addUnscanJarPrefix("encoder-"); +// ClassScanner.addUnscanJarPrefix("reflections-"); + +// JbootApplication.setBootArg("jboot.app.scanner.unScanJarPrefix","encoder-,reflections-"); + ClassScanner.setPrintScannerInfoEnable(true); + JbootApplication.run(args); + } + + @Override + public void onStart() { + + } +} diff --git a/src/test/java/io/jboot/test/HelloInterceptor.java b/src/test/java/io/jboot/test/HelloInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..801a20c812b90267b561dcbd56bc3651b31f2a59 --- /dev/null +++ b/src/test/java/io/jboot/test/HelloInterceptor.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; + +import javax.annotation.PostConstruct; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2019/12/12 + */ +public class HelloInterceptor implements Interceptor { + + @PostConstruct + public void aaa(){ + System.out.println("HelloInterceptor --->>> aaaa() invoked!"); + } + + @Override + public void intercept(Invocation inv) { + System.out.println("HelloInterceptor.intercept"); + inv.invoke(); + } +} \ No newline at end of file diff --git a/src/test/java/io/jboot/test/HelloWorld.java b/src/test/java/io/jboot/test/HelloWorld.java index bc0979f00461444afba863facf1324d97d4e77fe..198254494b731e67e8d73639e08b0639eb8a026e 100644 --- a/src/test/java/io/jboot/test/HelloWorld.java +++ b/src/test/java/io/jboot/test/HelloWorld.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2019, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -15,6 +15,9 @@ */ package io.jboot.test; +import com.jfinal.aop.Before; +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; import io.jboot.app.JbootApplication; import io.jboot.web.controller.JbootController; import io.jboot.web.controller.annotation.RequestMapping; @@ -26,11 +29,37 @@ import io.jboot.web.controller.annotation.RequestMapping; @RequestMapping("/hello") public class HelloWorld extends JbootController { + @Before({MyInterceptor.class,HelloInterceptor.class}) public void index(){ renderText("hello world"); +// throw new NullPointerException(); + } + + public void ex(){ + throw new NullPointerException("log test"); + } + + + @Before({HelloInterceptor.class}) + public void abc(String abc, long number){ + renderText("abc"); } public static void main(String[] args){ JbootApplication.run(args); } + + + public static class MyInterceptor implements Interceptor { + + @Override + public void intercept(Invocation inv) { + System.out.println("MyInterceptor.intercept"); +// throw new NullPointerException(""); +// inv.getController().renderText("aaa"); + inv.invoke(); + } + } + + } \ No newline at end of file diff --git a/src/test/java/io/jboot/test/JavaTester.java b/src/test/java/io/jboot/test/JavaTester.java index cd409280017e98efa9ee513fa4b59b7429eaa0d7..7a8fe241e7f90eea131b1bb9359594e69d90bcea 100644 --- a/src/test/java/io/jboot/test/JavaTester.java +++ b/src/test/java/io/jboot/test/JavaTester.java @@ -9,5 +9,4 @@ public class JavaTester { } - } diff --git a/src/test/java/io/jboot/test/TestAppStater.java b/src/test/java/io/jboot/test/TestAppStater.java index 270b8c2410e9f9d2883c9b7e5fb459925dfef949..640e36c50ad580cb77a5ac47f45be88dae022a80 100644 --- a/src/test/java/io/jboot/test/TestAppStater.java +++ b/src/test/java/io/jboot/test/TestAppStater.java @@ -2,7 +2,6 @@ package io.jboot.test; import io.jboot.app.JbootApplication; - public class TestAppStater { public static void main(String[] args){ diff --git a/src/test/java/io/jboot/test/aop/AopConfiguration.java b/src/test/java/io/jboot/test/aop/AopConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..c255a29c4a8bf598e35e99bc7ee0f31d5463f081 --- /dev/null +++ b/src/test/java/io/jboot/test/aop/AopConfiguration.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.aop; + +import io.jboot.aop.annotation.Bean; +import io.jboot.aop.annotation.Configuration; +import io.jboot.test.aop.cache.CommentService; +import io.jboot.test.aop.cache.CommentServiceImpl; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/11 + */ +@Configuration +public class AopConfiguration { + + @Bean(name = "myCommentServiceFromConfiguration") + public CommentService myCommentService(){ + CommentService commentService = new CommentServiceImpl(); + System.out.println("myCommentService:" + commentService); + return commentService; + } + + @Bean() + public CommentService myCommentService1(){ + CommentService commentService = new CommentServiceImpl(); + System.out.println("myCommentService:" + commentService); + return commentService; + } +} diff --git a/src/test/java/io/jboot/test/aop/FilterController.java b/src/test/java/io/jboot/test/aop/FilterController.java new file mode 100644 index 0000000000000000000000000000000000000000..4608eebd5ab64ca90354c1555769cb597499f38e --- /dev/null +++ b/src/test/java/io/jboot/test/aop/FilterController.java @@ -0,0 +1,54 @@ +package io.jboot.test.aop; + +import com.jfinal.core.Controller; +import io.jboot.aop.ValueFilter; +import io.jboot.aop.annotation.DefaultValue; +import io.jboot.aop.annotation.FilterBy; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/aop/filter") +public class FilterController extends Controller { + + public void test1(@FilterBy(Filter1.class) String orderBy) { + renderText(orderBy); + } + + public void test2(@FilterBy({Filter1.class, Filter2.class}) String orderBy) { + renderText(orderBy); + } + + + public void test3(@FilterBy({Filter3.class}) String orderBy) { + renderText(orderBy); + } + + public void test4(@FilterBy({Filter3.class}) @DefaultValue("id desc") String orderBy) { + renderText(orderBy); + } + + + public static class Filter1 implements ValueFilter { + + @Override + public Object doFilter(Object orignal) { + return orignal + "filter1"; + } + } + + public static class Filter2 implements ValueFilter { + + @Override + public Object doFilter(Object orignal) { + return orignal + "filter2"; + } + } + + + public static class Filter3 implements ValueFilter { + + @Override + public Object doFilter(Object orignal) { + return null; + } + } +} diff --git a/src/test/java/io/jboot/test/aop/InterceptorsTest.java b/src/test/java/io/jboot/test/aop/InterceptorsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0e6b0649ac77f3370dd756f9e7d317e1669ce79f --- /dev/null +++ b/src/test/java/io/jboot/test/aop/InterceptorsTest.java @@ -0,0 +1,46 @@ +package io.jboot.test.aop; + +import com.jfinal.aop.Interceptor; +import io.jboot.aop.Interceptors; +import io.jboot.test.HelloInterceptor; + +import java.util.Arrays; +import java.util.function.Predicate; + +public class InterceptorsTest { + + public static void main(String[] args) { + + Interceptors interceptors = new Interceptors(); + + interceptors.add(new NameInterceptor("name1")); + interceptors.add(new NameInterceptor("name2")); + interceptors.add(new NameInterceptor("name3")); + interceptors.addToFirst(new NameInterceptor("name0")); + interceptors.addToFirst(new NameInterceptor("name-1")); + interceptors.add(new Name1Interceptor("Name1Interceptor")); + interceptors.add(new NameInterceptor("name5")); + interceptors.add(new NameInterceptor("name6")); + interceptors.addBefore(new NameInterceptor("name1Before"),Name1Interceptor.class); + System.out.println(interceptors.addAfter(new NameInterceptor("name1Afater"),Name1Interceptor.class)); + System.out.println(interceptors.addAfter(new NameInterceptor("HelloInterceptorAfater"), HelloInterceptor.class)); + interceptors.addAfter(new NameInterceptor("name1.5"), interceptor -> { + if (interceptor instanceof NameInterceptor){ + NameInterceptor i = (NameInterceptor) interceptor; + return i.getName().equals("name1"); + } + return false; + }); + + + interceptors.addBefore(new NameInterceptor("name2.5"), interceptor -> { + if (interceptor instanceof NameInterceptor){ + NameInterceptor i = (NameInterceptor) interceptor; + return i.getName().equals("name3"); + } + return false; + }); + + System.out.println(Arrays.toString(interceptors.toArray())); + } +} diff --git a/src/test/java/io/jboot/test/aop/LayzService1.java b/src/test/java/io/jboot/test/aop/LayzService1.java new file mode 100644 index 0000000000000000000000000000000000000000..21076f5e8021169ae27b866792dd78ea27354f60 --- /dev/null +++ b/src/test/java/io/jboot/test/aop/LayzService1.java @@ -0,0 +1,6 @@ +package io.jboot.test.aop; + +public interface LayzService1 { + + public void doSth(); +} diff --git a/src/test/java/io/jboot/test/aop/LayzService1Impl.java b/src/test/java/io/jboot/test/aop/LayzService1Impl.java new file mode 100644 index 0000000000000000000000000000000000000000..8e99e128cdee61d0b7ad0074c5dc27cdf1c9a537 --- /dev/null +++ b/src/test/java/io/jboot/test/aop/LayzService1Impl.java @@ -0,0 +1,17 @@ +package io.jboot.test.aop; + +import com.jfinal.aop.Before; +import io.jboot.aop.annotation.Bean; + +@Bean +public class LayzService1Impl implements LayzService1{ + + public LayzService1Impl() { + System.out.println("LayzService1Impl init..."); + } + + @Before(Name1Interceptor.class) + public void doSth(){ + System.out.println("LayzService1Impl doSth..."); + } +} diff --git a/src/test/java/io/jboot/test/aop/LayzService2.java b/src/test/java/io/jboot/test/aop/LayzService2.java new file mode 100644 index 0000000000000000000000000000000000000000..183cfcc4eae2f8185145658fe7cec50e4c2d1742 --- /dev/null +++ b/src/test/java/io/jboot/test/aop/LayzService2.java @@ -0,0 +1,7 @@ +package io.jboot.test.aop; + +public interface LayzService2 { + + + public void doSth(); +} diff --git a/src/test/java/io/jboot/test/aop/LayzService2Impl.java b/src/test/java/io/jboot/test/aop/LayzService2Impl.java new file mode 100644 index 0000000000000000000000000000000000000000..b9661477be45c4c2a1164aca24790cc0bb4a1c6a --- /dev/null +++ b/src/test/java/io/jboot/test/aop/LayzService2Impl.java @@ -0,0 +1,23 @@ +package io.jboot.test.aop; + +import com.jfinal.aop.Inject; +import io.jboot.aop.annotation.Bean; + +@Bean +public class LayzService2Impl implements LayzService2 { + + @Inject + private LayzService1 service1; + + public LayzService2Impl() { + System.out.println("LayzService2Impl init..."); +// doSth(); + } + + + public void doSth(){ + System.out.println("LayzService2Impl doSth..."); + service1.doSth(); + + } +} diff --git a/src/test/java/io/jboot/test/aop/LazyController.java b/src/test/java/io/jboot/test/aop/LazyController.java new file mode 100644 index 0000000000000000000000000000000000000000..f79675bebdda2c8340651c957f5899427e307424 --- /dev/null +++ b/src/test/java/io/jboot/test/aop/LazyController.java @@ -0,0 +1,36 @@ +package io.jboot.test.aop; + +import com.jfinal.aop.Inject; +import com.jfinal.core.Controller; +import com.jfinal.core.Path; +import io.jboot.aop.annotation.Lazy; + +@Path("lazy") +public class LazyController extends Controller { + +// @Inject +// private LayzService1 userService; + + @Inject + @Lazy + LayzService2 bService; + + public void index1() { + System.out.println(""); +// System.out.println("userService ---->" + userService); + System.out.println("bService ---->" + bService); + + renderText("index1"); + } + + public void index2() { + + bService.doSth(); + +// System.out.println("userService ---->" + userService); +// System.out.println("bService ---->" + bService); + + + renderText("index2"); + } +} diff --git a/src/test/java/io/jboot/test/aop/Name1Interceptor.java b/src/test/java/io/jboot/test/aop/Name1Interceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..daae58773916e9d15e196ef69b8d8bb4462a8ba1 --- /dev/null +++ b/src/test/java/io/jboot/test/aop/Name1Interceptor.java @@ -0,0 +1,25 @@ +package io.jboot.test.aop; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; + +public class Name1Interceptor implements Interceptor { + private String name; + public Name1Interceptor(){} + public Name1Interceptor(String name) { + this.name = name; + } + + @Override + public void intercept(Invocation inv) { + System.out.println("Name1Interceptor.intercept..."); + inv.invoke(); + } + + @Override + public String toString() { + return "NameInterceptor{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/src/test/java/io/jboot/test/aop/NameInterceptor.java b/src/test/java/io/jboot/test/aop/NameInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..319aa1dba78c405423aefdca51d2f4dec6a68cfa --- /dev/null +++ b/src/test/java/io/jboot/test/aop/NameInterceptor.java @@ -0,0 +1,28 @@ +package io.jboot.test.aop; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; + +public class NameInterceptor implements Interceptor { + private String name; + + public NameInterceptor(String name) { + this.name = name; + } + + @Override + public void intercept(Invocation inv) { + + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "NameInterceptor{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/src/test/java/io/jboot/test/aop/cache/AopCacheController.java b/src/test/java/io/jboot/test/aop/cache/AopCacheController.java index cbb8aee87a738fb7007f1b97201a09805dd97daa..08a4f2233fed0f58eee3f9a52fb2f45afc5b2e46 100644 --- a/src/test/java/io/jboot/test/aop/cache/AopCacheController.java +++ b/src/test/java/io/jboot/test/aop/cache/AopCacheController.java @@ -1,6 +1,7 @@ package io.jboot.test.aop.cache; import com.jfinal.aop.Inject; +import io.jboot.aop.annotation.Bean; import io.jboot.test.db.model.User; import io.jboot.web.controller.JbootController; import io.jboot.web.controller.annotation.RequestMapping; @@ -14,9 +15,28 @@ public class AopCacheController extends JbootController { @Inject private CommentService commentService; + @Inject + @Bean(name = "myCommentServiceFromConfiguration") + private CommentService myCommentService1; + + + @Inject + @Bean(name = "myCommentService1") + private CommentService myCommentService2; + + + @Inject + @Bean(name = "name222") + private CommentService myCommentServiceImpl222; + public void index() { - renderText("text from : " + commentService.getCommentById("index")); + System.out.println("defaultCommentService:"+commentService); + System.out.println("myCommentService1:"+myCommentService1); + System.out.println("myCommentService2:"+myCommentService2); + System.out.println("myCommentServiceImpl222:"+myCommentServiceImpl222); + + renderText("text from : " + myCommentServiceImpl222.getCommentById("index")); } diff --git a/src/test/java/io/jboot/test/aop/cache/CommentServiceImpl.java b/src/test/java/io/jboot/test/aop/cache/CommentServiceImpl.java index 4ea30f63943ef246d788a6ebd4f73f8badcb6726..2b4d3b6eb42fda1b43078224ccd8474990d206fd 100644 --- a/src/test/java/io/jboot/test/aop/cache/CommentServiceImpl.java +++ b/src/test/java/io/jboot/test/aop/cache/CommentServiceImpl.java @@ -10,12 +10,12 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; -@Bean +@Bean(name = "name333") public class CommentServiceImpl implements CommentService { @Override public String getCommentById(String id) { - return "id:" + id + " data:" + UUID.randomUUID(); + return "CommentServiceImpl --id:" + id + " data:" + UUID.randomUUID(); } @Override diff --git a/src/test/java/io/jboot/test/aop/cache/CommentServiceImpl2.java b/src/test/java/io/jboot/test/aop/cache/CommentServiceImpl2.java new file mode 100644 index 0000000000000000000000000000000000000000..9b57647170bb1456b01f18a443662e452a957f5c --- /dev/null +++ b/src/test/java/io/jboot/test/aop/cache/CommentServiceImpl2.java @@ -0,0 +1,60 @@ +package io.jboot.test.aop.cache; + +import io.jboot.aop.annotation.Bean; +import io.jboot.components.cache.annotation.CacheEvict; +import io.jboot.components.cache.annotation.CachePut; +import io.jboot.components.cache.annotation.Cacheable; +import io.jboot.test.db.model.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Bean(name = "name222") +public class CommentServiceImpl2 implements CommentService { + + @Override + public String getCommentById(String id) { + return "CommentServiceImpl2 --id:" + id + " data:" + UUID.randomUUID(); + } + + @Override + @Cacheable(name = "cacheName", key = "#(id)") + public String getCommentByIdWithCache(String id) { + return "id:" + id + " data:" + UUID.randomUUID(); + } + + + @Override + @Cacheable(name = "cacheName", key = "#(id)", liveSeconds = 5) + public String getCommentByIdWithCacheTime(String id) { + return "id:" + id + " data:" + UUID.randomUUID(); + } + + + @Override + @CachePut(name = "cacheName", key = "#(id)") + public String updateCache(String id) { + return "id:" + id + " data:" + UUID.randomUUID(); + } + + @Override + @CacheEvict(name = "cacheName", key = "#(id)") + public void delCache(String id) { + } + + @Override + @Cacheable(name = "cacheName",returnCopyEnable = true) + public List findList() { + List userList = new ArrayList<>(); + userList.add(new User()); + return userList; + } + + @Override + @Cacheable(name = "cacheName",returnCopyEnable = true) + public User[] findArray() { + User[] users = new User[]{new User()}; + return users; + } +} diff --git a/src/test/java/io/jboot/test/aop/inject/AopController.java b/src/test/java/io/jboot/test/aop/inject/AopController.java index f21061db0c27e6b616d382b0e7ec92c5eddbbd68..4a234bae0fbaeed45af9030557a33e8075a54972 100644 --- a/src/test/java/io/jboot/test/aop/inject/AopController.java +++ b/src/test/java/io/jboot/test/aop/inject/AopController.java @@ -2,6 +2,7 @@ package io.jboot.test.aop.inject; import com.jfinal.aop.Inject; import io.jboot.aop.annotation.ConfigValue; +import io.jboot.components.limiter.LimitScope; import io.jboot.components.limiter.annotation.EnableLimit; import io.jboot.test.aop.staticconstruct.StaticConstructManager; import io.jboot.web.controller.JbootController; @@ -23,7 +24,7 @@ public class AopController extends JbootController { @ConfigValue("undertow.port") private int port; - @ConfigValue(value = "undertow.xxx",requireNullOrBlank = true) + @ConfigValue(value = "undertow.xxx") private int xxx; @@ -36,6 +37,11 @@ public class AopController extends JbootController { renderText("host:" + host + " port:" + port + " xxx:" + xxx); } + @EnableLimit(rate = 1, fallback = "aaa", scope = LimitScope.CLUSTER) + public void bbb() { + renderText("host:" + host + " port:" + port + " xxx:" + xxx); + } + public void aaa(){ renderText("aaa"); } diff --git a/src/test/java/io/jboot/test/apidoc/APIController.java b/src/test/java/io/jboot/test/apidoc/APIController.java new file mode 100644 index 0000000000000000000000000000000000000000..9459ab56a914c1061a63c463a69506e37ed65ab8 --- /dev/null +++ b/src/test/java/io/jboot/test/apidoc/APIController.java @@ -0,0 +1,38 @@ +package io.jboot.test.apidoc; + +import com.jfinal.plugin.activerecord.Page; +import io.jboot.apidoc.ApiRet; +import io.jboot.apidoc.annotation.Api; +import io.jboot.apidoc.annotation.ApiOper; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.List; +import java.util.Map; + +@RequestMapping("/apidoc/test") +@Api("test") +public class APIController extends BaseController { + + @ApiOper("index") + public List index(List list) { + return null; + } + + @ApiOper("users") + public ApiRet> users(Map map) { + return null; + } + + @ApiOper("index2") + public String index2(long str) { + return null; + } + + + @ApiOper("index3") + public void index3() { + return; + } + + +} diff --git a/src/test/java/io/jboot/test/apidoc/APIController2.java b/src/test/java/io/jboot/test/apidoc/APIController2.java new file mode 100644 index 0000000000000000000000000000000000000000..713b9c45fb53c6a75732e2e83ad40a10b529a33c --- /dev/null +++ b/src/test/java/io/jboot/test/apidoc/APIController2.java @@ -0,0 +1,38 @@ +package io.jboot.test.apidoc; + +import com.jfinal.plugin.activerecord.Page; +import io.jboot.apidoc.ApiRet; +import io.jboot.apidoc.annotation.Api; +import io.jboot.apidoc.annotation.ApiOper; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.List; +import java.util.Map; + +@RequestMapping("/apidoc/test2") +@Api("test") +public class APIController2 extends BaseController { + + @ApiOper("index") + public List index(List list) { + return null; + } + + @ApiOper("users") + public ApiRet> users(Map map) { + return null; + } + + @ApiOper("index2") + public String index2(long str) { + return null; + } + + + @ApiOper("index3") + public void index3() { + return; + } + + +} diff --git a/src/test/java/io/jboot/test/apidoc/ApiModel1.java b/src/test/java/io/jboot/test/apidoc/ApiModel1.java new file mode 100644 index 0000000000000000000000000000000000000000..8fa504ef577745d7d9514958a9982d8619dbdb02 --- /dev/null +++ b/src/test/java/io/jboot/test/apidoc/ApiModel1.java @@ -0,0 +1,14 @@ +package io.jboot.test.apidoc; + +public class ApiModel1 { + + private String aaa; + + public String getAaa() { + return aaa; + } + + public void setAaa(String aaa) { + this.aaa = aaa; + } +} diff --git a/src/test/java/io/jboot/test/apidoc/ApiModel2.java b/src/test/java/io/jboot/test/apidoc/ApiModel2.java new file mode 100644 index 0000000000000000000000000000000000000000..f1187904728ae2eb2f843cb20a296736c1a98a48 --- /dev/null +++ b/src/test/java/io/jboot/test/apidoc/ApiModel2.java @@ -0,0 +1,4 @@ +package io.jboot.test.apidoc; + +public class ApiModel2 { +} diff --git a/src/test/java/io/jboot/test/apidoc/BaseController.java b/src/test/java/io/jboot/test/apidoc/BaseController.java new file mode 100644 index 0000000000000000000000000000000000000000..50c53ed912e569790f084ab3be430bab2ab2322d --- /dev/null +++ b/src/test/java/io/jboot/test/apidoc/BaseController.java @@ -0,0 +1,21 @@ +package io.jboot.test.apidoc; + +import com.jfinal.core.Controller; +import io.jboot.apidoc.annotation.ApiOper; + +import java.util.List; +import java.util.Map; + +public class BaseController extends Controller { + + @ApiOper("detail") + public T detail(T t){ + return null; + } + + + @ApiOper("other") + public Map> other(Map> data){ + return null; + } +} diff --git a/src/test/java/io/jboot/test/apidoc/CollectController.java b/src/test/java/io/jboot/test/apidoc/CollectController.java new file mode 100644 index 0000000000000000000000000000000000000000..00e9606a677f3686f62055b89f2ce0f9abe1532f --- /dev/null +++ b/src/test/java/io/jboot/test/apidoc/CollectController.java @@ -0,0 +1,38 @@ +package io.jboot.test.apidoc; + +import com.jfinal.plugin.activerecord.Page; +import io.jboot.apidoc.ApiRet; +import io.jboot.apidoc.annotation.Api; +import io.jboot.apidoc.annotation.ApiOper; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.List; +import java.util.Map; + +@RequestMapping("/apidoc/ccc") +@Api(value = "CollectController",collect = {APIController.class,APIController2.class}) +public class CollectController extends BaseController { + + @ApiOper("index") + public List index(List list) { + return null; + } + + @ApiOper("users") + public ApiRet> users(Map map) { + return null; + } + + @ApiOper("index2") + public String index2(long str) { + return null; + } + + + @ApiOper("index3") + public void index3() { + return; + } + + +} diff --git a/src/test/java/io/jboot/test/apidoc/Generator.java b/src/test/java/io/jboot/test/apidoc/Generator.java new file mode 100644 index 0000000000000000000000000000000000000000..327492fc53e5b3426c0d09dd0c5cf53da82fb549 --- /dev/null +++ b/src/test/java/io/jboot/test/apidoc/Generator.java @@ -0,0 +1,25 @@ +package io.jboot.test.apidoc; + +import io.jboot.apidoc.ApiDocConfig; +import io.jboot.apidoc.ApiDocManager; + +public class Generator { + + public static void main(String[] args) { + +// JbootApplication.setBootArg("jboot.datasource.type","mysql"); +// JbootApplication.setBootArg("jboot.datasource.url","jdbc:mysql://127.0.0.1:3306/ketang8?useUnicode=true&characterEncoding=utf-8&useSSL=false"); +// JbootApplication.setBootArg("jboot.datasource.user","root"); +// JbootApplication.setBootArg("jboot.datasource.password","123456"); +// +// ApiJsonGenerator.genRemarksJson(); +// ApiJsonGenerator.genMockJson(); + + + ApiDocConfig config = new ApiDocConfig(); + config.setAllInOneEnable(true); +// config.setBasePath(); + + ApiDocManager.me().genDocs(config); + } +} diff --git a/src/test/java/io/jboot/test/app/TestAppListener.java b/src/test/java/io/jboot/test/app/TestAppListener.java index 53cf40b07704ddc8aca1f8be942e41fecf374244..5b0879d64cc631a9fc35d6f1ca13f969dbc8a8fc 100644 --- a/src/test/java/io/jboot/test/app/TestAppListener.java +++ b/src/test/java/io/jboot/test/app/TestAppListener.java @@ -7,9 +7,11 @@ import com.jfinal.template.Engine; import io.jboot.aop.jfinal.JfinalHandlers; import io.jboot.aop.jfinal.JfinalPlugins; import io.jboot.core.listener.JbootAppListener; -import io.jboot.web.fixedinterceptor.FixedInterceptors; +import io.jboot.web.render.JbootCaptchaRender; public class TestAppListener implements JbootAppListener { + + @Override public void onInit() { System.out.println("TestAppListener.onInit"); @@ -40,11 +42,6 @@ public class TestAppListener implements JbootAppListener { System.out.println("TestAppListener.onInterceptorConfig"); } - @Override - public void onFixedInterceptorConfig(FixedInterceptors fixedInterceptors) { - System.out.println("TestAppListener.onFixedInterceptorConfig"); - } - @Override public void onHandlerConfig(JfinalHandlers handlers) { System.out.println("TestAppListener.onHandlerConfig"); @@ -58,6 +55,12 @@ public class TestAppListener implements JbootAppListener { @Override public void onStart() { System.out.println("TestAppListener.onStart"); + JbootCaptchaRender.setRandomArrayString("1234567890"); + } + + @Override + public void onStartFinish() { + System.out.println("TestAppListener.onStartFinish"); } @Override diff --git a/src/test/java/io/jboot/test/base/JbootJunit4TestRunner.java b/src/test/java/io/jboot/test/base/JbootJunit4TestRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..102c3a643f9c9ef65a6199b8105b583f7174a835 --- /dev/null +++ b/src/test/java/io/jboot/test/base/JbootJunit4TestRunner.java @@ -0,0 +1,16 @@ +package io.jboot.test.base; + +import com.jfinal.aop.Aop; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.InitializationError; + +public class JbootJunit4TestRunner extends BlockJUnit4ClassRunner { + public JbootJunit4TestRunner(Class testClass) throws InitializationError { + super(testClass); + } + + @Override + protected Object createTest() throws Exception { + return Aop.get(this.getTestClass().getJavaClass()); + } +} diff --git a/src/test/java/io/jboot/test/base/JbootTestBase.java b/src/test/java/io/jboot/test/base/JbootTestBase.java new file mode 100644 index 0000000000000000000000000000000000000000..7611e3138182e2a7baef5f75084e9745fba70561 --- /dev/null +++ b/src/test/java/io/jboot/test/base/JbootTestBase.java @@ -0,0 +1,101 @@ +package io.jboot.test.base; + + +import com.jfinal.json.Json; +import io.jboot.app.JbootApplication; +import io.jboot.utils.HttpUtil; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.HashMap; +import java.util.Map; + +/** + * 测试基类,从随机端口启动jboot + */ +@Ignore +@RunWith(JbootJunit4TestRunner.class) +public class JbootTestBase { + + public static int PORT = 0; + public static String BASE_URL = String.format("http://localhost:%s", PORT); + + @BeforeClass + public synchronized static void startApp() { + if (PORT != 0) { + return; + } + + PORT = getAvailablePort(); + BASE_URL = String.format("http://localhost:%s", PORT); + JbootApplication.setBootArg("jboot.app.mode", "test"); + JbootApplication.setBootArg("undertow.port", PORT); + JbootApplication.setBootArg("undertow.ioThreads", 2); + + // 禁用undertow的dev模式,避免JbootTestRunner.createTest出现问题 + JbootApplication.setBootArg("undertow.devMode", false); + + JbootApplication.run(null); + } + + + private static Integer getAvailablePort() { + ServerSocket serverSocket = null; + try { + serverSocket = new ServerSocket(0); + return serverSocket.getLocalPort(); + } catch (IOException e) { + } finally { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + } + } + } + return null; + } + + public static String httpGet(String url) { + return HttpUtil.httpGet(BASE_URL + url); + } + + public static String httpGet(String url, Map paras) { + return HttpUtil.httpGet(BASE_URL + url, paras); + } + + public static String httpGet(String url, Map paras, Map headers) { + return HttpUtil.httpGet(BASE_URL + url, paras, headers); + } + + public static String httpPost(String url) { + return HttpUtil.httpPost(BASE_URL + url, null, null, null); + } + + public static String httpPost(String url, String postData) { + return HttpUtil.httpPost(BASE_URL + url, null, null, postData); + } + + public static String httpPost(String url, Map paras) { + return HttpUtil.httpPost(BASE_URL + url, paras, null, null); + } + + public static String httpPost(String url, Map paras, String postData) { + return HttpUtil.httpPost(BASE_URL + url, paras, null, postData); + } + + public static String httpPost(String url, Map paras, Map headers, String postData) { + return HttpUtil.httpPost(BASE_URL + url, paras, headers, postData); + } + + public static String httpPostJson(String url, Object body) { + Map header = new HashMap<>(); + header.put("Content-Type", "application/json; charset=utf-8"); + + return HttpUtil.httpPost(BASE_URL + url, null, header, Json.getJson().toJson(body)); + } + +} diff --git a/src/test/java/io/jboot/test/cache/CacheUtilTester.java b/src/test/java/io/jboot/test/cache/CacheUtilTester.java new file mode 100644 index 0000000000000000000000000000000000000000..0755da61c4054e4484df9dbc74455f12ab525773 --- /dev/null +++ b/src/test/java/io/jboot/test/cache/CacheUtilTester.java @@ -0,0 +1,37 @@ +package io.jboot.test.cache; + +import io.jboot.app.config.JbootConfigManager; +import io.jboot.components.cache.JbootCacheManager; +import io.jboot.utils.CacheUtil; + +import java.util.List; + +public class CacheUtilTester { + + public static void main(String[] args) { + JbootConfigManager.setBootArg("jboot.cache.redis.globalKeyPrefix","globalKeyPrefix"); + JbootConfigManager.setBootArg("jboot.cache.type","redis"); + JbootConfigManager.setBootArg("jboot.redis.host","127.0.0.1"); + + List names = JbootCacheManager.me().getCache().getNames(); + System.out.println(names); + + + String cacheName = "tester"; + CacheUtil.put(cacheName,"key1","value1"); + CacheUtil.put(cacheName,"key2","value2"); + + System.out.println(CacheUtil.get(cacheName,"key1").toString()); + System.out.println(CacheUtil.get(cacheName,"key2").toString()); + + + List keys = CacheUtil.getKeys(cacheName); + System.out.println(keys); + + + CacheUtil.removeAll(cacheName); + + keys = CacheUtil.getKeys(cacheName); + System.out.println(keys); + } +} diff --git a/src/test/java/io/jboot/test/cache/redis/RedisCacheTester.java b/src/test/java/io/jboot/test/cache/redis/RedisCacheTester.java index aeb5068ebfd27eed383a287e98de79e963e00081..05d60eb9d163a9ad27730d237c2c77ceea9d3d6e 100644 --- a/src/test/java/io/jboot/test/cache/redis/RedisCacheTester.java +++ b/src/test/java/io/jboot/test/cache/redis/RedisCacheTester.java @@ -15,6 +15,7 @@ public class RedisCacheTester { JbootApplication.setBootArg("jboot.cache.type", "redis"); JbootApplication.setBootArg("jboot.cache.redis.host", "127.0.0.1"); JbootApplication.setBootArg("jboot.cache.redis.port", "6379"); + JbootApplication.setBootArg("jboot.cache.redis.globalKeyPrefix", "myapp"); } @Test @@ -32,9 +33,10 @@ public class RedisCacheTester { @Test public void testGet() { - Jboot.getCache().put("cachename", "key", "value"); + Jboot.getCache().put("cachename", "key", "value1111"); String value = Jboot.getCache().get("cachename", "key"); - Assert.assertTrue("value".equals(value)); + System.out.println("testGet:" + value); + Assert.assertTrue("value1111".equals(value)); } diff --git a/src/test/java/io/jboot/test/captcha/CaptchaController.java b/src/test/java/io/jboot/test/captcha/CaptchaController.java new file mode 100644 index 0000000000000000000000000000000000000000..0b72a4e86619c1adb84d26098cdd5bf31613ee03 --- /dev/null +++ b/src/test/java/io/jboot/test/captcha/CaptchaController.java @@ -0,0 +1,20 @@ +package io.jboot.test.captcha; + +import com.jfinal.core.Controller; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/captcha") +public class CaptchaController extends Controller { + + public void index(){ + renderCaptcha(); + } + + public void validate(){ + if (validateCaptcha("c")){ + renderText("validated"); + }else { + renderText("not validate"); + } + } +} diff --git a/src/test/java/io/jboot/test/codegen/GenTester.java b/src/test/java/io/jboot/test/codegen/GenTester.java index 7cece7a7851a584440e85c4b115a0411be8e4a02..6d437ca3f072b8e10ff95eacffe2bfb02983ad4c 100644 --- a/src/test/java/io/jboot/test/codegen/GenTester.java +++ b/src/test/java/io/jboot/test/codegen/GenTester.java @@ -1,8 +1,8 @@ package io.jboot.test.codegen; -import com.jfinal.kit.PathKit; import io.jboot.app.JbootApplication; +import io.jboot.codegen.CodeGenHelpler; import io.jboot.codegen.model.JbootBaseModelGenerator; import io.jboot.codegen.model.JbootModelGenerator; import io.jboot.codegen.service.JbootServiceImplGenerator; @@ -19,8 +19,8 @@ public class GenTester { String modelPackage = "io.jboot.test.codegen.model"; String baseModelPackage = modelPackage + ".base"; - String modelDir = PathKit.getWebRootPath() + "/src/test/java/" + modelPackage.replace(".", "/"); - String baseModelDir = PathKit.getWebRootPath() + "/src/test/java/" + baseModelPackage.replace(".", "/"); + String modelDir = CodeGenHelpler.getUserDir() + "/src/test/java/" + modelPackage.replace(".", "/"); + String baseModelDir = CodeGenHelpler.getUserDir() + "/src/test/java/" + baseModelPackage.replace(".", "/"); System.out.println("start generate..."); System.out.println("generate dir:" + modelDir); @@ -33,12 +33,12 @@ public class GenTester { String servicePackage = "io.jboot.test.codegen.service"; String serviceImplPackage = "io.jboot.test.codegen.service.provider"; - String serviceOutputDir = PathKit.getWebRootPath() + "/src/test/java/" + servicePackage.replace(".", "/"); - String serviceImplOutputDir = PathKit.getWebRootPath() + "/src/test/java/" + serviceImplPackage.replace(".", "/"); + String serviceOutputDir = CodeGenHelpler.getUserDir() + "/src/test/java/" + servicePackage.replace(".", "/"); + String serviceImplOutputDir = CodeGenHelpler.getUserDir() + "/src/test/java/" + serviceImplPackage.replace(".", "/"); new JbootServiceInterfaceGenerator(servicePackage, serviceOutputDir, modelPackage).generate(); - new JbootServiceImplGenerator(servicePackage, serviceImplOutputDir, modelPackage).setImplName("provider").generate(); + new JbootServiceImplGenerator(servicePackage, serviceImplPackage, serviceImplOutputDir, modelPackage).setImplName("provider").generate(); } } diff --git a/src/test/java/io/jboot/test/config/ConfigController.java b/src/test/java/io/jboot/test/config/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..ab82c15ff19a6b0190cefcf7f3a28dc3ae2fbbde --- /dev/null +++ b/src/test/java/io/jboot/test/config/ConfigController.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.config; + +import io.jboot.utils.ConfigUtil; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/19 + */ +@RequestMapping("config") +public class ConfigController extends JbootController { + + public void index(){ + renderJson(ConfigUtil.getConfigModels(TestConfigModel.class,"config.test.test")); + } +} diff --git a/src/test/java/io/jboot/test/config/TestConfigModel.java b/src/test/java/io/jboot/test/config/TestConfigModel.java new file mode 100644 index 0000000000000000000000000000000000000000..63ab09161327d8a61ee408a4c3bf48d82cba6260 --- /dev/null +++ b/src/test/java/io/jboot/test/config/TestConfigModel.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.config; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/3/19 + */ +public class TestConfigModel { + + private String name; + private String type; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/src/test/java/io/jboot/test/config/TestI592AL.java b/src/test/java/io/jboot/test/config/TestI592AL.java new file mode 100644 index 0000000000000000000000000000000000000000..ec5df0d2137f797e6315c1c98d0d240afd19ebea --- /dev/null +++ b/src/test/java/io/jboot/test/config/TestI592AL.java @@ -0,0 +1,16 @@ +package io.jboot.test.config; + +import io.jboot.app.JbootApplication; + +/** + * test https://gitee.com/JbootProjects/jboot/issues/I592AL + */ +public class TestI592AL { + + public static void main(String[] args) { + JbootApplication.setBootArg("jboot.config.nacos.enable","true"); + JbootApplication.setBootArg("jboot.config.nacos.serverAddr","${NACOS_SERVER:127.0.0.1:8848}"); + + JbootApplication.run(args); + } +} diff --git a/src/test/java/io/jboot/test/controller/CacheController.java b/src/test/java/io/jboot/test/controller/CacheController.java new file mode 100644 index 0000000000000000000000000000000000000000..085bd41c1b6f15cc4870692c0321de4012a746a9 --- /dev/null +++ b/src/test/java/io/jboot/test/controller/CacheController.java @@ -0,0 +1,104 @@ +package io.jboot.test.controller; + +import io.jboot.aop.annotation.DefaultValue; +import io.jboot.components.cache.annotation.CacheEvict; +import io.jboot.components.cache.annotation.Cacheable; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.HashMap; +import java.util.Map; + +@RequestMapping(value = "/cache") +public class CacheController extends JbootController { + + + @Cacheable(name = "aaa", liveSeconds = 10) + public void index() { + System.out.println("index() invoked!!!!!!!!!"); + renderText("index"); + } + + + @Cacheable(name = "aaa", liveSeconds = 10) + public void json() { + System.out.println("json() invoked!!!!!!!!!"); + Map data = new HashMap<>(); + data.put("age", 1); + data.put("name", "张三"); + data.put("sex", 1); + renderJson(data); + } + + + @Cacheable(name = "json99", liveSeconds = 3) + public Map json99() { + System.out.println("json99() invoked!!!!!!!!!"); + Map data = new HashMap<>(); + data.put("age", 1); + data.put("name", "张三"); + data.put("sex", 1); + return data; + } + + @Cacheable(name = "json88", liveSeconds = 10) + public Map json88(@DefaultValue("3")Integer age) { + System.out.println("json99() invoked!!!!!!!!!"); + Map data = new HashMap<>(); + data.put("age", 1); + data.put("name", "张三"); + data.put("sex", 1); + return data; + } + + @Cacheable(name = "aaa", liveSeconds = 10, unless = "para('unless')=='nocache'") + public void json2() { + System.out.println("json2() invoked!!!!!!!!!"); + Map data = new HashMap<>(); + data.put("age", 1); + data.put("name", "张三"); + data.put("sex", 1); + renderJson(data); + } + + @Cacheable(name = "aaa", liveSeconds = 10, unless = "para('type')==1") + public void json3() { + System.out.println("json2() invoked!!!!!!!!!"); + Map data = new HashMap<>(); + data.put("age", 1); + data.put("name", "张三"); + data.put("sex", 1); + renderJson(data); + } + + + @Cacheable(name = "aaa", liveSeconds = 10) + public void html() { + System.out.println("html() invoked!!!!!!!!!"); + renderText("/index.html"); + } + + + @Cacheable(name = "aaa", liveSeconds = 10) + public void xml() { + System.out.println("xml() invoked!!!!!!!!!"); + String xml = "\n" + + " \n" + + " \n" + + " 1348831860\n" + + " \n" + + " \n" + + " 1234567890123456\n" + + ""; + + renderText(xml, "xml"); + } + + + @CacheEvict(name = "aaa") + public void removeAll() { + renderText("ok"); + } + + +} diff --git a/src/test/java/io/jboot/test/controller/IndexController.java b/src/test/java/io/jboot/test/controller/IndexController.java index fcbdf8cd6098c2f020cc815193189a7f0da4cdc3..43dacf4de654c64a69724f4b8bbef07334ae28db 100644 --- a/src/test/java/io/jboot/test/controller/IndexController.java +++ b/src/test/java/io/jboot/test/controller/IndexController.java @@ -1,21 +1,140 @@ package io.jboot.test.controller; import com.jfinal.kit.PathKit; +import io.jboot.test.db.model.User; +import io.jboot.utils.RequestUtil; +import io.jboot.web.ResponseEntity; import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.GetRequest; +import io.jboot.web.controller.annotation.PostRequest; import io.jboot.web.controller.annotation.RequestMapping; -@RequestMapping("/") +@RequestMapping(value = "/") public class IndexController extends JbootController { public void index() { - render("/index.html"); + render("index.html"); } + public void classPath() { renderText(PathKit.getRootClassPath()); } public void error500(){ + render("/xxx/xx/x/xxx/index.html"); + } + + public void csv(){ + String text = "1,0\n" + + "2,15000\n" + + "3,20000\n" + + "4,30000"; + + renderText(text); + } + + public String ping(){ + return "ping:" + getPara("ping"); + } + + @PostRequest + public void post(){ + renderText("post ok"); + } + + + @GetRequest + public void get(){ + renderText("get ok"); + } + + @GetRequest + @PostRequest + public void getpost(){ + renderText("get or post ok"); + } + + public void baseUrl(){ + renderText(RequestUtil.getBaseUrl()); + } + + + public void currentUrl(){ + renderText(RequestUtil.getCurrentUrl()); + } + + public String r1(){ +// render("xxxx"); + return "text...."; + } + + public String r2(){ + return "error : 404"; + } + + + public String r3(){ + return "error : 404"; + } + + public String r4(){ + return "error:500"; + } + + + public String r5(){ + return "error : 500"; + } + + + public String r6(){ + return "index.html"; + } + + public ResponseEntity r7(){ + return ResponseEntity.ok().body("aaa"); + } + + + + public String r8(){ + return "redirect : r1"; + } + + + public String r9(){ + return "redirect:r2"; + } + + + + public String r10(){ + return "forward : r3"; + } + + + public String r11(){ + return "forward:r4"; + } + + public String r12(){ + return "forward:./r4"; + } + public String r13(){ + return "forward: "; + } + + public String r14(){ + return "forward: classPath"; + } + public String r15(){ + return "redirect: classPath"; + } + public Object r16(){ + User user = new User(); + user.put("aaa",123); + return user; } } diff --git a/src/test/java/io/jboot/test/controller/PathController.java b/src/test/java/io/jboot/test/controller/PathController.java new file mode 100644 index 0000000000000000000000000000000000000000..f0245396c45208e449c73d10ccd393006b50e818 --- /dev/null +++ b/src/test/java/io/jboot/test/controller/PathController.java @@ -0,0 +1,28 @@ +package io.jboot.test.controller; + +import com.jfinal.core.Path; +import com.jfinal.kit.PathKit; +import io.jboot.web.controller.JbootController; + +@Path("/path") +public class PathController extends JbootController { + + public void index() { + render("/index.html"); + } + + public void classPath() { + renderText(PathKit.getRootClassPath()); + } + + public void error500(){ + + } + + public String ping(){ + return "ping:" + getPara("ping"); + } + + + +} diff --git a/src/test/java/io/jboot/test/cors/CorsController.java b/src/test/java/io/jboot/test/cors/CorsController.java index b09de83dc4601736241133a117528665a4fa573e..6412e57086af144280467c983329f2668fbe4a8d 100644 --- a/src/test/java/io/jboot/test/cors/CorsController.java +++ b/src/test/java/io/jboot/test/cors/CorsController.java @@ -1,9 +1,9 @@ package io.jboot.test.cors; +import com.jfinal.ext.cors.EnableCORS; import com.jfinal.kit.Ret; import io.jboot.web.controller.JbootController; import io.jboot.web.controller.annotation.RequestMapping; -import io.jboot.web.cors.EnableCORS; @RequestMapping("/cors") public class CorsController extends JbootController { diff --git a/src/test/java/io/jboot/test/db/clickhouse/ClickHouseController.java b/src/test/java/io/jboot/test/db/clickhouse/ClickHouseController.java new file mode 100644 index 0000000000000000000000000000000000000000..d9ec4b38e5a9284c47bc774027b17b40ec8f2932 --- /dev/null +++ b/src/test/java/io/jboot/test/db/clickhouse/ClickHouseController.java @@ -0,0 +1,200 @@ +package io.jboot.test.db.clickhouse; + +import com.jfinal.kit.Ret; +import com.jfinal.plugin.activerecord.Db; +import com.jfinal.plugin.activerecord.Page; +import com.jfinal.plugin.activerecord.Record; +import io.jboot.app.JbootApplication; +import io.jboot.db.JbootDb; +import io.jboot.db.model.Columns; +import io.jboot.test.db.model.User; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.Arrays; +import java.util.List; + +@RequestMapping("/clickhouse") +public class ClickHouseController extends JbootController { + + + public static void main(String[] args) { + + //设置 数据源 的相关信息 + JbootApplication.setBootArg("jboot.datasource.factory", "druid"); + JbootApplication.setBootArg("jboot.datasource.type", "clickhouse"); +// JbootApplication.setBootArg("jboot.datasource.url", "jdbc:clickhouse://localhost:9000/tutorial"); + + JbootApplication.setBootArg("jboot.datasource.driverClassName", "io.jboot.db.driver.OfficialClickHouseDriver"); + JbootApplication.setBootArg("jboot.datasource.url", "jdbc:clickhouse://localhost:8123/tutorial"); + +// JbootApplication.setBootArg("jboot.datasource.user", "root"); +// JbootApplication.setBootArg("jboot.datasource.password", "123456"); + JbootApplication.setBootArg("jboot.model.unscanPackage", "*"); + JbootApplication.setBootArg("jboot.model.scanPackage", "io.jboot.test.db.clickhouse"); +// JbootApplication.setBootArg("undertow.devMode", "false"); + + //启动应用程序 + JbootApplication.run(args); + + Columns columns = Columns.create(); + columns.between("id",1,5); + List users = new UserInfo().findListByColumns(columns); + + + System.out.println(Arrays.toString(users.toArray())); + System.out.println(Db.find("select * from user_info")); + + } + + public void index() { + List records = Db.find("select * from user_info"); + renderJson(records); + } + + + + + public void find1(){ + + UserInfo dao = new UserInfo(); + + Columns columns = Columns.create(); + columns.between("id",1,5); + + List users = dao.findListByColumns(columns); + renderJson(users); + } + + public void find2(){ + + UserInfo dao = new UserInfo(); + + Columns columns = Columns.create(); + columns.in("id",1,2,3,4); + + List users = dao.findListByColumns(columns); + renderJson(users); + } + + public void find3(){ + + UserInfo dao = new UserInfo(); + + Columns columns = Columns.create(); + columns.in("id",1,2,3,4); + + List users = dao.findListByColumns(columns); + renderJson(users); + } + + + public void find4(){ + List users = JbootDb.find("user_info",Columns.create()); + renderJson(users); + } + + + public void find5(){ + List users = JbootDb.find("user_info",Columns.create("login_name","aaa")); + renderJson(users); + } + + + public void find6(){ + List users = JbootDb.use().find("user_info",Columns.create("login_name","aaa")); + renderJson(users); + } + + public void find7(){ + User dao = new User(); + + Columns columns = Columns.create(); + columns.in("u.id",1,2,3,4); +// columns.likeAppendPercent("login_name","c"); + +// List users = dao.leftJoin("article","a","user.id=a.user_id").findListByColumns(columns); + List users = dao.loadColumns("u.id,a.id").alias("u").leftJoin("article").as("a").on("u.id=a.user_id").findListByColumns(columns); + + dao.findAll(); + + renderJson(users); + } + + + + + public void find8(){ + List users = JbootDb.use().find("user_info",Columns.create("login_name",true)); + renderJson(users); + } + + + public void find9(){ + UserInfo dao = new UserInfo(); + Page page = dao.paginateByColumns(getInt("page",1),2,Columns.create()); + renderJson(page); + } + + + + public void del1(){ + UserInfo dao = new UserInfo(); + dao.batchDeleteByIds("1",2); + renderJson(Ret.ok()); + } + + + public void del2(){ + UserInfo dao = new UserInfo(); + dao.set("id",100); + dao.delete(); + renderJson(Ret.ok()); + } + + public void save1(){ + UserInfo user = new UserInfo(); + user.set("id",101); + user.set("age",20); + user.set("name","张三"); + user.save(); + renderJson(Ret.ok()); + } + + public void update1(){ + UserInfo user = new UserInfo(); + user.set("id",100); + user.set("name","李四"); + user.update(); + renderJson(Ret.ok()); + } + + + public void safeMode(){ + UserInfo dao = new UserInfo(); + + Columns columns = Columns.safeMode(); + columns.in("user.`id`",1,2,3,4); + columns.likeAppendPercent("login_name",null); + + + List users = dao.leftJoin("article").as("a").on("user.id=a.user_id").findListByColumns(columns); + renderJson(users); + } + + + public void use(){ + UserInfo dao = new UserInfo(); + + Columns columns = Columns.create(); + columns.in("user.`id`",1,2,3,4); + + User newDao = (User) dao.use("aaa"); +// User newDao = (User) dao.useFirst("aaa"); + + +// List users = newDao.findListByColumns(columns); + List users = newDao.leftJoin("article").as("a").on("user.id=a.user_id").findListByColumns(columns); + renderJson(users); + } +} diff --git a/src/test/java/io/jboot/test/db/clickhouse/UserInfo.java b/src/test/java/io/jboot/test/db/clickhouse/UserInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..59ee80093af0f883dea7c272f57e747592c89092 --- /dev/null +++ b/src/test/java/io/jboot/test/db/clickhouse/UserInfo.java @@ -0,0 +1,15 @@ +package io.jboot.test.db.clickhouse; + +import com.alibaba.fastjson.annotation.JSONField; +import io.jboot.db.annotation.Table; +import io.jboot.db.model.JbootModel; + +@Table(tableName = "user_info",primaryKey = "id") +public class UserInfo extends JbootModel { + + @JSONField(name = "sex") + public String getSexString(){ + return "男"; + } + +} diff --git a/src/test/java/io/jboot/test/db/dao/DaoTester.java b/src/test/java/io/jboot/test/db/dao/DaoTester.java new file mode 100644 index 0000000000000000000000000000000000000000..d1593a2a83a6d187264d2c4cbe00741b3724813a --- /dev/null +++ b/src/test/java/io/jboot/test/db/dao/DaoTester.java @@ -0,0 +1,61 @@ +package io.jboot.test.db.dao; + +import com.alibaba.fastjson.JSON; +import io.jboot.app.JbootApplication; +import io.jboot.db.model.Columns; +import io.jboot.test.db.model.User; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.List; + +@RequestMapping("/dao") +public class DaoTester extends JbootController { + + + public static void main(String[] args) { + + //设置 datasource 1 的相关信息 + JbootApplication.setBootArg("jboot.datasource.type", "mysql"); + JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://127.0.0.1:3306/jbootdemo"); + JbootApplication.setBootArg("jboot.datasource.user", "root"); + JbootApplication.setBootArg("jboot.datasource.password", "123456"); + JbootApplication.setBootArg("jboot.model.scanPackage", "io.jboot.test.db.model"); + + + //启动应用程序 + JbootApplication.run(args); + + } + + + public void index() { + User dao = new User().dao(); + renderJson(JSON.toJSON(dao.findCountByColumns(Columns.EMPTY))); + } + + public void distinct() { + User dao = new User().dao(); + List users = dao.distinct("id").findAll(); + renderJson(JSON.toJSON(users)); + } + + public void use() { + User dao = new User().dao(); + List users = dao.use("id").findAll(); + renderJson(JSON.toJSON(users)); + } + + public void useFirst() { + User dao = new User().dao(); + List users = dao.useFirst("id").findAll(); + renderJson(JSON.toJSON(users)); + } + + + public void error() { + User dao = new User().dao(); + dao.save(); + } + +} diff --git a/src/test/java/io/jboot/test/db/model/User.java b/src/test/java/io/jboot/test/db/model/User.java index 761b61fe542245afea4db03a9b46cc79cb09a427..e00c770b46dcbe6550375375af5de9cc53ae40fd 100644 --- a/src/test/java/io/jboot/test/db/model/User.java +++ b/src/test/java/io/jboot/test/db/model/User.java @@ -1,8 +1,26 @@ package io.jboot.test.db.model; +import com.alibaba.fastjson.annotation.JSONField; import io.jboot.db.annotation.Table; import io.jboot.db.model.JbootModel; +import io.jboot.web.json.JsonIgnore; @Table(tableName = "user",primaryKey = "id") -public class User extends JbootModel { +public class User extends JbootModel { + + @JSONField(name = "sex") + public String getSexString(){ + return "男"; + } + + @JsonIgnore +//@JSONField(name = "myId") + public String getId(){ + return "111"; + } + + + public Integer getUserId(){ + return (Integer) get("user_id"); + } } diff --git a/src/test/java/io/jboot/test/db/simple/DbController.java b/src/test/java/io/jboot/test/db/simple/DbController.java index 278ae3d5bb5a6815d998347aa472f02bc3165eeb..9c80c4a33d1b59be12de2cdb534ee509555972a8 100644 --- a/src/test/java/io/jboot/test/db/simple/DbController.java +++ b/src/test/java/io/jboot/test/db/simple/DbController.java @@ -2,19 +2,19 @@ package io.jboot.test.db.simple; import com.jfinal.kit.Ret; import com.jfinal.plugin.activerecord.Db; +import com.jfinal.plugin.activerecord.Page; import com.jfinal.plugin.activerecord.Record; import io.jboot.app.JbootApplication; import io.jboot.db.JbootDb; import io.jboot.db.model.Columns; import io.jboot.test.db.model.User; -import io.jboot.web.controller.JbootController; import io.jboot.web.controller.annotation.RequestMapping; import java.util.Arrays; import java.util.List; @RequestMapping("/db") -public class DbController extends JbootController { +public class DbController extends SuperDbController { public static void main(String[] args) { @@ -26,7 +26,7 @@ public class DbController extends JbootController { JbootApplication.setBootArg("jboot.datasource.password", "123456"); JbootApplication.setBootArg("jboot.model.unscanPackage", "*"); JbootApplication.setBootArg("jboot.model.scanPackage", "io.jboot.test.db.model"); - JbootApplication.setBootArg("undertow.devMode", "false"); +// JbootApplication.setBootArg("undertow.devMode", "false"); //启动应用程序 JbootApplication.run(args); @@ -34,17 +34,16 @@ public class DbController extends JbootController { Columns columns = Columns.create(); columns.between("id",1,5); List users = new User().findListByColumns(columns); + + System.out.println(Arrays.toString(users.toArray())); + System.out.println(Db.find("select * from user")); } - public void index() { - List records = Db.find("select * from `user`"); - renderJson(records); - } - public void find1(){ + public void find1(User invocation){ User dao = new User(); @@ -100,14 +99,34 @@ public class DbController extends JbootController { User dao = new User(); Columns columns = Columns.create(); - columns.in("user.`id`",1,2,3,4); - columns.likeAppendPercent("login_name","c"); + columns.in("u.id",1,2,3,4); +// columns.likeAppendPercent("login_name","c"); - List users = dao.leftJoin("article").as("a").on("user.id=a.user_id").findListByColumns(columns); +// List users = dao.leftJoin("article","a","user.id=a.user_id").findListByColumns(columns); + List users = dao.loadColumns("u.id,a.id").alias("u").leftJoin("article").as("a").on("u.id=a.user_id").findListByColumns(columns); + + dao.findAll(); + + renderJson(users); + } + + + + + public void find8(){ + List users = JbootDb.use().find("user",Columns.create("login_name",true)); renderJson(users); } + public void find9(){ + User dao = new User(); + Page page = dao.paginateByColumns(getInt("page",1),10,Columns.create()); + renderJson(page); + } + + + public void del1(){ User dao = new User(); dao.batchDeleteByIds("1",2,3); @@ -130,4 +149,32 @@ public class DbController extends JbootController { renderJson(Ret.ok()); } + + public void safeMode(){ + User dao = new User(); + + Columns columns = Columns.safeMode(); + columns.in("user.`id`",1,2,3,4); + columns.likeAppendPercent("login_name",null); + + + List users = dao.leftJoin("article").as("a").on("user.id=a.user_id").findListByColumns(columns); + renderJson(users); + } + + + public void use(){ + User dao = new User(); + + Columns columns = Columns.create(); + columns.in("user.`id`",1,2,3,4); + + User newDao = (User) dao.use("aaa"); +// User newDao = (User) dao.useFirst("aaa"); + + +// List users = newDao.findListByColumns(columns); + List users = newDao.leftJoin("article").as("a").on("user.id=a.user_id").findListByColumns(columns); + renderJson(users); + } } diff --git a/src/test/java/io/jboot/test/db/simple/SuperDbController.java b/src/test/java/io/jboot/test/db/simple/SuperDbController.java new file mode 100644 index 0000000000000000000000000000000000000000..0e5baf6bec235d563989bdc6ee04ee06e041b28e --- /dev/null +++ b/src/test/java/io/jboot/test/db/simple/SuperDbController.java @@ -0,0 +1,19 @@ +package io.jboot.test.db.simple; + +import com.jfinal.plugin.activerecord.Db; +import com.jfinal.plugin.activerecord.Record; +import io.jboot.web.controller.JbootController; + +import java.util.List; + +public class SuperDbController extends JbootController { + + + + + public void index() { + List records = Db.find("select * from `user`"); + renderJson(records); + } + +} diff --git a/src/test/java/io/jboot/test/db/tx/TestService.java b/src/test/java/io/jboot/test/db/tx/TestService.java new file mode 100644 index 0000000000000000000000000000000000000000..87fea13432deda206717fc584d92c48263c20b3f --- /dev/null +++ b/src/test/java/io/jboot/test/db/tx/TestService.java @@ -0,0 +1,26 @@ +package io.jboot.test.db.tx; + +import com.jfinal.kit.Ret; +import io.jboot.aop.annotation.Transactional; + +public class TestService { + + @Transactional + public void test1() { + System.out.println("currentThread------->>>>" + Thread.currentThread().getName()); + System.out.println("test1"); + } + + @Transactional(rollbackForFalse = true) + public boolean test2() { + System.out.println("currentThread------->>>>" + Thread.currentThread().getName()); + System.out.println("test1"); + return false; + } + + @Transactional(inNewThread = true,threadPoolName = "aaa") + public Ret test3() { + System.out.println("currentThread------->>>>" + Thread.currentThread().getName()); + return Ret.ok(); + } +} diff --git a/src/test/java/io/jboot/test/db/tx/TxTestController.java b/src/test/java/io/jboot/test/db/tx/TxTestController.java new file mode 100644 index 0000000000000000000000000000000000000000..fa109bd1aee60301c03a87004b837f9c1a8e3c5a --- /dev/null +++ b/src/test/java/io/jboot/test/db/tx/TxTestController.java @@ -0,0 +1,54 @@ +package io.jboot.test.db.tx; + +import com.jfinal.aop.Inject; +import com.jfinal.plugin.activerecord.Db; +import io.jboot.app.JbootApplication; +import io.jboot.db.model.Columns; +import io.jboot.test.db.model.User; +import io.jboot.test.db.simple.SuperDbController; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.Arrays; +import java.util.List; + +@RequestMapping("/tx") +public class TxTestController extends SuperDbController { + + + public static void main(String[] args) { + + //设置 数据源 的相关信息 + JbootApplication.setBootArg("jboot.datasource.type", "mysql"); + JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://127.0.0.1:3306/jbootdemo"); + JbootApplication.setBootArg("jboot.datasource.user", "root"); + JbootApplication.setBootArg("jboot.datasource.password", "123456"); + JbootApplication.setBootArg("jboot.model.unscanPackage", "*"); + JbootApplication.setBootArg("jboot.model.scanPackage", "io.jboot.test.db.model"); +// JbootApplication.setBootArg("undertow.devMode", "false"); + + //启动应用程序 + JbootApplication.run(args); + + Columns columns = Columns.create(); + columns.between("id",1,5); + List users = new User().findListByColumns(columns); + + + System.out.println(Arrays.toString(users.toArray())); + System.out.println(Db.find("select * from user")); + + } + + + @Inject + private TestService testService; + + public void index(){ + testService.test1(); + + System.out.println("--2-->>>" + testService.test2()); + System.out.println("--3-->>>" + testService.test3()); + + renderText("index..."); + } +} diff --git a/src/test/java/io/jboot/test/distributed/Runable1.java b/src/test/java/io/jboot/test/distributed/Runable1.java new file mode 100644 index 0000000000000000000000000000000000000000..8aa1277494f3e0a2edfe5158b43e7d2092aa64e5 --- /dev/null +++ b/src/test/java/io/jboot/test/distributed/Runable1.java @@ -0,0 +1,17 @@ +//package io.jboot.test.distributed; +// +//import io.jboot.components.schedule.annotation.EnableDistributedRunnable; +//import io.jboot.components.schedule.annotation.FixedDelay; +// +//@FixedDelay(period = 30) +//@EnableDistributedRunnable(redisKey = "aaa",expireSeconds = 30) +//public class Runable1 implements Runnable { +// int i =0; +// @Override +// public void run() { +// if (i++ >= 3){ +// throw new IllegalStateException("messss..."); +// } +// System.err.println("Runable1.running.........."+this); +// } +//} diff --git a/src/test/java/io/jboot/test/distributed/Runable2.java b/src/test/java/io/jboot/test/distributed/Runable2.java new file mode 100644 index 0000000000000000000000000000000000000000..3913c7cf0785616c14c2e32f1b1827c48a5c8e0b --- /dev/null +++ b/src/test/java/io/jboot/test/distributed/Runable2.java @@ -0,0 +1,17 @@ +//package io.jboot.test.distributed; +// +//import io.jboot.components.schedule.annotation.EnableDistributedRunnable; +//import io.jboot.components.schedule.annotation.FixedDelay; +// +//@FixedDelay(period = 30) +//@EnableDistributedRunnable(redisKey = "aaa",expireSeconds = 30) +//public class Runable2 implements Runnable { +// int i =0; +// @Override +// public void run() { +// if (i++ >= 3){ +// throw new IllegalStateException("messss..."); +// } +// System.err.println("Runable2.running.........."+this); +// } +//} diff --git a/src/test/java/io/jboot/test/distributed/Runable3.java b/src/test/java/io/jboot/test/distributed/Runable3.java new file mode 100644 index 0000000000000000000000000000000000000000..0eea7a5866d9f6772f3b917edd8736c7b4a3e24e --- /dev/null +++ b/src/test/java/io/jboot/test/distributed/Runable3.java @@ -0,0 +1,17 @@ +//package io.jboot.test.distributed; +// +//import io.jboot.components.schedule.annotation.EnableDistributedRunnable; +//import io.jboot.components.schedule.annotation.FixedDelay; +// +//@FixedDelay(period = 30) +//@EnableDistributedRunnable(redisKey = "aaa",expireSeconds = 30) +//public class Runable3 implements Runnable { +// int i =0; +// @Override +// public void run() { +// if (i++ >= 3){ +// throw new IllegalStateException("messss..."); +// } +// System.err.println("Runable3.running.........."+this); +// } +//} diff --git a/src/test/java/io/jboot/test/ehcache/EhcacheTester.java b/src/test/java/io/jboot/test/ehcache/EhcacheTester.java index 0b1f1ea7f636a926a14f70a6f6b64028979903e1..3cfb6b2222e11fef066d276a4504cc5fb0df88c7 100644 --- a/src/test/java/io/jboot/test/ehcache/EhcacheTester.java +++ b/src/test/java/io/jboot/test/ehcache/EhcacheTester.java @@ -2,6 +2,7 @@ package io.jboot.test.ehcache; import io.jboot.components.cache.JbootCache; +import io.jboot.components.cache.JbootCacheConfig; import io.jboot.components.cache.ehcache.JbootEhcacheImpl; public class EhcacheTester { @@ -9,7 +10,7 @@ public class EhcacheTester { private static final String CACHE_NAME = "test"; public static void main(String[] args) throws Exception { - JbootCache cache = new JbootEhcacheImpl(); + JbootCache cache = new JbootEhcacheImpl(new JbootCacheConfig()); cache.put(CACHE_NAME, "key1", "value1"); cache.put(CACHE_NAME, "key2", "value2", 2); diff --git a/src/test/java/io/jboot/test/file/FileController.java b/src/test/java/io/jboot/test/file/FileController.java index f3492b2f9657da4317677625294b8be9c24d3cae..93d8364cef4347648c53e7773bcce1e2a42f06c0 100644 --- a/src/test/java/io/jboot/test/file/FileController.java +++ b/src/test/java/io/jboot/test/file/FileController.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2019, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, diff --git a/src/test/java/io/jboot/test/gateway/App1Stater.java b/src/test/java/io/jboot/test/gateway/App1Stater.java new file mode 100644 index 0000000000000000000000000000000000000000..b418b89109ef3963f952fee12e7fde41f5d8b1c4 --- /dev/null +++ b/src/test/java/io/jboot/test/gateway/App1Stater.java @@ -0,0 +1,12 @@ +package io.jboot.test.gateway; + +import io.jboot.app.JbootApplication; + +public class App1Stater { + + public static void main(String[] args){ + JbootApplication.setBootArg("undertow.port",9901); + JbootApplication.setBootArg("jboot.gateway.enable ",false); + JbootApplication.run(args); + } +} diff --git a/src/test/java/io/jboot/test/gateway/App2Stater.java b/src/test/java/io/jboot/test/gateway/App2Stater.java new file mode 100644 index 0000000000000000000000000000000000000000..f12ed874460214560230a4378ddd2dcbea85b34a --- /dev/null +++ b/src/test/java/io/jboot/test/gateway/App2Stater.java @@ -0,0 +1,12 @@ +package io.jboot.test.gateway; + +import io.jboot.app.JbootApplication; + +public class App2Stater { + + public static void main(String[] args){ + JbootApplication.setBootArg("undertow.port",9902); + JbootApplication.setBootArg("jboot.gateway.enable ",false); + JbootApplication.run(args); + } +} diff --git a/src/test/java/io/jboot/test/gateway/App3Stater.java b/src/test/java/io/jboot/test/gateway/App3Stater.java new file mode 100644 index 0000000000000000000000000000000000000000..dcb73c8e1474c969e9e62f53634d64811aa9e570 --- /dev/null +++ b/src/test/java/io/jboot/test/gateway/App3Stater.java @@ -0,0 +1,12 @@ +package io.jboot.test.gateway; + +import io.jboot.app.JbootApplication; + +public class App3Stater { + + public static void main(String[] args){ + JbootApplication.setBootArg("undertow.port",9903); + JbootApplication.setBootArg("jboot.gateway.enable ",false); + JbootApplication.run(args); + } +} diff --git a/src/test/java/io/jboot/test/gateway/GatewayController.java b/src/test/java/io/jboot/test/gateway/GatewayController.java new file mode 100644 index 0000000000000000000000000000000000000000..5371cc74c225a0878ade44dbe4524495d2705ccf --- /dev/null +++ b/src/test/java/io/jboot/test/gateway/GatewayController.java @@ -0,0 +1,35 @@ +package io.jboot.test.gateway; + +import com.jfinal.kit.Ret; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * @author michael yang (fuhai999@gmail.com) + * @Date: 2020/4/5 + */ +@RequestMapping("/gateway") +public class GatewayController extends JbootController { + + public void index(){ + renderText("index"); + } + + + + public void render(){ + List headers= new ArrayList<>(); + Enumeration headerNames = getRequest().getHeaderNames(); + while (headerNames.hasMoreElements()){ + headers.add(headerNames.nextElement()); + } + System.out.println("headers: "+headers); + + renderJson(Ret.ok()); + + } +} diff --git a/src/test/java/io/jboot/test/gateway/TestInterceptor.java b/src/test/java/io/jboot/test/gateway/TestInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..7cb2198c452615397ca1516edc369da596a4f1bb --- /dev/null +++ b/src/test/java/io/jboot/test/gateway/TestInterceptor.java @@ -0,0 +1,20 @@ +package io.jboot.test.gateway; + +import io.jboot.components.gateway.GatewayInterceptor; +import io.jboot.components.gateway.GatewayInvocation; + +public class TestInterceptor implements GatewayInterceptor { + + @Override + public void intercept(GatewayInvocation inv) { + System.out.println("TestInterceptor.invoke...."); + + inv.getProxy().addHeader("aaa", "bbbb").addHeader("cccc", "eeeee"); + + inv.invoke(); + + System.out.println(inv.getResponse().getHeaderNames()); + + inv.getResponse().addHeader("aaa", "bbb"); + } +} diff --git a/src/test/java/io/jboot/test/http/GbkContentTest.java b/src/test/java/io/jboot/test/http/GbkContentTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8f4637e43fca165e59d1f227ffc6f8005a08ee60 --- /dev/null +++ b/src/test/java/io/jboot/test/http/GbkContentTest.java @@ -0,0 +1,17 @@ +package io.jboot.test.http; + +import io.jboot.components.http.JbootHttpRequest; +import io.jboot.components.http.JbootHttpResponse; +import io.jboot.utils.HttpUtil; + +public class GbkContentTest { + + public static void main(String[] args) { + JbootHttpRequest request = new JbootHttpRequest("http://www.**.com.cn/20.asp"); + request.setMethod(JbootHttpRequest.METHOD_GET); + request.setCharset("GBK"); + + JbootHttpResponse response = HttpUtil.handle(request); + System.out.println(response.getContent()); + } +} diff --git a/src/test/java/io/jboot/test/http/HttpUtilTester.java b/src/test/java/io/jboot/test/http/HttpUtilTester.java new file mode 100644 index 0000000000000000000000000000000000000000..201f83a805a8fd6c29b35d411083a2a1ab96a4c7 --- /dev/null +++ b/src/test/java/io/jboot/test/http/HttpUtilTester.java @@ -0,0 +1,15 @@ +package io.jboot.test.http; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class HttpUtilTester { + + public static void main(String[] args) { + + + + } + + +} \ No newline at end of file diff --git a/src/test/java/io/jboot/test/jfinal/EnumController.java b/src/test/java/io/jboot/test/jfinal/EnumController.java new file mode 100644 index 0000000000000000000000000000000000000000..cdeeda23b7c99274518ed9765c081411171df198 --- /dev/null +++ b/src/test/java/io/jboot/test/jfinal/EnumController.java @@ -0,0 +1,13 @@ +package io.jboot.test.jfinal; + +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/enum") +public class EnumController extends JbootController { + + public void index(){ + render("/enum.html"); + } + +} diff --git a/src/test/java/io/jboot/test/jfinal/JfinalEnum.java b/src/test/java/io/jboot/test/jfinal/JfinalEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..310c8771c8f7576be022a7ff55859d84b127bc4f --- /dev/null +++ b/src/test/java/io/jboot/test/jfinal/JfinalEnum.java @@ -0,0 +1,87 @@ +package io.jboot.test.jfinal; + +import io.jboot.Jboot; +import io.jboot.web.directive.annotation.JFinalSharedEnum; + +import java.util.Arrays; + +@JFinalSharedEnum +public enum JfinalEnum { + + MASTER(1, "总部"), + + AGENT(10, "代理商"), + + CLIENT(20, "SaaS租户"), + + DEV(99, "开发者"); + + + private int value; + private String text; + + JfinalEnum(int value, String text) { + this.value = value; + this.text = text; + } + + public int getValue() { + return value; + } + + + public String getText() { + return text; + } + public void setText(String text) { + this.text = text; + } + + public static JfinalEnum get(Integer value) { + if (value != null) { + for (JfinalEnum type : values()) { + if (type.value == value) { + return type; + } + } + } + return null; + } + + + public static boolean isDev(Integer value, Object r) throws InterruptedException { + Thread.sleep(2000); + return true; + } + + public static boolean isDev(Integer value) { + // 只有在开发模式下,isDev 才会生效 + return Jboot.isDevMode() && value != null && value == DEV.value; + } + + + public static boolean isMaster(Integer value) { + return value != null && value == MASTER.value; + } + + + public static boolean isAgent(Integer value) { + return value != null && value == AGENT.value; + } + + + public static boolean isClient(Integer value) { + return value != null && value == CLIENT.value; + } + + public static void main(String[] args) { + Object[] xxxs= values(); + System.out.println(Arrays.toString(xxxs)); + } + + public static String str(){ + return "hello"; + } + + +} diff --git a/src/test/java/io/jboot/test/jfinal/RedisLock.java b/src/test/java/io/jboot/test/jfinal/RedisLock.java new file mode 100644 index 0000000000000000000000000000000000000000..3a28a4b0294a991810c403028d024968dc7b8b17 --- /dev/null +++ b/src/test/java/io/jboot/test/jfinal/RedisLock.java @@ -0,0 +1,4 @@ +package io.jboot.test.jfinal; + +public class RedisLock { +} diff --git a/src/test/java/io/jboot/test/jfinal/StaticInvoke.java b/src/test/java/io/jboot/test/jfinal/StaticInvoke.java new file mode 100644 index 0000000000000000000000000000000000000000..856b4382a7d19b0aaf6c4ae84546e40867c9d381 --- /dev/null +++ b/src/test/java/io/jboot/test/jfinal/StaticInvoke.java @@ -0,0 +1,16 @@ +package io.jboot.test.jfinal; + +import com.jfinal.template.Engine; + +public class StaticInvoke { + + public static void main(String[] args) { + + Engine engine = new Engine(); + String template = "#(io.jboot.utils.StrUtil::isNumeric('123'))"; + String string = engine.getTemplateByString(template).renderToString(null); + System.out.println(string); + + + } +} diff --git a/src/test/java/io/jboot/test/join/JoinController.java b/src/test/java/io/jboot/test/join/JoinController.java new file mode 100644 index 0000000000000000000000000000000000000000..7d73156b5aae193e6210ec517bc92de6283f930f --- /dev/null +++ b/src/test/java/io/jboot/test/join/JoinController.java @@ -0,0 +1,55 @@ +package io.jboot.test.join; + +import com.jfinal.aop.Aop; +import io.jboot.app.JbootApplication; +import io.jboot.test.join.model.Article; +import io.jboot.test.join.model.Author; +import io.jboot.test.join.model.Category; +import io.jboot.test.join.service.ArticleService; +import io.jboot.test.join.service.AuthorService; +import io.jboot.test.join.service.CategoryService; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.List; + +@RequestMapping("/join") +public class JoinController extends JbootController { + + + public static void main(String[] args) { + + //设置 数据源 的相关信息 + JbootApplication.setBootArg("jboot.datasource.type", "mysql"); + JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://127.0.0.1:3306/jboot_join_demo"); + JbootApplication.setBootArg("jboot.datasource.user", "root"); + JbootApplication.setBootArg("jboot.datasource.password", "123456"); + JbootApplication.setBootArg("jboot.model.unscanPackage", "*"); + JbootApplication.setBootArg("jboot.model.scanPackage", "io.jboot.test.join.model"); + JbootApplication.setBootArg("undertow.devMode", "false"); + + //启动应用程序 + JbootApplication.run(args); + + } + + + public void articles() { + List

articles = Aop.get(ArticleService.class).findListWithAuthorAndCategories(); + renderJson(articles); + } + + + public void authors() { + List authors = Aop.get(AuthorService.class).findListWithArticles(); + renderJson(authors); + } + + + + public void categories() { + List categories = Aop.get(CategoryService.class).findListWithArticles(); + renderJson(categories); + } + +} diff --git a/src/test/java/io/jboot/test/join/jboot_join_demo.sql b/src/test/java/io/jboot/test/join/jboot_join_demo.sql new file mode 100644 index 0000000000000000000000000000000000000000..dd2cc15f09f67a0fb1000358c78956813add179a --- /dev/null +++ b/src/test/java/io/jboot/test/join/jboot_join_demo.sql @@ -0,0 +1,99 @@ +# Dump of table article +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `article`; + +CREATE TABLE `article` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `author_id` int(11) unsigned DEFAULT NULL, + `title` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `content` text COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +LOCK TABLES `article` WRITE; +/*!40000 ALTER TABLE `article` DISABLE KEYS */; + +INSERT INTO `article` (`id`, `author_id`, `title`, `content`) +VALUES + (1,1,'文章1','内容111'), + (2,1,'文章2','内容2222'), + (3,2,'文章3','内容333'), + (4,2,'文章4','内容444'); + +/*!40000 ALTER TABLE `article` ENABLE KEYS */; +UNLOCK TABLES; + + +# Dump of table article_category +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `article_category`; + +CREATE TABLE `article_category` ( + `article_id` int(11) unsigned NOT NULL, + `category_id` int(11) unsigned DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +LOCK TABLES `article_category` WRITE; +/*!40000 ALTER TABLE `article_category` DISABLE KEYS */; + +INSERT INTO `article_category` (`article_id`, `category_id`) +VALUES + (1,1), + (1,2), + (2,2), + (3,1), + (3,2), + (4,1); + +/*!40000 ALTER TABLE `article_category` ENABLE KEYS */; +UNLOCK TABLES; + + +# Dump of table author +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `author`; + +CREATE TABLE `author` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `nickname` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +LOCK TABLES `author` WRITE; +/*!40000 ALTER TABLE `author` DISABLE KEYS */; + +INSERT INTO `author` (`id`, `nickname`, `email`) +VALUES + (1,'孙悟空','swk@gmail.com'), + (2,'猪八戒','zbj@gmail.com'); + +/*!40000 ALTER TABLE `author` ENABLE KEYS */; +UNLOCK TABLES; + + +# Dump of table category +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `category`; + +CREATE TABLE `category` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` text COLLATE utf8mb4_unicode_ci, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +LOCK TABLES `category` WRITE; +/*!40000 ALTER TABLE `category` DISABLE KEYS */; + +INSERT INTO `category` (`id`, `title`, `description`) +VALUES + (1,'文章分类1','文章分类描述111'), + (2,'文章分类2','文章分类描述222'); + +/*!40000 ALTER TABLE `category` ENABLE KEYS */; +UNLOCK TABLES; diff --git a/src/test/java/io/jboot/test/join/model/Article.java b/src/test/java/io/jboot/test/join/model/Article.java new file mode 100644 index 0000000000000000000000000000000000000000..f13ddd9318dd8b2b1d5e821ab8565bbc226b54e9 --- /dev/null +++ b/src/test/java/io/jboot/test/join/model/Article.java @@ -0,0 +1,50 @@ +package io.jboot.test.join.model; + +import io.jboot.db.annotation.Table; +import io.jboot.db.model.JbootModel; + +@Table(tableName = "article",primaryKey = "id") +public class Article extends JbootModel
{ + + /** + * `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + * `author_id` int(11) unsigned DEFAULT NULL, + * `title` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + * `content` text COLLATE utf8mb4_unicode_ci, + */ + + public Long getId(){ + return getLong("id"); + } + + public void setId(Long id){ + set("id",id); + } + + public Long getAuthorId(){ + return getLong("author_id"); + } + + public void setAuthorId(Long id){ + set("author_id",id); + } + + + public String getTitle(){ + return getStr("title"); + } + + public void setTitle(String title){ + set("title",title); + } + + public String getContent(){ + return getStr("content"); + } + + public void setContent(String content){ + set("content",content); + } + + +} diff --git a/src/test/java/io/jboot/test/join/model/Author.java b/src/test/java/io/jboot/test/join/model/Author.java new file mode 100644 index 0000000000000000000000000000000000000000..f384f17f8f1d8874f983275c724fada16e02e94b --- /dev/null +++ b/src/test/java/io/jboot/test/join/model/Author.java @@ -0,0 +1,43 @@ +package io.jboot.test.join.model; + +import io.jboot.db.annotation.Table; +import io.jboot.db.model.JbootModel; + +@Table(tableName = "author",primaryKey = "id") +public class Author extends JbootModel { + + + /** + * `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + * `nickname` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + * `email` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + */ + + + + public Long getId(){ + return getLong("id"); + } + + public void setId(Long id){ + set("id",id); + } + + + public String getNickname(){ + return getStr("nickname"); + } + + public void setNickname(String nickname){ + set("nickname",nickname); + } + + public String getEmail(){ + return getStr("email"); + } + + public void setEmail(String email){ + set("email",email); + } + +} diff --git a/src/test/java/io/jboot/test/join/model/Category.java b/src/test/java/io/jboot/test/join/model/Category.java new file mode 100644 index 0000000000000000000000000000000000000000..564d10d071f906b05c8bb4fd375e665f385f32dd --- /dev/null +++ b/src/test/java/io/jboot/test/join/model/Category.java @@ -0,0 +1,43 @@ +package io.jboot.test.join.model; + +import io.jboot.db.annotation.Table; +import io.jboot.db.model.JbootModel; + +@Table(tableName = "category",primaryKey = "id") +public class Category extends JbootModel { + + + /** + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` text COLLATE utf8mb4_unicode_ci, + */ + + + + public Long getId(){ + return getLong("id"); + } + + public void setId(Long id){ + set("id",id); + } + + + public String getTitle(){ + return getStr("title"); + } + + public void setTitle(String title){ + set("title",title); + } + + public String getDescription(){ + return getStr("description"); + } + + public void setDescription(String description){ + set("description",description); + } + +} diff --git a/src/test/java/io/jboot/test/join/readme.txt b/src/test/java/io/jboot/test/join/readme.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/test/java/io/jboot/test/join/service/ArticleService.java b/src/test/java/io/jboot/test/join/service/ArticleService.java new file mode 100644 index 0000000000000000000000000000000000000000..c74ae6d4fc0e51d126ea1a9606e011ba9328dc53 --- /dev/null +++ b/src/test/java/io/jboot/test/join/service/ArticleService.java @@ -0,0 +1,27 @@ +package io.jboot.test.join.service; + +import com.jfinal.aop.Inject; +import io.jboot.service.JbootServiceBase; +import io.jboot.test.join.model.Article; + +import java.util.List; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class ArticleService extends JbootServiceBase
{ + + @Inject + private AuthorService authorService; + + @Inject + private CategoryService categoryService; + + + public List
findListWithAuthorAndCategories(){ + List
articles = DAO.findAll(); + authorService.join(articles,"author_id"); + categoryService.joinManyByTable(articles,"article_category","article_id","category_id"); + return articles; + } +} diff --git a/src/test/java/io/jboot/test/join/service/AuthorService.java b/src/test/java/io/jboot/test/join/service/AuthorService.java new file mode 100644 index 0000000000000000000000000000000000000000..312246be7d2d38acb3b0b11b2c9d0784e2dd840a --- /dev/null +++ b/src/test/java/io/jboot/test/join/service/AuthorService.java @@ -0,0 +1,24 @@ +package io.jboot.test.join.service; + +import com.jfinal.aop.Inject; +import io.jboot.service.JbootServiceBase; +import io.jboot.test.join.model.Author; + +import java.util.List; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class AuthorService extends JbootServiceBase { + + + @Inject + private ArticleService articleService; + + + public List findListWithArticles(){ + List authors = DAO.findAll(); + articleService.joinMany(authors,"author_id"); + return authors; + } +} diff --git a/src/test/java/io/jboot/test/join/service/CategoryService.java b/src/test/java/io/jboot/test/join/service/CategoryService.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb0f3f877f729e808a4639d5b37722d55ed90d2 --- /dev/null +++ b/src/test/java/io/jboot/test/join/service/CategoryService.java @@ -0,0 +1,22 @@ +package io.jboot.test.join.service; + +import com.jfinal.aop.Inject; +import io.jboot.service.JbootServiceBase; +import io.jboot.test.join.model.Category; + +import java.util.List; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class CategoryService extends JbootServiceBase { + + @Inject + private ArticleService articleService; + + public List findListWithArticles(){ + List categories = DAO.findAll(); + articleService.joinManyByTable(categories,"article_category","category_id","article_id"); + return categories; + } +} diff --git a/src/test/java/io/jboot/test/json/BaseJsonBodyController.java b/src/test/java/io/jboot/test/json/BaseJsonBodyController.java new file mode 100644 index 0000000000000000000000000000000000000000..8039b62e3550bf96bc3b319b9fee1dc8e21cf44f --- /dev/null +++ b/src/test/java/io/jboot/test/json/BaseJsonBodyController.java @@ -0,0 +1,22 @@ +package io.jboot.test.json; + +import io.jboot.web.controller.JbootController; +import io.jboot.web.json.JsonBody; + +import java.util.Map; + + +public class BaseJsonBodyController extends JbootController { + + + public void update(@JsonBody() T t) { + renderJson("update--->" + t); + } + +// public void update(@JsonBody() K t) { +// renderJson("update--->" + t.getClass().toString()); +// } +// +// public void test33(Map map){} + +} diff --git a/src/test/java/io/jboot/test/json/BaseJsonBodyTestController.java b/src/test/java/io/jboot/test/json/BaseJsonBodyTestController.java new file mode 100644 index 0000000000000000000000000000000000000000..112db508e94ac3f5282c2c045e779074a848a478 --- /dev/null +++ b/src/test/java/io/jboot/test/json/BaseJsonBodyTestController.java @@ -0,0 +1,30 @@ +package io.jboot.test.json; + +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/jsonbody/base") +public class BaseJsonBodyTestController extends BaseJsonBodyController{ + + +// public void update(Object object,String aa) { +// +// } + +// @Override +// public void update(Map jsonMap) { +// renderText("JsonMap ok"); +// } + +// @Override +// public void update(@JsonBody JsonMap map) { +// super.update(map); +// } +// public void update(HashMap map) { +//// super.update(map); +// } + + // @Override +// public void test33(HashMap map) { +// super.test33(map); +// } +} diff --git a/src/test/java/io/jboot/test/json/JsonBodyController.java b/src/test/java/io/jboot/test/json/JsonBodyController.java new file mode 100644 index 0000000000000000000000000000000000000000..98e54e32a441b472dfa07b0f71710343a0c02d6f --- /dev/null +++ b/src/test/java/io/jboot/test/json/JsonBodyController.java @@ -0,0 +1,391 @@ +package io.jboot.test.json; + +import com.jfinal.kit.JsonKit; +import io.jboot.utils.TypeDef; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; +import io.jboot.web.json.JsonBody; + +import javax.validation.Valid; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; + +@RequestMapping("/jsonbody") +public class JsonBodyController extends JbootController { + + /** + * send json : + *

+ * { + * "aaa":{ + * "bbb":{ + * "id":"abc", + * "age":17, + * "amount":123 + * } + * } + * } + * + * @param bean + */ + public void bean(@JsonBody(value = "aaa.bbb") @Valid MyBean bean) { + System.out.println("bean--->" + JsonKit.toJson(bean)); + renderText("ok"); + } + + public void bean1(@JsonBody("aaa.bbb") MyBean bean,@JsonBody("aaa.bbb") MyBean bean1,@JsonBody("aaa.bbb.age") int age) { + System.out.println("bean--->" + JsonKit.toJson(bean)); + System.out.println("bean--->" + JsonKit.toJson(bean1)); + System.out.println("bean--->" + JsonKit.toJson(age)); + renderText("ok"); + } + + + public void bean2(@JsonBody() MyBean bean) { + System.out.println("bean--->" + JsonKit.toJson(bean)); + renderText("ok"); + } + + public void intValue(@JsonBody("aaa.bbb.age") int bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + + public void intValue1(@JsonBody("aaa.bbb.age") Integer bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + public void intValue2(@JsonBody("aaa.bbb.age") String bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + + public void bigint1(@JsonBody("aaa.bbb.age") BigInteger bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + + public void bigdec1(@JsonBody("aaa.bbb.age") BigDecimal bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + + public void float1(@JsonBody("aaa.bbb.age") float bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + + public void long1(@JsonBody("aaa.bbb.age") long bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + + public void long2(@JsonBody("aaa.bbb.age") Long bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + + public void strValue(@JsonBody("aaa.bbb.id") String bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + public void date(@JsonBody("aaa.bbb.date") Date bean) { + System.out.println("bean--->" + bean); + renderText("ok"); + } + + + + public void map(@JsonBody("aaa.bbb") HashMap map) { + System.out.println("map--->" + JsonKit.toJson(map)); + renderText("ok"); + } + + + public void mapString(@JsonBody("aaa.bbb") HashMap map) { + System.out.println("map--->" + JsonKit.toJson(map)); + renderText("ok"); + } + + /** + * { + * "aaa":{ + * "bbb":[{ + * "id":"abc", + * "age":17, + * "amount":123 + * },{ + * "id":"abc", + * "age":17, + * "amount":123 + * }] + * } + * } + * + * @param list + */ + public void list(@JsonBody("aaa.bbb") List list) { + System.out.println("list--->" + JsonKit.toJson(list)); + renderText("ok"); + } + + + public void intInArray(@JsonBody("aaa.bbb [ 2] .amount") long intvalue) { + renderText("intInArray--->" + intvalue); + } + + public void array11(@JsonBody("aaa.bbb[0].beans[id]") String[] ids) { + System.out.println("array--->" + JsonKit.toJson(ids)); + renderText("ok"); + } + + /** + * { + * "aaa":{ + * "bbb":[{ + * "id":"abc", + * "age":17, + * "amount":123 + * },{ + * "id":"abc", + * "age":17, + * "amount":123 + * }] + * } + * } + * + * @param beans + */ + public void array(@JsonBody("aaa.bbb") MyBean[] beans) { + System.out.println("array--->" + JsonKit.toJson(beans)); + renderText("ok"); + } + + public void arrayb() { + List beans = getRawObject(new TypeDef>(){},"aaa.bbb"); + MyBean bean = getRawObject(MyBean.class,"aaa.bbb[1]"); + String id = getRawObject(String.class,"aaa.bbb[1].id"); + System.out.println("beans--->" + JsonKit.toJson(beans)); + System.out.println("bean--->" + JsonKit.toJson(bean)); + System.out.println("id--->" + JsonKit.toJson(id)); + renderText("ok"); + } + + + + public void strArray(@JsonBody("aaa.bbb[0].xx[age]") String[] beans) { + renderText("strArray--->" + JsonKit.toJson(beans)); + } + + + + public void strArray1() { + renderText("strArray--->" + JsonKit.toJson(getRawObject(String[].class,"aaa.bbb[0].xx[id]"))); + } + + + /** + * { + * "aaa":{ + * "bbb":[1,2,3] + * } + * } + * + * @param list + */ + public void list1(@JsonBody("aaa.bbb") List list) { + System.out.println("list1--->" + JsonKit.toJson(list)); + renderText("ok"); + } + + + /** + * { + * "aaa":{ + * "bbb":[1,2,3] + * } + * } + * + * @param beans + */ + public void array1(@JsonBody("aaa.bbb") int[] beans) { + System.out.println("array1--->" + JsonKit.toJson(beans)); + renderText("ok"); + } + + + /** + * [1,2,3] + * + * @param list + */ + public void list2(@JsonBody() List list) { + System.out.println("list2--->" + JsonKit.toJson(list)); + renderText("ok"); + } + + + /** + * [1,2,3] + * + * @param beans + */ + public void array2(@JsonBody() int[] beans) { + System.out.println("array2--->" + JsonKit.toJson(beans)); + renderText("ok"); + } + + + /** + * [1,2,3] + * + * @param array + */ + public void array3(@JsonBody() List array, int a) { + String s = array.get(0); + System.out.println("array3--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + /** + * [1,2,3] + * + * @param array + */ + public void array4(@JsonBody() List array, int a) { + System.out.println("array4--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + + /** + * [1,2,3] + * + * @param array + */ + public void set1(@JsonBody() Set array, int a) { +// String s = array.g(0); + System.out.println("set1--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + /** + * [1,2,3] + * + * @param array + */ + public void set2(@JsonBody() Set array, int a) { + System.out.println("set2--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + /** + * [1,2,3] + * + * @param array + */ + public void set3(@JsonBody() HashSet array, int a) { +// String s = array.g(0); + System.out.println("set1--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + /** + * [1,2,3] + * + * @param array + */ + public void set4(@JsonBody() HashSet array, int a) { + System.out.println("set2--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + + /** + * [1,2,3] + * + * @param array + */ + public void queue1(@JsonBody() Queue array, int a) { +// String s = array.g(0); + System.out.println("queue1--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + /** + * [1,2,3] + * + * @param array + */ + public void queue2(@JsonBody() Queue array, int a) { + System.out.println("queue2--->" + JsonKit.toJson(array)); + renderText("ok"); + } + /** + * [1,2,3] + * + * @param array + */ + public void vector1(@JsonBody() Vector array, int a) { +// String s = array.g(0); + System.out.println("vector1--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + /** + * [1,2,3] + * + * @param array + */ + public void vector2(@JsonBody() Vector array, int a) { + System.out.println("vector2--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + + /** + * [1,2,3] + * + * @param array + */ + public void stack1(@JsonBody() Stack array, int a) { +// String s = array.g(0); + System.out.println("stack1--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + /** + * [1,2,3] + * + * @param array + */ + public void stack2(@JsonBody() Stack array, int a) { + System.out.println("stack2--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + + /** + * [1,2,3] + * + * @param array + */ + public void deque1(@JsonBody() Deque array, int a) { +// String s = array.g(0); + System.out.println("deque1--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + /** + * [1,2,3] + * + * @param array + */ + public void deque2(@JsonBody() Deque array, int a) { + System.out.println("deque2--->" + JsonKit.toJson(array)); + renderText("ok"); + } + + +} diff --git a/src/test/java/io/jboot/test/json/JsonMap.java b/src/test/java/io/jboot/test/json/JsonMap.java new file mode 100644 index 0000000000000000000000000000000000000000..68b7108417fc07c7d11e5ffac61523e6b207d135 --- /dev/null +++ b/src/test/java/io/jboot/test/json/JsonMap.java @@ -0,0 +1,6 @@ +package io.jboot.test.json; + +import java.util.HashMap; + +public class JsonMap extends HashMap { +} diff --git a/src/test/java/io/jboot/test/json/JsonRenderController.java b/src/test/java/io/jboot/test/json/JsonRenderController.java new file mode 100644 index 0000000000000000000000000000000000000000..7515ad8d9e059529e73d52d0236b5651e1ec5b63 --- /dev/null +++ b/src/test/java/io/jboot/test/json/JsonRenderController.java @@ -0,0 +1,26 @@ +package io.jboot.test.json; + +import com.jfinal.core.Controller; +import io.jboot.app.JbootApplication; +import io.jboot.test.db.model.User; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/json") +public class JsonRenderController extends Controller { + + public static void main(String[] args) { + JbootApplication.setBootArg("jboot.json.camelCaseJsonStyleEnable", "false"); + JbootApplication.setBootArg("jboot.json.skipBeanGetters", "true"); + JbootApplication.run(args); + } + + public Object test(){ + + User user = new User(); + user.put("user_id",1); + user.put("type","aaa"); + user.put("type_id",100); + + return user; + } +} diff --git a/src/test/java/io/jboot/test/json/JsonTester.java b/src/test/java/io/jboot/test/json/JsonTester.java new file mode 100644 index 0000000000000000000000000000000000000000..5ca8c96ce8faba21a131bf53d65b4e74602eb370 --- /dev/null +++ b/src/test/java/io/jboot/test/json/JsonTester.java @@ -0,0 +1,37 @@ +package io.jboot.test.json; + +import com.alibaba.fastjson.JSON; +import io.jboot.test.db.model.User; +import io.jboot.web.json.JbootJson; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author michael yang (fuhai999@gmail.com) + */ +public class JsonTester { + + public static void main(String[] args) { + User user = new User(); + user.put("id",100); + user.put("tenant_id","xxx"); + + user.put("other_user",new User()); + user.put("myAbcDef",new User()); + + System.out.println(new JbootJson().toJson(user)); + + + Map map = new HashMap(); + map.put("zhangsan",100); + map.put("lisi",200); + map.put("wangyu",300); + map.put("zhao_liu",300); + map.put("self",map); + + System.out.println(new JbootJson().toJson(map)); + System.out.println(JSON.toJSONString(map)); + System.out.println(JSON.toJSONString(user)); + } +} diff --git a/src/test/java/io/jboot/test/json/JsonUtilTester.java b/src/test/java/io/jboot/test/json/JsonUtilTester.java new file mode 100644 index 0000000000000000000000000000000000000000..425396613afd502eac696a5673f7926fd8ca026d --- /dev/null +++ b/src/test/java/io/jboot/test/json/JsonUtilTester.java @@ -0,0 +1,43 @@ +package io.jboot.test.json; + +import io.jboot.utils.JsonUtil; +import io.jboot.utils.TypeDef; + +import java.util.List; +import java.util.Set; + +public class JsonUtilTester { + + public static void main(String[] args) { + String json = "{\n" + + " \"aaa\": [\n" + + " {\n" + + " \"id\": \"100\",\n" + + " \"age\": 10,\n" + + " \"amount\": 99\n" + + " },\n" + + " {\n" + + " \"id\": 101,\n" + + " \"age\": \"20\",\n" + + " \"amount\": 999\n" + + " },\n" + + " {\n" + + " \"id\": 101,\n" + + " \"age\": \"30\",\n" + + " \"amount\": \"9999\"\n" + + " }\n" + + " ]\n" + + "}"; + + + System.out.println("----"); + List myBeans0 = JsonUtil.get(json, "aaa", new TypeDef>() { + }); + + List myBeans1 = JsonUtil.getList(json, "aaa", MyBean.class); + + Set myBeans2 = JsonUtil.getSet(json, "aaa", MyBean.class); + + System.out.println("-----"); + } +} diff --git a/src/test/java/io/jboot/test/json/MyBean.java b/src/test/java/io/jboot/test/json/MyBean.java new file mode 100644 index 0000000000000000000000000000000000000000..e7b05a6a68d06cb5acde30b0f7f77b69643b0e76 --- /dev/null +++ b/src/test/java/io/jboot/test/json/MyBean.java @@ -0,0 +1,56 @@ +package io.jboot.test.json; + +import javax.validation.constraints.NotNull; +import java.math.BigInteger; +import java.util.Objects; + +public class MyBean { + private String id; + private int age; + private BigInteger amount; + + @NotNull + public String getId() { + return id; + } + + + public void setId(String id) { + this.id = id; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public BigInteger getAmount() { + return amount; + } + + public void setAmount(BigInteger amount) { + this.amount = amount; + } + + @Override + public String toString() { + return "MyBean{" + + "id='" + id + '\'' + + ", age=" + age + + ", amount=" + amount + + '}'; + } + + @Override + public boolean equals(Object obj) { + return id.equals(((MyBean)obj).id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/test/java/io/jboot/test/junit/Junit4TestDemo.java b/src/test/java/io/jboot/test/junit/Junit4TestDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..a89eb46c41cfd6bc86413de2954225e9471ec894 --- /dev/null +++ b/src/test/java/io/jboot/test/junit/Junit4TestDemo.java @@ -0,0 +1,63 @@ +package io.jboot.test.junit; + +import com.jfinal.aop.Inject; +import io.jboot.test.MockMethod; +import io.jboot.test.MockMvc; +import io.jboot.test.junit4.JbootRunner; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(JbootRunner.class) +public class Junit4TestDemo { + + private static MockMvc mvc = new MockMvc(); + + @Inject + private TestService myService; + + + @Test + public void test_url_aaa() { + Map paras = new HashMap<>(); + paras.put("p1", "v1"); + paras.put("p2", "v2"); + mvc.get("/test/aaa", paras).printResult() + .assertThat(result -> Assert.assertEquals(result.getContent(), "aaa")) + .assertTrue(result -> result.getStatus() == 200); + } + + @Test + public void test_url_bbb() { + Map paras = new HashMap<>(); + paras.put("p1", "v1"); + paras.put("p2", "v2"); + mvc.post("/test/bbb", paras).printResult() + .assertThat(result -> Assert.assertEquals(result.getContent(), "bbb")) + .assertTrue(result -> result.getStatus() == 200); + } + + + @Test + public void test_my_service() { + String ret = myService.doSomething(); + Assert.assertEquals(ret, "ok"); + } + + + @Test + public void test_my_service_mock() { + String ret = myService.doOther(); + System.out.println(">>>>>>" + ret); + } + + + @MockMethod(targetClass = TestService.class, targetMethod = "doOther") + public String mockTestService(TestService oo) { + return "from mock"; + } + +} diff --git a/src/test/java/io/jboot/test/junit/Junit5TestDemo.java b/src/test/java/io/jboot/test/junit/Junit5TestDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..c18eeba48f83eb986e0debab99595c0fbd0fa9cf --- /dev/null +++ b/src/test/java/io/jboot/test/junit/Junit5TestDemo.java @@ -0,0 +1,63 @@ +package io.jboot.test.junit; + +import com.jfinal.aop.Inject; +import io.jboot.test.MockMethod; +import io.jboot.test.MockMvc; +import io.jboot.test.junit5.JbootExtension; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.HashMap; +import java.util.Map; + +@ExtendWith(JbootExtension.class) +public class Junit5TestDemo { + + private static MockMvc mvc = new MockMvc(); + + + @Inject + private TestService myService; + + + @Test + public void test_url_aaa() { + Map paras = new HashMap<>(); + paras.put("p1","v1"); + paras.put("p2","v2"); + mvc.get("/test/aaa",paras).printResult() + .assertThat(result -> Assertions.assertEquals(result.getContent(),"aaa")) + .assertTrue(result -> result.getStatus() == 200); + } + + @Test + public void test_url_bbb() { + Map paras = new HashMap<>(); + paras.put("p1","v1"); + paras.put("p2","v2"); + mvc.post("/test/bbb",paras).printResult() + .assertThat(result -> Assertions.assertEquals(result.getContent(),"bbb")) + .assertTrue(result -> result.getStatus() == 200); + } + + @Test + public void test_my_service() { + String ret = myService.doSomething(); + Assertions.assertEquals(ret, "ok"); + } + + + @Test + public void test_my_service_mock() { + String ret = myService.doOther(); + System.out.println(">>>>>>" + ret); + } + + + @MockMethod(targetClass = TestService.class, targetMethod = "doOther") + public String mockTestService(TestService oo) { + return "from mock"; + } + +} diff --git a/src/test/java/io/jboot/test/junit/TestController.java b/src/test/java/io/jboot/test/junit/TestController.java new file mode 100644 index 0000000000000000000000000000000000000000..dd730cf1cfc8ee006632fdc71e2ea28c46247af2 --- /dev/null +++ b/src/test/java/io/jboot/test/junit/TestController.java @@ -0,0 +1,18 @@ +package io.jboot.test.junit; + +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/test") +public class TestController extends JbootController { + + public void aaa(){ + System.out.println(">>>>>>>queryString:" + getRequest().getQueryString()); + renderText("aaa"); + } + + public void bbb(){ + System.out.println(">>>>>>>queryString:" + getRequest().getQueryString()); + renderText("bbb"); + } +} diff --git a/src/test/java/io/jboot/test/junit/TestService.java b/src/test/java/io/jboot/test/junit/TestService.java new file mode 100644 index 0000000000000000000000000000000000000000..3fd7dbafeade2538f2addd6dfc5a6e0eb2e794f0 --- /dev/null +++ b/src/test/java/io/jboot/test/junit/TestService.java @@ -0,0 +1,13 @@ +package io.jboot.test.junit; + +public class TestService{ + + public String doSomething(){ + System.out.println(">>>>>>>>TestService.doSomething"); + return "ok"; + } + + public String doOther(){ + return "doOther"; + } +} diff --git a/src/test/java/io/jboot/test/jwt/JwtBaseController.java b/src/test/java/io/jboot/test/jwt/JwtBaseController.java new file mode 100644 index 0000000000000000000000000000000000000000..f17250ea009af2eeda0e62b38c7b699cd14c14e3 --- /dev/null +++ b/src/test/java/io/jboot/test/jwt/JwtBaseController.java @@ -0,0 +1,9 @@ +package io.jboot.test.jwt; + +import io.jboot.support.jwt.EnableJwt; +import io.jboot.web.controller.JbootController; + +@EnableJwt +public class JwtBaseController extends JbootController { + +} diff --git a/src/test/java/io/jboot/test/jwt/JwtController.java b/src/test/java/io/jboot/test/jwt/JwtController.java new file mode 100644 index 0000000000000000000000000000000000000000..c2de8e398893e56fc57fd036becda93b31b16fce --- /dev/null +++ b/src/test/java/io/jboot/test/jwt/JwtController.java @@ -0,0 +1,11 @@ +package io.jboot.test.jwt; + +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/jwt") +public class JwtController extends JwtBaseController{ + + public void index(){ + renderText("jwt ok"); + } +} diff --git a/src/test/java/io/jboot/test/lock/JbootLocalLockTester.java b/src/test/java/io/jboot/test/lock/JbootLocalLockTester.java new file mode 100644 index 0000000000000000000000000000000000000000..c3fbe8f2f9f42512a941d645779a0cd51c8f1b10 --- /dev/null +++ b/src/test/java/io/jboot/test/lock/JbootLocalLockTester.java @@ -0,0 +1,42 @@ +package io.jboot.test.lock; + +import io.jboot.app.JbootApplication; +import io.jboot.objects.lock.JbootLock; +import io.jboot.objects.lock.JbootLockManager; + +public class JbootLocalLockTester { + + static int index = 0; + + public static void main(String[] args) { + + JbootApplication.setBootArg("jboot.object.lock.type","local"); + + JbootLock lock = JbootLockManager.me().create("myLock"); + + for (int i=0;i<10;i++){ + new Thread(new Runnable() { + @Override + public void run() { + try { + lock.lock(); + JbootLocalLockTester.run(); + }finally { + lock.unlock(); + } + } + }).start(); + } + + } + + private static void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + index += 1; + System.out.println("run " + index); + } +} diff --git a/src/test/java/io/jboot/test/lock/JbootRedisLockTester.java b/src/test/java/io/jboot/test/lock/JbootRedisLockTester.java new file mode 100644 index 0000000000000000000000000000000000000000..eccaa21ba013df1915852cf5c81a762b9dceaeba --- /dev/null +++ b/src/test/java/io/jboot/test/lock/JbootRedisLockTester.java @@ -0,0 +1,45 @@ +package io.jboot.test.lock; + +import io.jboot.app.JbootApplication; +import io.jboot.objects.lock.JbootLock; +import io.jboot.objects.lock.JbootLockManager; + +public class JbootRedisLockTester { + + static int index = 0; + + public static void main(String[] args) { + + JbootApplication.setBootArg("jboot.redis.host","127.0.0.1"); + JbootApplication.setBootArg("jboot.redis.password",""); + + JbootApplication.setBootArg("jboot.object.lock.type","redis"); + + JbootLock lock = JbootLockManager.me().create("myLock"); + + for (int i=0;i<100;i++){ + new Thread(new Runnable() { + @Override + public void run() { + try { + lock.lock(); + JbootRedisLockTester.run(); + }finally { + lock.unlock(); + } + } + }).start(); + } + + } + + private static void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + index += 1; + System.out.println("run " + index); + } +} diff --git a/src/test/java/io/jboot/test/lock/NoneLockTester.java b/src/test/java/io/jboot/test/lock/NoneLockTester.java new file mode 100644 index 0000000000000000000000000000000000000000..d3f5f6eaecdf57a439c6bc25a08eb11d1204a29e --- /dev/null +++ b/src/test/java/io/jboot/test/lock/NoneLockTester.java @@ -0,0 +1,25 @@ +package io.jboot.test.lock; + +public class NoneLockTester { + + static int index = 0; + + public static void main(String[] args) { + + + for (int i=0;i<100;i++){ + new Thread(() -> run()).start(); + } + + } + + private static void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + index += 1; + System.out.println("run " + index); + } +} diff --git a/src/test/java/io/jboot/test/metrics/MetricsController.java b/src/test/java/io/jboot/test/metrics/MetricsController.java index 3a9002719af4a3192a069f154b122781d0858748..44673793b25213ff6582a1cab4d3784ab5d3617c 100644 --- a/src/test/java/io/jboot/test/metrics/MetricsController.java +++ b/src/test/java/io/jboot/test/metrics/MetricsController.java @@ -14,7 +14,7 @@ public class MetricsController extends JbootController { * 配置 reporter 为slf4j 输出 * * 当用户访问的时候,log 定时会输出 index() 的访问次数和当前并发量,1分钟输出一次 - * 同时,配置 jboot.metric.url = /metrics_admin + * 同时,配置 jboot.metric.url = /metrics.admin * 可以通过浏览器访问 /metrics_admin 查看当前的 index() 的并发量和访问次数 * * PS:只有通过浏览器访问 http://127.0.0.1:8888/metrics 才会生成 metrics 记录 @@ -22,8 +22,9 @@ public class MetricsController extends JbootController { * @param args */ public static void main(String[] args) { - JbootApplication.setBootArg("jboot.metric.url", "/metrics_admin"); - JbootApplication.setBootArg("jboot.metric.reporter", "slf4j"); + JbootApplication.setBootArg("jboot.metric.enable", "true"); +// JbootApplication.setBootArg("jboot.metric.adminServletMapping", "/metrics.admin"); +// JbootApplication.setBootArg("jboot.metric.reporter", "slf4j"); JbootApplication.run(args); } diff --git a/src/test/java/io/jboot/test/metrics/MetricsInfluxdbController.java b/src/test/java/io/jboot/test/metrics/MetricsInfluxdbController.java index 7ba5b928296bec48b3ce8e198a0173e9b3abe957..b736e0c906de96150fcfea91d9dc7b906d059d09 100644 --- a/src/test/java/io/jboot/test/metrics/MetricsInfluxdbController.java +++ b/src/test/java/io/jboot/test/metrics/MetricsInfluxdbController.java @@ -36,7 +36,8 @@ public class MetricsInfluxdbController extends JbootController { * @param args */ public static void main(String[] args) { - JbootApplication.setBootArg("jboot.metric.url", "/metrics_admin"); + JbootApplication.setBootArg("jboot.metric.enable", "true"); + JbootApplication.setBootArg("jboot.metric.adminServletMapping", "/metrics_admin"); JbootApplication.setBootArg("jboot.metric.reporter", "influxdb"); JbootApplication.setBootArg("jboot.metric.reporter.influxdb.host", "127.0.0.1"); diff --git a/src/test/java/io/jboot/test/metrics/MetricsPrometheusController.java b/src/test/java/io/jboot/test/metrics/MetricsPrometheusController.java new file mode 100644 index 0000000000000000000000000000000000000000..952a7df53111caaa712f86b0b69b2f739226ec1d --- /dev/null +++ b/src/test/java/io/jboot/test/metrics/MetricsPrometheusController.java @@ -0,0 +1,49 @@ +package io.jboot.test.metrics; + +import io.jboot.app.JbootApplication; +import io.jboot.support.metric.annotation.*; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +import java.util.HashMap; +import java.util.Map; + +@RequestMapping("/metrics/prometheus") +public class MetricsPrometheusController extends JbootController { + + public static void main(String[] args) { + JbootApplication.setBootArg("jboot.metric.enable", "true"); + JbootApplication.setBootArg("jboot.metric.reporter", "prometheus"); + +// JbootApplication.setBootArg("jboot.metric.reporter.prometheus.host", "127.0.0.1"); +// JbootApplication.setBootArg("jboot.metric.reporter.prometheus.port", "1234"); + + JbootApplication.run(args); + } + + @EnableMetricCounter + @EnableMetricConcurrency + @EnableMetricTimer + @EnableMetricHistogram + @EnableMetricMeter + public void index() { + renderText("metrics prometheus index. "); + } + + + + public void error1(){ + int i = 1/0; + render("aaa"); + } + + public void error2(){ + for (int i=0;i< 100;i++){ + Map map = new HashMap(); + for (int j=0;j<10000;j++){ + map.put("aaa" + j,"value" + j); + } + } + renderText("error2"); + } +} \ No newline at end of file diff --git a/src/test/java/io/jboot/test/mq/rabbit/RabbitMqReceiver1.java b/src/test/java/io/jboot/test/mq/rabbit/RabbitMqReceiver1.java new file mode 100644 index 0000000000000000000000000000000000000000..d52df70a47d231548efa390752f989ee603a2ec5 --- /dev/null +++ b/src/test/java/io/jboot/test/mq/rabbit/RabbitMqReceiver1.java @@ -0,0 +1,55 @@ +package io.jboot.test.mq.rabbit; + + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; +import io.jboot.components.mq.JbootmqMessageListener; +import io.jboot.components.mq.MessageContext; + +/** + * 开始之前 先通过通过 docker 把 rabbitmq 运行起来 + * docker run -d -p 15672:15672 -p 5672:5672 rabbitmq:management + */ + +public class RabbitMqReceiver1 { + + public static void main(String[] args) { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8001"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "rabbitmq"); + JbootApplication.setBootArg("jboot.mq.channel", "channel1,channel2"); + + + //以下可以不用配置,是默认信息 +// JbootApplication.setBootArg("jboot.mq.rabbitmq.username", "guest"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.password", "guest"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.host", "127.0.0.1"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.port", "5672"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.virtualHost", ""); + + //非常重要,多个应用如果同时接受同一个 channel 的广播,必须配置此项,而且必须不能相同,否则广播的时候只有一个应用能够接受到 + JbootApplication.setBootArg("jboot.mq.rabbitmq.broadcastChannelPrefix", "app1"); + + + // 只监听 channel1 这个通道 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + },"channel1"); + + + + + //启动应用程序 + JbootApplication.run(args); + + + + System.out.println("RabbitMqReceiver1 started."); + } +} diff --git a/src/test/java/io/jboot/test/mq/rabbit/RabbitMqReceiver2.java b/src/test/java/io/jboot/test/mq/rabbit/RabbitMqReceiver2.java new file mode 100644 index 0000000000000000000000000000000000000000..b544a3215eb766f617ec09fc411ccca4688869f4 --- /dev/null +++ b/src/test/java/io/jboot/test/mq/rabbit/RabbitMqReceiver2.java @@ -0,0 +1,54 @@ +package io.jboot.test.mq.rabbit; + + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.JbootmqMessageListener; + +/** + * 开始之前 先通过通过 docker 把 rabbitmq 运行起来 + * docker run -d -p 15672:15672 -p 5672:5672 rabbitmq:management + */ +public class RabbitMqReceiver2 { + + public static void main(String[] args) { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8002"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "rabbitmq"); + JbootApplication.setBootArg("jboot.mq.channel", "channel1,channel2,myChannel"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.useQueue", false); + JbootApplication.setBootArg("jboot.mq.rabbitmq.broadcastExchangeDeclareExchangeType", "direct"); + + //以下可以不用配置,是默认信息 +// JbootApplication.setBootArg("jboot.mq.rabbitmq.username", "guest"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.password", "guest"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.host", "127.0.0.1"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.port", "5672"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.virtualHost", ""); + + //非常重要,多个应用如果同时接受同一个 channel 的广播,必须配置此项,而且必须不能相同,否则广播的时候只有一个应用能够接受到 +// JbootApplication.setBootArg("jboot.mq.rabbitmq.broadcastChannelPrefix", "app2"); + + + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + },"myChannel"); + + + //启动应用程序 + JbootApplication.run(args); + + + + + + System.out.println("RabbitMqReceiver2 started."); + } +} diff --git a/src/test/java/io/jboot/test/mq/rabbit/RabbitMqReceiver3.java b/src/test/java/io/jboot/test/mq/rabbit/RabbitMqReceiver3.java new file mode 100644 index 0000000000000000000000000000000000000000..a99231396b9fd29435a3405f3fe4d0c6fc5c42e1 --- /dev/null +++ b/src/test/java/io/jboot/test/mq/rabbit/RabbitMqReceiver3.java @@ -0,0 +1,52 @@ +package io.jboot.test.mq.rabbit; + + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.JbootmqMessageListener; + +/** + * 开始之前 先通过通过 docker 把 rabbitmq 运行起来 + * docker run -d -p 15672:15672 -p 5672:5672 rabbitmq:management + */ +public class RabbitMqReceiver3 { + + public static void main(String[] args) { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8003"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "rabbitmq"); + JbootApplication.setBootArg("jboot.mq.channel", "channel1,channel2,myChannel"); + + + //以下可以不用配置,是默认信息 +// JbootApplication.setBootArg("jboot.mq.rabbitmq.username", "guest"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.password", "guest"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.host", "127.0.0.1"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.port", "5672"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.virtualHost", ""); + + //非常重要,多个应用如果同时接受同一个 channel 的广播,必须配置此项,而且必须不能相同,否则广播的时候只有一个应用能够接受到 + JbootApplication.setBootArg("jboot.mq.rabbitmq.broadcastChannelPrefix", "app3"); + + //添加监听,不指定通道,则监听所有通道 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + }); + + + + //启动应用程序 + JbootApplication.run(args); + + + + System.out.println("RabbitMqReceiver3 started."); + } +} diff --git a/src/test/java/io/jboot/test/mq/rabbit/RabbitMqSender.java b/src/test/java/io/jboot/test/mq/rabbit/RabbitMqSender.java new file mode 100644 index 0000000000000000000000000000000000000000..620b023a8fa9be0e419ae7fd02b9237d037eb98a --- /dev/null +++ b/src/test/java/io/jboot/test/mq/rabbit/RabbitMqSender.java @@ -0,0 +1,50 @@ +package io.jboot.test.mq.rabbit; + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; + +/** + * 开始之前 先通过通过 docker 把 rabbitmq 运行起来 + * docker run -d -p 15672:15672 -p 5672:5672 rabbitmq:management + */ +public class RabbitMqSender { + + + public static void main(String[] args) throws InterruptedException { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8000"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "rabbitmq"); + JbootApplication.setBootArg("jboot.mq.rabbitmq.broadcastExchangeDeclareExchangeType", "direct"); + + //以下可以不用配置,是默认信息 +// JbootApplication.setBootArg("jboot.mq.rabbitmq.username", "guest"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.password", "guest"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.host", "127.0.0.1"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.port", "5672"); +// JbootApplication.setBootArg("jboot.mq.rabbitmq.virtualHost", ""); + + //启动应用程序 + JbootApplication.run(args); + + long index = 0; + + while (true && index < 3) { + + Jboot.getMq().publish("broadcast-channel1-index:" + index, "channel1"); + Jboot.getMq().publish("broadcast-channel2-index:" + index, "channel2"); + Jboot.getMq().publish("broadcast-myChannel-index:" + index, "myChannel"); + + Jboot.getMq().enqueue("enqueue-channel1-index:" + index, "channel1"); + + System.out.println("jboot mq publish success... index: " + index); + index++; + + Thread.sleep(2000); + } + + } + +} diff --git a/src/test/java/io/jboot/test/mq/redis/RedisMqReceiver1.java b/src/test/java/io/jboot/test/mq/redis/RedisMqReceiver1.java index 3127c0a6b8b4a33a17bdf4bb707cb877f2f8bf76..78f6dd8949f4344b2f13f7ffe6e1597ffd83c020 100644 --- a/src/test/java/io/jboot/test/mq/redis/RedisMqReceiver1.java +++ b/src/test/java/io/jboot/test/mq/redis/RedisMqReceiver1.java @@ -1,36 +1,47 @@ -package io.jboot.test.mq.redis; - - -import io.jboot.Jboot; -import io.jboot.app.JbootApplication; - -public class RedisMqReceiver1 { - - public static void main(String[] args) { - - //Undertow端口号配置 - JbootApplication.setBootArg("undertow.port", "8001"); - - //设置 mq 的相关信息 - JbootApplication.setBootArg("jboot.mq.type", "redis"); - JbootApplication.setBootArg("jboot.mq.channel", "channel1,channel2,myChannel"); - JbootApplication.setBootArg("jboot.mq.redis.host", "127.0.0.1"); - - //启动应用程序 - JbootApplication.run(args); - - //添加监听 - Jboot.getMq().addMessageListener((channel, message) -> { - System.out.println("listener1 receive msg : " + message + ", from channel : " + channel); - }); - - // 只监听 myChannel 这个通道 - Jboot.getMq().addMessageListener((channel, message) -> { - System.out.println("listener2 receive msg : " + message + ", from channel : " + channel); - },"myChannel"); - - Jboot.getMq().startListening(); - - System.out.println("RedisMqReceiver1 started."); - } -} +package io.jboot.test.mq.redis; + + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.JbootmqMessageListener; + +public class RedisMqReceiver1 { + + public static void main(String[] args) { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8001"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "redis"); + JbootApplication.setBootArg("jboot.mq.channel", "channel1,channel2,myChannel"); + JbootApplication.setBootArg("jboot.mq.redis.host", "127.0.0.1"); + JbootApplication.setBootArg("jboot.mq.redis.port", 6379); + JbootApplication.setBootArg("jboot.mq.redis.database", 9); + JbootApplication.setBootArg("jboot.mq.redis.password", ""); + + //添加监听 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + }); + + // 只监听 myChannel 这个通道 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + },"myChannel"); + + + //启动应用程序 + JbootApplication.run(args); + Jboot.getMq().startListening(); + + System.out.println("RedisMqReceiver1 started."); + } +} diff --git a/src/test/java/io/jboot/test/mq/redis/RedisMqReceiver2.java b/src/test/java/io/jboot/test/mq/redis/RedisMqReceiver2.java index ccba717c5400c7239aebede09fa2c3819d89937b..052e09cd294ad022cdd1dcb5d33a875d6b7c745b 100644 --- a/src/test/java/io/jboot/test/mq/redis/RedisMqReceiver2.java +++ b/src/test/java/io/jboot/test/mq/redis/RedisMqReceiver2.java @@ -1,36 +1,46 @@ -package io.jboot.test.mq.redis; - - -import io.jboot.Jboot; -import io.jboot.app.JbootApplication; - -public class RedisMqReceiver2 { - - public static void main(String[] args) { - - //Undertow端口号配置 - JbootApplication.setBootArg("undertow.port", "8002"); - - //设置 mq 的相关信息 - JbootApplication.setBootArg("jboot.mq.type", "redis"); - JbootApplication.setBootArg("jboot.mq.channel", "channel1,channel2,myChannel"); - JbootApplication.setBootArg("jboot.mq.redis.host", "127.0.0.1"); - - //启动应用程序 - JbootApplication.run(args); - - //添加监听 - Jboot.getMq().addMessageListener((channel, message) -> { - System.out.println("listener1 receive msg : " + message + ", from channel : " + channel); - }); - - // 只监听 myChannel 这个通道 - Jboot.getMq().addMessageListener((channel, message) -> { - System.out.println("listener2 receive msg : " + message + ", from channel : " + channel); - },"myChannel"); - - Jboot.getMq().startListening(); - - System.out.println("RedisMqReceiver1 started."); - } -} +package io.jboot.test.mq.redis; + + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.JbootmqMessageListener; + +public class RedisMqReceiver2 { + + public static void main(String[] args) { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8002"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "redis"); + JbootApplication.setBootArg("jboot.mq.channel", "channel1,channel2,myChannel"); + JbootApplication.setBootArg("jboot.mq.redis.host", "127.0.0.1"); + JbootApplication.setBootArg("jboot.mq.redis.port", 6379); + JbootApplication.setBootArg("jboot.mq.redis.database", 10); + + //添加监听 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + }); + + // 只监听 myChannel 这个通道 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + },"myChannel"); + + //启动应用程序 + JbootApplication.run(args); + + Jboot.getMq().startListening(); + + System.out.println("RedisMqReceiver2 started."); + } +} diff --git a/src/test/java/io/jboot/test/mq/redis/RedisMqSender.java b/src/test/java/io/jboot/test/mq/redis/RedisMqSender.java index 45267dc20165ee2cf1d5a8f65c9079faae93bb56..f47458f95a67cd1c4a97cd831e3b1c55a61f0528 100644 --- a/src/test/java/io/jboot/test/mq/redis/RedisMqSender.java +++ b/src/test/java/io/jboot/test/mq/redis/RedisMqSender.java @@ -1,38 +1,57 @@ -package io.jboot.test.mq.redis; - - -import io.jboot.Jboot; -import io.jboot.app.JbootApplication; - -import java.util.UUID; - -public class RedisMqSender { - - public static void main(String[] args) throws InterruptedException { - - //Undertow端口号配置 - JbootApplication.setBootArg("undertow.port", "8000"); - - //设置 mq 的相关信息 - JbootApplication.setBootArg("jboot.mq.type", "redis"); - JbootApplication.setBootArg("jboot.mq.redis.host", "127.0.0.1"); - - //启动应用程序 - JbootApplication.run(args); - - while (true) { - - Jboot.getMq().publish("message from RedisMqSender", "channel1"); - Jboot.getMq().publish("message from RedisMqSender", "channel2"); - Jboot.getMq().publish("message from RedisMqSender", "myChannel"); - - Jboot.getMq().enqueue("message from RedisMqSender by enqueue : " + UUID.randomUUID(), "channel1"); - - Thread.sleep(2000); - System.out.println("jboot mq publish success..."); - } - - } - - -} +package io.jboot.test.mq.redis; + + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; + +public class RedisMqSender { + + public static void main(String[] args) throws InterruptedException { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8659"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "redis"); + JbootApplication.setBootArg("jboot.mq.redis.host", "127.0.0.1"); + JbootApplication.setBootArg("jboot.mq.redis.port", 6379); + JbootApplication.setBootArg("jboot.mq.redis.database", 9); + JbootApplication.setBootArg("jboot.mq.redis.password", ""); + + JbootApplication.setBootArg("jboot.mq.other1.type", "redis"); + JbootApplication.setBootArg("jboot.mq.other1.typeName", "test"); + //JbootApplication.setBootArg("jboot.mq.other1.channel", "channel1,channel2,myChannel"); + JbootApplication.setBootArg("jboot.mq.redis.test.host", "127.0.0.1"); + JbootApplication.setBootArg("jboot.mq.redis.test.port", 6379); + JbootApplication.setBootArg("jboot.mq.redis.test.database", 10); + JbootApplication.setBootArg("jboot.mq.redis.test.password", ""); + + //启动应用程序 + JbootApplication.run(args); + + int count = 10; + for (int i = 0; i < count; i++) { + Jboot.getMq().publish("message from RedisMqSender", "channel1"); + Jboot.getMq().publish("message from RedisMqSender", "channel2"); + Jboot.getMq().publish("message from RedisMqSender", "myChannel"); + + Jboot.getMq().enqueue("message " + i, "channel1"); + + Thread.sleep(1000); + System.out.println("jboot mq publish success..."); + } + + for (int i = 0; i < count; i++) { + Jboot.getMq("test").publish("message from RedisMqSender", "channel1"); + Jboot.getMq("test").publish("message from RedisMqSender", "channel2"); + Jboot.getMq("test").publish("message from RedisMqSender", "myChannel"); + + Jboot.getMq("test").enqueue("message " + i, "channel1"); + + Thread.sleep(1000); + System.out.println("jboot mq publish success..."); + } + } + + +} diff --git a/src/test/java/io/jboot/test/mq/rocketmq/RocketmqReceiver1.java b/src/test/java/io/jboot/test/mq/rocketmq/RocketmqReceiver1.java new file mode 100644 index 0000000000000000000000000000000000000000..b1e167dce11da8c465187aeb876a7ad24f8367a7 --- /dev/null +++ b/src/test/java/io/jboot/test/mq/rocketmq/RocketmqReceiver1.java @@ -0,0 +1,44 @@ +package io.jboot.test.mq.rocketmq; + + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.JbootmqMessageListener; + +public class RocketmqReceiver1 { + + public static void main(String[] args) { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8001"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "rocketmq"); + JbootApplication.setBootArg("jboot.mq.channel", "channel1,channel2,myChannel"); + JbootApplication.setBootArg("jboot.mq.rocket.namesrvAddr", "127.0.0.1:9876"); + + //启动应用程序 + JbootApplication.run(args); + + //添加监听 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + }); + + // 只监听 myChannel 这个通道 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + },"myChannel"); + + Jboot.getMq().startListening(); + + System.out.println("RedisMqReceiver1 started."); + } +} diff --git a/src/test/java/io/jboot/test/mq/rocketmq/RocketmqReceiver2.java b/src/test/java/io/jboot/test/mq/rocketmq/RocketmqReceiver2.java new file mode 100644 index 0000000000000000000000000000000000000000..ffe1ee9b093b2ed409264f5433e810d36517657b --- /dev/null +++ b/src/test/java/io/jboot/test/mq/rocketmq/RocketmqReceiver2.java @@ -0,0 +1,44 @@ +package io.jboot.test.mq.rocketmq; + + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; +import io.jboot.components.mq.MessageContext; +import io.jboot.components.mq.JbootmqMessageListener; + +public class RocketmqReceiver2 { + + public static void main(String[] args) { + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "8002"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "rocketmq"); + JbootApplication.setBootArg("jboot.mq.channel", "channel1,channel2,myChannel"); + JbootApplication.setBootArg("jboot.mq.rocket.namesrvAddr", "127.0.0.1:9876"); + + //启动应用程序 + JbootApplication.run(args); + + //添加监听 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + }); + + // 只监听 myChannel 这个通道 + Jboot.getMq().addMessageListener(new JbootmqMessageListener() { + @Override + public void onMessage(String channel, Object message, MessageContext context) { + System.out.println("Receive msg: " + message + ", from channel: " + channel); + } + },"myChannel"); + + Jboot.getMq().startListening(); + + System.out.println("RedisMqReceiver1 started."); + } +} diff --git a/src/test/java/io/jboot/test/mq/rocketmq/RocketmqSender.java b/src/test/java/io/jboot/test/mq/rocketmq/RocketmqSender.java new file mode 100644 index 0000000000000000000000000000000000000000..a5f130fe1afe58938ac724635ecc9ea4d1838c2d --- /dev/null +++ b/src/test/java/io/jboot/test/mq/rocketmq/RocketmqSender.java @@ -0,0 +1,39 @@ +package io.jboot.test.mq.rocketmq; + + +import io.jboot.Jboot; +import io.jboot.app.JbootApplication; + +public class RocketmqSender { + + public static void main(String[] args) throws InterruptedException { + + //Undertow端口号配置 +// JbootApplication.setBootArg("undertow.port", "8000"); + + //设置 mq 的相关信息 + JbootApplication.setBootArg("jboot.mq.type", "rocketmq"); + JbootApplication.setBootArg("jboot.mq.rocket.namesrvAddr", "127.0.0.1:9876"); + + //启动应用程序 + JbootApplication.run(args); + + int i = 0; + + while (i<100) { + +// Jboot.getMq().publish("message from RocketmqSender", "channel1"); +// Jboot.getMq().publish("message from RocketmqSender", "channel2"); +// Jboot.getMq().publish("message from RocketmqSender", "myChannel"); +// + Jboot.getMq().enqueue("message from RocketmqSender by enqueue : " +i, "channel1"); + +// Thread.sleep(2000); + System.out.println("jboot mq publish success..."); + i++; + } + + } + + +} diff --git a/src/test/java/io/jboot/test/other/ModelCopierTest.java b/src/test/java/io/jboot/test/other/ModelUtilTest.java similarity index 76% rename from src/test/java/io/jboot/test/other/ModelCopierTest.java rename to src/test/java/io/jboot/test/other/ModelUtilTest.java index 4adeaa6d68d73c823998f3c93137352e52234152..65f5127d0004553b81e95d59bfc99d7b0beae8e3 100644 --- a/src/test/java/io/jboot/test/other/ModelCopierTest.java +++ b/src/test/java/io/jboot/test/other/ModelUtilTest.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -16,7 +16,7 @@ package io.jboot.test.other; import io.jboot.test.db.model.User; -import io.jboot.utils.ModelCopier; +import io.jboot.utils.ModelUtil; import java.util.ArrayList; import java.util.List; @@ -25,20 +25,20 @@ import java.util.List; * @author michael yang (fuhai999@gmail.com) * @Date: 2020/2/9 */ -public class ModelCopierTest { +public class ModelUtilTest { public static void main(String[] args) { List users = new ArrayList<>(); users.add(new User()); - List newUsers = ModelCopier.copy(users); + List newUsers = ModelUtil.copy(users); System.out.println("users == newUsers ---> " + (users == newUsers)); System.out.println("users.get(0) == newUsers.get(0) ---> " + (users.get(0) == newUsers.get(0))); User[] userArray = new User[]{ new User()}; - User[] newUserArray = ModelCopier.copy(userArray); + User[] newUserArray = ModelUtil.copy(userArray); System.out.println("userArray == newUserArray ---> " + (userArray == newUserArray)); diff --git a/src/test/java/io/jboot/test/redis/RedisTester.java b/src/test/java/io/jboot/test/redis/RedisTester.java new file mode 100644 index 0000000000000000000000000000000000000000..45bb80db81c168e528a88d1e60b81226e6e57e30 --- /dev/null +++ b/src/test/java/io/jboot/test/redis/RedisTester.java @@ -0,0 +1,95 @@ +package io.jboot.test.redis; + +import io.jboot.app.JbootApplication; +import io.jboot.components.limiter.redis.RedisRateLimitUtil; +import io.jboot.support.redis.JbootRedis; +import io.jboot.support.redis.JbootRedisManager; +import io.jboot.support.redis.RedisScanResult; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class RedisTester { + + @Before + public void config() { + JbootApplication.setBootArg("jboot.redis.host", "127.0.0.1"); + JbootApplication.setBootArg("jboot.redis.port", "6379"); + } + + @Test + public void testGetAndSet() { + JbootRedis redis = JbootRedisManager.me().getRedis(); + String key = "JbootRedisValue"; + Assert.assertEquals("OK", redis.set(key, "10")); + Assert.assertEquals("10", redis.get(key)); + redis.del(key); + } + + @Test + public void testEval() { + JbootRedis redis = JbootRedisManager.me().getRedis(); + String response = (String) redis.eval("return KEYS[1]", 1, "key1"); + Assert.assertEquals("key1", response); + } + + @Test + public void testRateLimit() { + String resource = "limited-resource"; + Assert.assertTrue(RedisRateLimitUtil.tryAcquire(resource, 2, 1)); + Assert.assertTrue(RedisRateLimitUtil.tryAcquire(resource, 2, 1)); + Assert.assertFalse(RedisRateLimitUtil.tryAcquire(resource, 2, 1)); + } + + public static void main(String[] args) { + JbootApplication.setBootArg("jboot.redis.host", "127.0.0.1"); + JbootApplication.setBootArg("jboot.redis.database", "3"); + +// for (int i=0;i<2350;i++){ +// redis.set("testkey:"+i,i); +// } +// System.out.println("set ok"); +// +// System.out.println(getKeys("testkey").size()); +// +// +// JbootRedisCacheImpl redisCache = new JbootRedisCacheImpl(); +// for (int i = 0; i < 23; i++) { +// redisCache.put("myName", "myKey" + i, i); +// } +// System.out.println(redisCache.getKeys("myName")); +// System.out.println(redisCache.getNames()); +// redisCache.removeAll("myName"); +// System.out.println(redisCache.getKeys("myName")); +// System.out.println(redisCache.getNames()); + + } + + public static List getKeys(String cacheName) { + JbootRedis redis = JbootRedisManager.me().getRedis(); + ; + List keys = new ArrayList<>(); + String cursor = "0"; + int scanCount = 1000; + List scanResult = null; + do { + RedisScanResult redisScanResult = redis.scan(cacheName + ":*", cursor, scanCount); + if (redisScanResult != null) { + scanResult = redisScanResult.getResults(); + cursor = redisScanResult.getCursor(); + if (scanResult != null && scanResult.size() > 0) { + keys.addAll(scanResult); + } + if (redisScanResult.isCompleteIteration()) { + //终止循环 + scanResult = null; + } + } + } while (scanResult != null && scanResult.size() != 0); + + return keys; + } +} diff --git a/src/test/java/io/jboot/test/rpc/CallBack.java b/src/test/java/io/jboot/test/rpc/CallBack.java new file mode 100644 index 0000000000000000000000000000000000000000..daeda80698bfacd7f2102660140df8ecdc629605 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/CallBack.java @@ -0,0 +1,15 @@ +package io.jboot.test.rpc; + +public class CallBack { + + public void oninvoke(){ + System.out.println("CallBack.oninvoke..."); + + + } + + public void onthrow(Throwable ex) { +// errors.put(id, ex); + System.out.println(">>>>>onthrow>>>>"); + } +} diff --git a/src/test/java/io/jboot/test/rpc/RpcBeans.java b/src/test/java/io/jboot/test/rpc/RpcBeans.java new file mode 100644 index 0000000000000000000000000000000000000000..ef517e3f6fad099e43d42b2f08c690b3ee6f7487 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/RpcBeans.java @@ -0,0 +1,14 @@ +package io.jboot.test.rpc; + +import io.jboot.aop.annotation.Bean; +import io.jboot.aop.annotation.Configuration; + +@Configuration +public class RpcBeans { + + @Bean(name = "callback") + public CallBack createCallback(){ + System.out.println("createCallback..."); + return new CallBack(); + } +} diff --git a/src/test/java/io/jboot/test/rpc/commons/BlogServiceMock.java b/src/test/java/io/jboot/test/rpc/commons/BlogServiceMock.java new file mode 100644 index 0000000000000000000000000000000000000000..a4efa8a36f2c71da4df28a5c6b5a7e9eb9cc7c0e --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/commons/BlogServiceMock.java @@ -0,0 +1,21 @@ +package io.jboot.test.rpc.commons; + +import com.google.common.collect.Lists; +import io.jboot.components.rpc.annotation.RPCBean; + +import java.util.List; + +@RPCBean +public class BlogServiceMock implements BlogService { + + @Override + public String findById() { + System.err.println("BlogServiceMock.findById() invoked."); + return "id from BlogServiceMock"; + } + + @Override + public List findAll() { + return Lists.newArrayList("item1","item2"); + } +} diff --git a/src/test/java/io/jboot/test/rpc/commons/BlogServiceProvider.java b/src/test/java/io/jboot/test/rpc/commons/BlogServiceProvider.java index 4c3e62d0ff7157cecd7bcab75c9ab53bfe10f0cf..88896f36c47b61a78b7f7caed0be97829399c0d6 100644 --- a/src/test/java/io/jboot/test/rpc/commons/BlogServiceProvider.java +++ b/src/test/java/io/jboot/test/rpc/commons/BlogServiceProvider.java @@ -10,8 +10,8 @@ public class BlogServiceProvider implements BlogService { @Override public String findById() { - System.out.println("BlogServiceProvider.findById() invoked."); - return "id from provider"; + System.err.println("BlogServiceProvider.findById() invoked."); + return "id from BlogServiceProvider"; } @Override diff --git a/src/test/java/io/jboot/test/rpc/commons/BookService.java b/src/test/java/io/jboot/test/rpc/commons/BookService.java new file mode 100644 index 0000000000000000000000000000000000000000..9c34a5b65d5e1c6bda4cc7307fa03e6c8d2885f7 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/commons/BookService.java @@ -0,0 +1,12 @@ +package io.jboot.test.rpc.commons; + + +import java.util.List; + +public interface BookService { + + public String findById(); + public List findAll(); + + public void doOther(); +} diff --git a/src/test/java/io/jboot/test/rpc/commons/BookServiceProvider.java b/src/test/java/io/jboot/test/rpc/commons/BookServiceProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..3be92a5f5702a5f312a172887c8f7b752965f769 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/commons/BookServiceProvider.java @@ -0,0 +1,26 @@ +package io.jboot.test.rpc.commons; + +import com.google.common.collect.Lists; +import io.jboot.components.rpc.annotation.RPCBean; + +import java.util.List; + +@RPCBean +public class BookServiceProvider implements BookService { + + @Override + public String findById() { + System.err.println("BookServiceProvider.findById() invoked."); + return "id from BookServiceProvider"; + } + + @Override + public List findAll() { + return Lists.newArrayList("item1","item2"); + } + + @Override + public void doOther() { + + } +} diff --git a/src/test/java/io/jboot/test/rpc/dubbo/DubboClient.java b/src/test/java/io/jboot/test/rpc/dubbo/DubboClient.java index 85005bb8e577f4a544d4227cc8d162a4d9abc3ea..b6b433d7a3e286caa3c6451925751532440bf940 100644 --- a/src/test/java/io/jboot/test/rpc/dubbo/DubboClient.java +++ b/src/test/java/io/jboot/test/rpc/dubbo/DubboClient.java @@ -1,3 +1,18 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jboot.test.rpc.dubbo; @@ -14,20 +29,29 @@ public class DubboClient extends JbootController { //Undertow端口号配置 - JbootApplication.setBootArg("undertow.port", "8888"); + JbootApplication.setBootArg("undertow.port", "9999"); //RPC配置 JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); - JbootApplication.setBootArg("jboot.rpc.callMode", "direct");//直连模式,默认为注册中心 - JbootApplication.setBootArg("jboot.rpc.directUrl", "127.0.0.1:8000");//直连模式的server url地址 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", false); + + JbootApplication.setBootArg("jboot.rpc.dubbo.method.name", "findById"); + JbootApplication.setBootArg("jboot.rpc.dubbo.method.oninvoke", "callback.oninvoke"); + JbootApplication.setBootArg("jboot.rpc.dubbo.method.onthrow", "callback.onthrow"); + + + //设置直连模式,方便调试,默认为注册中心 + JbootApplication.setBootArg("jboot.rpc.urls", "io.jboot.test.rpc.commons.BlogService:127.0.0.1:28080"); + JbootApplication.run(args); } - @RPCInject + @RPCInject(timeout = 3300, check = false) private BlogService blogService; + // @Before(DubboInterceptor.class) public void index() { System.out.println("blogService:" + blogService); diff --git a/src/test/java/io/jboot/test/rpc/dubbo/DubboInterceptor.java b/src/test/java/io/jboot/test/rpc/dubbo/DubboInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..0a55dc81fabec4f9e1f2f1be5c41818486be288c --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbo/DubboInterceptor.java @@ -0,0 +1,39 @@ +///** +// * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). +// *

+// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// *

+// * http://www.apache.org/licenses/LICENSE-2.0 +// *

+// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +//package io.jboot.test.rpc.dubbo; +// +//import com.jfinal.aop.Interceptor; +//import com.jfinal.aop.Invocation; +//import io.jboot.components.rpc.annotation.RPCInject; +//import io.jboot.test.rpc.commons.BlogService; +// +///** +// * @author michael yang (fuhai999@gmail.com) +// * @Date: 2020/3/24 +// */ +//public class DubboInterceptor implements Interceptor { +// +// @RPCInject(check = false) +// private BlogService blogService; +// +// @Override +// public void intercept(Invocation inv) { +// +// System.out.println("intercept : " + blogService); +// +// inv.invoke(); +// } +//} diff --git a/src/test/java/io/jboot/test/rpc/dubbo/DubboServer.java b/src/test/java/io/jboot/test/rpc/dubbo/DubboServer.java index cce8bb5d1ddc888afbc06060604d24277a42c7b7..2a74cd5b497149acc897f13f9afd8aaf62cf13fb 100644 --- a/src/test/java/io/jboot/test/rpc/dubbo/DubboServer.java +++ b/src/test/java/io/jboot/test/rpc/dubbo/DubboServer.java @@ -1,26 +1,41 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.jboot.test.rpc.dubbo; import io.jboot.app.JbootApplication; +import io.jboot.app.JbootSimpleApplication; public class DubboServer { public static void main(String[] args) throws InterruptedException { - JbootApplication.setBootArg("undertow.port", "8887"); JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); - //开启 @RPCBean 自动暴露功能,默认情况下是自动暴露的,但是 jboot.properties 文件关闭了,这里需要开启下 + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); - //设置直连模式,方便调试,默认为注册中心 - JbootApplication.setBootArg("jboot.rpc.callMode", "direct"); + //dubbo 的通信协议配置 + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name", "dubbo"); + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port", "28080"); - //直连模式的url地址 - JbootApplication.setBootArg("jboot.rpc.directUrl", "127.0.0.1:8000"); - JbootApplication.run(args); + JbootSimpleApplication.run(args); System.out.println("DubboServer started..."); diff --git a/src/test/java/io/jboot/test/rpc/dubbo3/comsumer/ConsumerStarter.java b/src/test/java/io/jboot/test/rpc/dubbo3/comsumer/ConsumerStarter.java new file mode 100644 index 0000000000000000000000000000000000000000..43bc6669ff36064e48c05c85824bdad69aeb7d5d --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbo3/comsumer/ConsumerStarter.java @@ -0,0 +1,43 @@ +package io.jboot.test.rpc.dubbo3.comsumer; + +import io.jboot.app.JbootApplication; + +public class ConsumerStarter { + public static void main(String[] args) { + + /* + * undertow.devMode=true + * undertow.port=8082 + * jboot.rpc.type = dubbo + * jboot.rpc.autoExportEnable=true + * jboot.rpc.application.service-discovery.migration=FORCE_APPLICATION + * jboot.rpc.dubbo.protocol.name=dubbo + * jboot.rpc.dubbo.protocol.port=28080 + * jboot.rpc.dubbo.registry.protocol=nacos + * jboot.rpc.dubbo.registry.address=127.0.0.1:8848 + * jboot.rpc.dubbo.application.name = BlogConsumer + * jboot.rpc.dubbo.application.version = 1.0 + */ + + JbootApplication.setBootArg("undertow.devMode","true"); + JbootApplication.setBootArg("undertow.port","8082"); + + JbootApplication.setBootArg("jboot.rpc.type","dubbo"); + JbootApplication.setBootArg("jboot.rpc.autoExportEnable","true"); + + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name","dubbo"); + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port","28080"); + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol","nacos"); + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address","127.0.0.1:8848"); + JbootApplication.setBootArg("jboot.rpc.dubbo.application.name","BlogConsumer"); + JbootApplication.setBootArg("jboot.rpc.dubbo.application.version","1.0"); + + //dubbo3 主要是增加这个配置 + JbootApplication.setBootArg("jboot.rpc.application.service-discovery.migration","FORCE_APPLICATION"); + + + + JbootApplication.run(args); + System.out.println("Consumer Started"); + } +} \ No newline at end of file diff --git a/src/test/java/io/jboot/test/rpc/dubbo3/comsumer/controller/DubboClient.java b/src/test/java/io/jboot/test/rpc/dubbo3/comsumer/controller/DubboClient.java new file mode 100644 index 0000000000000000000000000000000000000000..45bdae5983e76e4373e6581b3ba146cf007735c1 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbo3/comsumer/controller/DubboClient.java @@ -0,0 +1,26 @@ +package io.jboot.test.rpc.dubbo3.comsumer.controller; + +import com.alibaba.fastjson.JSONArray; +import io.jboot.components.rpc.annotation.RPCInject; +import io.jboot.test.rpc.dubbo3.service.BlogService; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/dubbo3") +public class DubboClient extends JbootController { + + @RPCInject + private BlogService blogService; + + + public void index() { + System.out.println(blogService); + renderText("blogId : " + blogService.findById()); + } + + + public void blogList() { + System.out.println(blogService); + renderText("blogList : " + JSONArray.toJSONString(blogService.findAll())); + } +} diff --git a/src/test/java/io/jboot/test/rpc/dubbo3/provider/ProviderStarter.java b/src/test/java/io/jboot/test/rpc/dubbo3/provider/ProviderStarter.java new file mode 100644 index 0000000000000000000000000000000000000000..064967070cdcca76e54bebcddc7909c14ec957dd --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbo3/provider/ProviderStarter.java @@ -0,0 +1,41 @@ +package io.jboot.test.rpc.dubbo3.provider; + +import io.jboot.app.JbootSimpleApplication; + +public class ProviderStarter { + public static void main(String[] args) { + + /* + * undertow.devMode=true + * undertow.port=8081 + * jboot.rpc.type = dubbo + * jboot.rpc.autoExportEnable=true + * jboot.rpc.dubbo.application.version = 1.0 + * jboot.rpc.dubbo.protocol.name=dubbo + * jboot.rpc.dubbo.protocol.port=28080 + * jboot.rpc.dubbo.registry.protocol=nacos + * jboot.rpc.dubbo.registry.address=127.0.0.1:8848 + * jboot.rpc.dubbo.application.name = BlogProvider + * + * jboot.rpc.dubbo.registry.registerMode=instance + */ + + JbootSimpleApplication.setBootArg("jboot.rpc.type","dubbo"); + JbootSimpleApplication.setBootArg("jboot.rpc.autoExportEnable","true"); + + JbootSimpleApplication.setBootArg("jboot.rpc.dubbo.protocol.name","dubbo"); + JbootSimpleApplication.setBootArg("jboot.rpc.dubbo.protocol.port","28080"); + JbootSimpleApplication.setBootArg("jboot.rpc.dubbo.registry.protocol","nacos"); + JbootSimpleApplication.setBootArg("jboot.rpc.dubbo.registry.address","127.0.0.1:8848"); + JbootSimpleApplication.setBootArg("jboot.rpc.dubbo.application.name","BlogProvider"); + JbootSimpleApplication.setBootArg("jboot.rpc.dubbo.application.version","1.0"); + + //dubbo3 主要是添加这个配置 + JbootSimpleApplication.setBootArg("jboot.rpc.dubbo.registry.registerMod","instance"); + + + JbootSimpleApplication.run(args); + + System.out.println("Provider Started"); + } +} \ No newline at end of file diff --git a/src/test/java/io/jboot/test/rpc/dubbo3/provider/service/impl/BlogServiceProvider.java b/src/test/java/io/jboot/test/rpc/dubbo3/provider/service/impl/BlogServiceProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..3c42f6a7370c90c9b846b69d428d5910686212de --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbo3/provider/service/impl/BlogServiceProvider.java @@ -0,0 +1,22 @@ +package io.jboot.test.rpc.dubbo3.provider.service.impl; + +import com.google.common.collect.Lists; +import io.jboot.components.rpc.annotation.RPCBean; +import io.jboot.test.rpc.dubbo3.service.BlogService; + +import java.util.List; + +@RPCBean +public class BlogServiceProvider implements BlogService { + + + @Override + public String findById() { + return "id from provider"; + } + + @Override + public List findAll() { + return Lists.newArrayList("item1","item2"); + } +} diff --git a/src/test/java/io/jboot/test/rpc/dubbo3/readme.md b/src/test/java/io/jboot/test/rpc/dubbo3/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..78f967f2b505a9e868a24cafa2bf8237fdf072dd --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbo3/readme.md @@ -0,0 +1,10 @@ +整体上来说,jboot 升级 dubbo2 到 dubbo3 是不需要修改其他任何代码的 + +理论上只需要添加两个配置就可以了 + +- 1、生产端增加配置: +jboot.rpc.dubbo.registry.registerMode=instance + + +- 2、消费端配置: +jboot.rpc.application.service-discovery.migration=FORCE_APPLICATION diff --git a/src/test/java/io/jboot/test/rpc/dubbo3/service/BlogService.java b/src/test/java/io/jboot/test/rpc/dubbo3/service/BlogService.java new file mode 100644 index 0000000000000000000000000000000000000000..831826a707d21cb18f34c0a6de748a7a450c8aa2 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbo3/service/BlogService.java @@ -0,0 +1,8 @@ +package io.jboot.test.rpc.dubbo3.service; + +import java.util.List; + +public interface BlogService { + String findById(); + List findAll(); +} diff --git a/src/test/java/io/jboot/test/rpc/dubbonacos/DubboClientNacosDemo.java b/src/test/java/io/jboot/test/rpc/dubbonacos/DubboClientNacosDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..39ac6f30056cbbae3d8dd1ccefb01082cb30e63c --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbonacos/DubboClientNacosDemo.java @@ -0,0 +1,47 @@ +package io.jboot.test.rpc.dubbonacos; + +import io.jboot.app.JbootApplication; +import io.jboot.components.rpc.annotation.RPCInject; +import io.jboot.test.rpc.commons.BlogService; +import io.jboot.test.rpc.commons.BookService; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/dubbonacos") +public class DubboClientNacosDemo extends JbootController { + + public static void main(String[] args) { + + //jboot端口号配置 + JbootApplication.setBootArg("undertow.port", "9999"); + + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + + // dubbo 的注册中心的协议,支持的类型有 dubbo, multicast, zookeeper, redis, consul(2.7.1), sofa(2.7.2), etcd(2.7.2), nacos(2.7.2) + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "nacos"); + //注册中心地址,即 nacos 的地址 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:8848"); + + JbootApplication.run(args); + } + + + @RPCInject + private BlogService blogService; + + + @RPCInject + private BookService bookService; + + public void index() { + + System.out.println("DubboClientNacosDemo.index()"); + + System.out.println(blogService.findById()); + System.out.println(bookService.findById()); + + + renderText("ok"); + } +} diff --git a/src/test/java/io/jboot/test/rpc/dubbonacos/DubboServer1NacosDemo.java b/src/test/java/io/jboot/test/rpc/dubbonacos/DubboServer1NacosDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..4d313d7ba8ef1148fbe2b6dfe004b34d7c0d8a20 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbonacos/DubboServer1NacosDemo.java @@ -0,0 +1,38 @@ +package io.jboot.test.rpc.dubbonacos; + + +import io.jboot.app.JbootApplication; +import io.jboot.app.JbootSimpleApplication; + +public class DubboServer1NacosDemo { + + public static void main(String[] args) { + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + + + // dubbo 的注册中心的协议,支持的类型有 dubbo, multicast, zookeeper, redis, consul(2.7.1), sofa(2.7.2), etcd(2.7.2), nacos(2.7.2) + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "nacos"); + //注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:8848"); + + + //dubbo 的通信协议配置,name 可以不用配置,默认值为 dubbo + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name", "dubbo"); + //dubbo 的通信协议配置,如果port配置为-1,则会分配一个没有被占用的端口。 + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port", "28080"); + + + + JbootSimpleApplication.run(args); + + + System.out.println("DubboServer1NacosDemo started..."); + + + } +} diff --git a/src/test/java/io/jboot/test/rpc/dubbonacos/DubboServer2NacosDemo.java b/src/test/java/io/jboot/test/rpc/dubbonacos/DubboServer2NacosDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..9e3b2a7c652ba7ac659ec28e755a1069e479ee85 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbonacos/DubboServer2NacosDemo.java @@ -0,0 +1,37 @@ +package io.jboot.test.rpc.dubbonacos; + + +import io.jboot.app.JbootApplication; +import io.jboot.app.JbootSimpleApplication; + +public class DubboServer2NacosDemo { + + public static void main(String[] args) { + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + + + // dubbo 的注册中心的协议,支持的类型有 dubbo, multicast, zookeeper, redis, consul(2.7.1), sofa(2.7.2), etcd(2.7.2), nacos(2.7.2) + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "nacos"); + //注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:8848"); + + + //dubbo 的通信协议配置,name 可以不用配置,默认值为 dubbo + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name", "dubbo"); + //dubbo 的通信协议配置,如果port配置为-1,则会分配一个没有被占用的端口。 + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port", "28081"); + + JbootSimpleApplication.run(args); + + + + System.out.println("DubboServer2NacosDemo started..."); + + + } +} diff --git a/src/test/java/io/jboot/test/rpc/dubbonacos/readme.md b/src/test/java/io/jboot/test/rpc/dubbonacos/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..6514ca2cd063ede249ca0ce5cc729660a9f684a4 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/dubbonacos/readme.md @@ -0,0 +1,21 @@ +在开始之前,先启动 nacos,启动建议使用 docker 的方式,更加方便。 + +Clone 项目 + +```shell +git clone https://github.com/nacos-group/nacos-docker.git +cd nacos-docker +``` + +单机模式 Derby + +```shell +docker-compose -f example/standalone-derby.yaml up +``` + + +访问Nacos 控制台 :[http://127.0.0.1:8848/nacos/ +](http://127.0.0.1:8848/nacos/) + + +更多参考 https://nacos.io/zh-cn/docs/quick-start-docker.html \ No newline at end of file diff --git a/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboClientZookeeperDemo.java b/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboClientZookeeperDemo.java index 53999315897cb508a32ef72c66932a95fdb5c907..f06311b14b1ede6001481ab404c71581ac5d3ef3 100644 --- a/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboClientZookeeperDemo.java +++ b/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboClientZookeeperDemo.java @@ -12,27 +12,30 @@ public class DubboClientZookeeperDemo extends JbootController { public static void main(String[] args) { //jboot端口号配置 - JbootApplication.setBootArg("undertow.port", "8888"); + JbootApplication.setBootArg("undertow.port", "9999"); - //RPC配置 JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); - JbootApplication.setBootArg("jboot.rpc.callMode", "registry");//注册中心模式 - JbootApplication.setBootArg("jboot.rpc.registryType", "zookeeper");//注册中心的类型:zookeeper - JbootApplication.setBootArg("jboot.rpc.registryAddress", "127.0.0.1:2181");//注册中心,即zookeeper的地址 + + // dubbo 的注册中心的协议,支持的类型有 dubbo, multicast, zookeeper, redis, consul(2.7.1), sofa(2.7.2), etcd(2.7.2), nacos(2.7.2) + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "zookeeper"); + //注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:2181"); + + JbootApplication.setBootArg("jboot.rpc.dubbo.consumer.mock", "true"); JbootApplication.run(args); } - @RPCInject + @RPCInject(check = false) private BlogService blogService; public void index() { System.out.println("DubboClientZookeeperDemo.index()"); - System.out.println(blogService); - System.out.println(blogService.findById()); - renderText("ok"); + System.out.println("blogService>>>>" + blogService); + + renderText(blogService.findById()); } } diff --git a/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboServer1ZookeeperDemo.java b/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboServer1ZookeeperDemo.java index 80edab49c0c7008e71aeb052cddac3baa9aac335..b03d0318dbb8da98e944597e31e105da6dab8b0d 100644 --- a/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboServer1ZookeeperDemo.java +++ b/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboServer1ZookeeperDemo.java @@ -2,23 +2,32 @@ package io.jboot.test.rpc.dubbozookeeper; import io.jboot.app.JbootApplication; +import io.jboot.app.JbootSimpleApplication; public class DubboServer1ZookeeperDemo { public static void main(String[] args) { - //jboot端口号配置 - JbootApplication.setBootArg("undertow.port", "8081"); - + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); - JbootApplication.setBootArg("jboot.rpc.callMode", "registry");//注册中心模式 - JbootApplication.setBootArg("jboot.rpc.registryType", "zookeeper");//注册中心的类型:zookeeper - JbootApplication.setBootArg("jboot.rpc.registryAddress", "127.0.0.1:2181");//注册中心,即zookeeper的地址 - //开启 @RPCBean 自动暴露功能,默认情况下是自动暴露的,但是 jboot.properties 文件关闭了,这里需要开启下 - JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); - JbootApplication.run(args); + // dubbo 的注册中心的协议,支持的类型有 dubbo, multicast, zookeeper, redis, consul(2.7.1), sofa(2.7.2), etcd(2.7.2), nacos(2.7.2) + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "zookeeper"); + //注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:2181"); + + + //dubbo 的通信协议配置,name 可以不用配置,默认值为 dubbo + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name", "dubbo"); + //dubbo 的通信协议配置,如果port配置为-1,则会分配一个没有被占用的端口。 + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port", "28080"); + + + + JbootSimpleApplication.run(args); System.out.println("DubboServer1ZookeeperDemo started..."); diff --git a/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboServer2ZookeeperDemo.java b/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboServer2ZookeeperDemo.java index 50b817965fe20e173cb048df684a07456e128d83..62a806455194461f0721119cac1857145aaad4f7 100644 --- a/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboServer2ZookeeperDemo.java +++ b/src/test/java/io/jboot/test/rpc/dubbozookeeper/DubboServer2ZookeeperDemo.java @@ -2,23 +2,32 @@ package io.jboot.test.rpc.dubbozookeeper; import io.jboot.app.JbootApplication; +import io.jboot.app.JbootSimpleApplication; public class DubboServer2ZookeeperDemo { public static void main(String[] args) { - //jboot端口号配置 - JbootApplication.setBootArg("undertow.port", "8082"); + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); - JbootApplication.setBootArg("jboot.rpc.callMode", "registry");//注册中心模式 - JbootApplication.setBootArg("jboot.rpc.registryType", "zookeeper");//注册中心的类型:zookeeper - JbootApplication.setBootArg("jboot.rpc.registryAddress", "127.0.0.1:2181");//注册中心,即zookeeper的地址 - //开启 @RPCBean 自动暴露功能,默认情况下是自动暴露的,但是 jboot.properties 文件关闭了,这里需要开启下 - JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); - JbootApplication.run(args); + + // dubbo 的注册中心的协议,支持的类型有 dubbo, multicast, zookeeper, redis, consul(2.7.1), sofa(2.7.2), etcd(2.7.2), nacos(2.7.2) + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "zookeeper"); + //注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:2181"); + + + //dubbo 的通信协议配置,name 可以不用配置,默认值为 dubbo + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name", "dubbo"); + //dubbo 的通信协议配置,如果port配置为-1,则会分配一个没有被占用的端口。 + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port", "28081"); + + JbootSimpleApplication.run(args); diff --git a/src/test/java/io/jboot/test/rpc/motan/MotanClient.java b/src/test/java/io/jboot/test/rpc/motan/MotanClient.java new file mode 100644 index 0000000000000000000000000000000000000000..07c91cfa7a08615d318af3fe0e40a8a79f956c8e --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/motan/MotanClient.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.rpc.motan; + + +import io.jboot.app.JbootApplication; +import io.jboot.components.rpc.annotation.RPCInject; +import io.jboot.test.rpc.commons.BlogService; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/motan") +public class MotanClient extends JbootController { + + public static void main(String[] args) { + + + //Undertow端口号配置 + JbootApplication.setBootArg("undertow.port", "9999"); + + //RPC配置 + JbootApplication.setBootArg("jboot.rpc.type", "motan"); + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", false); + + //设置直连模式,方便调试,默认为注册中心 + JbootApplication.setBootArg("jboot.rpc.urls", "io.jboot.test.rpc.commons.BlogService:127.0.0.1:28080"); + + + JbootApplication.run(args); + } + + + @RPCInject + private BlogService blogService; + +// @Before(MotanInterceptor.class) + public void index() { + + System.out.println("blogService:" + blogService); + + renderText("blogId : " + blogService.findById()); + } + + +} diff --git a/src/test/java/io/jboot/test/rpc/motan/MotanInterceptor.java b/src/test/java/io/jboot/test/rpc/motan/MotanInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..ef891054fbb0ac596343f2dc59f2970d13838191 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/motan/MotanInterceptor.java @@ -0,0 +1,39 @@ +///** +// * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). +// *

+// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// *

+// * http://www.apache.org/licenses/LICENSE-2.0 +// *

+// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +//package io.jboot.test.rpc.motan; +// +//import com.jfinal.aop.Interceptor; +//import com.jfinal.aop.Invocation; +//import io.jboot.components.rpc.annotation.RPCInject; +//import io.jboot.test.rpc.commons.BlogService; +// +///** +// * @author michael yang (fuhai999@gmail.com) +// * @Date: 2020/3/24 +// */ +//public class MotanInterceptor implements Interceptor { +// +// @RPCInject(check = false) +// private BlogService blogService; +// +// @Override +// public void intercept(Invocation inv) { +// +// System.out.println("intercept : " + blogService); +// +// inv.invoke(); +// } +//} diff --git a/src/test/java/io/jboot/test/rpc/motan/MotanServer.java b/src/test/java/io/jboot/test/rpc/motan/MotanServer.java new file mode 100644 index 0000000000000000000000000000000000000000..4891d37344130b386ba50f0dd2d3b3a110fbc62e --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/motan/MotanServer.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jboot.test.rpc.motan; + + +import io.jboot.app.JbootApplication; +import io.jboot.app.JbootSimpleApplication; + +public class MotanServer { + + public static void main(String[] args) throws InterruptedException { + + + JbootApplication.setBootArg("jboot.rpc.type", "motan"); + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + + // motan 与 dubbo 不一样,motan 需要配置 export, + // export 配置内容为 协议ID:端口号,默认的协议 id 为 default + JbootApplication.setBootArg("jboot.rpc.motan.defaultExport", "default:28080"); + + + + JbootSimpleApplication.run(args); + + System.out.println("MotanServer started..."); + + } +} diff --git a/src/test/java/io/jboot/test/rpc/motanzookeeper/MotanClientZookeeperDemo.java b/src/test/java/io/jboot/test/rpc/motanzookeeper/MotanClientZookeeperDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..a30c1396beaccdf99e0e4bd2a6715a6c40fdba20 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/motanzookeeper/MotanClientZookeeperDemo.java @@ -0,0 +1,39 @@ +package io.jboot.test.rpc.motanzookeeper; + +import io.jboot.app.JbootApplication; +import io.jboot.components.rpc.annotation.RPCInject; +import io.jboot.test.rpc.commons.BlogService; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/motanzk") +public class MotanClientZookeeperDemo extends JbootController { + + public static void main(String[] args) { + + //jboot端口号配置 + JbootApplication.setBootArg("undertow.port", "9999"); + + JbootApplication.setBootArg("jboot.rpc.type", "motan"); + + // motan 的注册中心的协议 + JbootApplication.setBootArg("jboot.rpc.motan.registry.regProtocol", "zookeeper"); + // 注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.motan.registry.address", "127.0.0.1:2181"); + + JbootApplication.run(args); + } + + + @RPCInject + private BlogService blogService; + + public void index() { + + System.out.println("MotanClientZookeeperDemo.index()"); + + System.out.println(blogService); + + renderText(blogService.findById()); + } +} diff --git a/src/test/java/io/jboot/test/rpc/motanzookeeper/MotanServer1ZookeeperDemo.java b/src/test/java/io/jboot/test/rpc/motanzookeeper/MotanServer1ZookeeperDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..0d875123b613da9040818ce725e6989390b0dc2d --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/motanzookeeper/MotanServer1ZookeeperDemo.java @@ -0,0 +1,35 @@ +package io.jboot.test.rpc.motanzookeeper; + + +import io.jboot.app.JbootApplication; +import io.jboot.app.JbootSimpleApplication; + +public class MotanServer1ZookeeperDemo { + + public static void main(String[] args) { + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + JbootApplication.setBootArg("jboot.rpc.type", "motan"); + + + // motan 的注册中心的协议 + JbootApplication.setBootArg("jboot.rpc.motan.registry.regProtocol", "zookeeper"); + //注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.motan.registry.address", "127.0.0.1:2181"); + + + //export + JbootApplication.setBootArg("jboot.rpc.motan.defaultExport", "default:28080"); + + + + JbootSimpleApplication.run(args); + + + System.out.println("MotanServer1ZookeeperDemo started..."); + + + } +} diff --git a/src/test/java/io/jboot/test/rpc/motanzookeeper/MotanServer2ZookeeperDemo.java b/src/test/java/io/jboot/test/rpc/motanzookeeper/MotanServer2ZookeeperDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..42b462fc8246dd0e2919a334b9a2dddcd38c5bd3 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/motanzookeeper/MotanServer2ZookeeperDemo.java @@ -0,0 +1,36 @@ +package io.jboot.test.rpc.motanzookeeper; + + +import io.jboot.app.JbootApplication; +import io.jboot.app.JbootSimpleApplication; + +public class MotanServer2ZookeeperDemo { + + public static void main(String[] args) { + + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + JbootApplication.setBootArg("jboot.rpc.type", "motan"); + + + + // motan 的注册中心的协议 + JbootApplication.setBootArg("jboot.rpc.motan.registry.regProtocol", "zookeeper"); + //注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.motan.registry.address", "127.0.0.1:2181"); + + + //export + JbootApplication.setBootArg("jboot.rpc.motan.defaultExport", "default:28081"); + + JbootSimpleApplication.run(args); + + + + System.out.println("MotanServer2ZookeeperDemo started..."); + + + } +} diff --git a/src/test/java/io/jboot/test/rpc/multiregistry/DubboClientMultiDemo.java b/src/test/java/io/jboot/test/rpc/multiregistry/DubboClientMultiDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..12d4e75e51a659ce1259e604d0ba7774df7938ef --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/multiregistry/DubboClientMultiDemo.java @@ -0,0 +1,43 @@ +package io.jboot.test.rpc.multiregistry; + +import io.jboot.app.JbootApplication; +import io.jboot.components.rpc.annotation.RPCInject; +import io.jboot.test.rpc.commons.BlogService; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/dubbo/mutil") +public class DubboClientMultiDemo extends JbootController { + + public static void main(String[] args) { + + //jboot端口号配置 + JbootApplication.setBootArg("undertow.port", "9005"); + + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + + //两个注册中心,id 分别为 default、zk + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "nacos"); + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:8848"); + + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.zk.protocol", "zookeeper"); + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.zk.address", "127.0.0.1:2181"); + + + + JbootApplication.run(args); + } + + + @RPCInject + private BlogService blogService; + + public void index() { + + System.out.println("DubboClientNacosDemo.index()"); + + System.out.println(blogService); + renderText(blogService.findById()); + } +} diff --git a/src/test/java/io/jboot/test/rpc/multiregistry/DubboClientNacosDemo.java b/src/test/java/io/jboot/test/rpc/multiregistry/DubboClientNacosDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..7788ec28068f23215d8dc2a2f619b7f1da9e815c --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/multiregistry/DubboClientNacosDemo.java @@ -0,0 +1,41 @@ +package io.jboot.test.rpc.multiregistry; + +import io.jboot.app.JbootApplication; +import io.jboot.components.rpc.annotation.RPCInject; +import io.jboot.test.rpc.commons.BlogService; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/dubbo/nacos") +public class DubboClientNacosDemo extends JbootController { + + public static void main(String[] args) { + + //jboot端口号配置 + JbootApplication.setBootArg("undertow.port", "9004"); + + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + + // dubbo 的注册中心的协议,支持的类型有 dubbo, multicast, zookeeper, redis, consul(2.7.1), sofa(2.7.2), etcd(2.7.2), nacos(2.7.2) + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "nacos"); + //注册中心地址,即 nacos 的地址 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:8848"); + + + + JbootApplication.run(args); + } + + + @RPCInject + private BlogService blogService; + + public void index() { + + System.out.println("DubboClientNacosDemo.index()"); + + System.out.println(blogService); + renderText(blogService.findById()); + } +} diff --git a/src/test/java/io/jboot/test/rpc/multiregistry/DubboClientZkDemo.java b/src/test/java/io/jboot/test/rpc/multiregistry/DubboClientZkDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..99bc1bd793c7dd2461291178ed11502be44559fd --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/multiregistry/DubboClientZkDemo.java @@ -0,0 +1,38 @@ +package io.jboot.test.rpc.multiregistry; + +import io.jboot.app.JbootApplication; +import io.jboot.components.rpc.annotation.RPCInject; +import io.jboot.test.rpc.commons.BlogService; +import io.jboot.web.controller.JbootController; +import io.jboot.web.controller.annotation.RequestMapping; + +@RequestMapping("/dubbo/zk") +public class DubboClientZkDemo extends JbootController { + + public static void main(String[] args) { + + //jboot端口号配置 + JbootApplication.setBootArg("undertow.port", "9003"); + + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.zk.protocol", "zookeeper"); + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.zk.address", "127.0.0.1:2181"); + + + JbootApplication.run(args); + } + + + @RPCInject + private BlogService blogService; + + public void index() { + + System.out.println("DubboClientZkDemo.index()"); + + System.out.println(blogService); + renderText(blogService.findById()); + } +} diff --git a/src/test/java/io/jboot/test/rpc/multiregistry/DubboServerMultiDemo.java b/src/test/java/io/jboot/test/rpc/multiregistry/DubboServerMultiDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..c6f36867aba766c49bae7d5f05f497e92ff3639c --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/multiregistry/DubboServerMultiDemo.java @@ -0,0 +1,65 @@ +package io.jboot.test.rpc.multiregistry; + + +import io.jboot.app.JbootApplication; + +public class DubboServerMultiDemo { + + public static void main(String[] args) { + + //jboot端口号配置 + JbootApplication.setBootArg("undertow.port", "9002"); + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + + + + // 3个注册中心,id 分别为 default、zk、zk2 + // 默认情况下,如果 service 没指定注册中心,service 会向所有的注册中心发布自己的服务 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "nacos"); + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:8848"); + + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.zk.protocol", "zookeeper"); + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.zk.address", "127.0.0.1:2181"); + + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.zk2.protocol", "zookeeper"); + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.zk2.address", "127.0.0.1:2181"); + + + //定义一个名称为 pro3 的 provider,只注册到名称为 default 的注册中心 +// JbootApplication.setBootArg("jboot.rpc.dubbo.provider.pro3.registry", "default"); + + //定义一个名称为 pro3 的 provider,只注册到名称为 zk 的注册中心 + JbootApplication.setBootArg("jboot.rpc.dubbo.provider.pro3.registry", "zk"); + + + + // 让服务 BlogService 使用名称为 pro3 的 provider + // 当然,此项配置也可以在 @RPCInject 里进行配置 + JbootApplication.setBootArg("jboot.rpc.providers", "io.jboot.test.rpc.commons.BlogService:pro3"); + + + + + + + + + //dubbo 的通信协议配置,name 可以不用配置,默认值为 dubbo + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name", "dubbo"); + //dubbo 的通信协议配置,如果port配置为-1,则会分配一个没有被占用的端口。 + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port", "-1"); + + JbootApplication.run(args); + + + + System.out.println("DubboServer2NacosDemo started..."); + + + } +} diff --git a/src/test/java/io/jboot/test/rpc/multiregistry/DubboServerNacosDemo.java b/src/test/java/io/jboot/test/rpc/multiregistry/DubboServerNacosDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..c698a4672df1a165a6c160ea39dd5ca06f888136 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/multiregistry/DubboServerNacosDemo.java @@ -0,0 +1,42 @@ +package io.jboot.test.rpc.multiregistry; + + +import io.jboot.app.JbootApplication; + +public class DubboServerNacosDemo { + + public static void main(String[] args) { + + //jboot端口号配置 + JbootApplication.setBootArg("undertow.port", "9001"); + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + + + + + // dubbo 的注册中心的协议,支持的类型有 dubbo, multicast, zookeeper, redis, consul(2.7.1), sofa(2.7.2), etcd(2.7.2), nacos(2.7.2) + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "nacos"); + //注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:8848"); + + + //dubbo 的通信协议配置,name 可以不用配置,默认值为 dubbo + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name", "dubbo"); + //dubbo 的通信协议配置,如果port配置为-1,则会分配一个没有被占用的端口。 + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port", "-1"); + + + + JbootApplication.run(args); + + + System.out.println("DubboServer1NacosDemo started..."); + + + } +} diff --git a/src/test/java/io/jboot/test/rpc/multiregistry/DubboServerZkDemo.java b/src/test/java/io/jboot/test/rpc/multiregistry/DubboServerZkDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..539c881b5868a982053a32a00192eeff5916eb81 --- /dev/null +++ b/src/test/java/io/jboot/test/rpc/multiregistry/DubboServerZkDemo.java @@ -0,0 +1,39 @@ +package io.jboot.test.rpc.multiregistry; + + +import io.jboot.app.JbootApplication; + +public class DubboServerZkDemo { + + public static void main(String[] args) { + + //jboot端口号配置 + JbootApplication.setBootArg("undertow.port", "9000"); + + // 开启 @RPCBean 自动暴露功能,默认情况下是开启的,无需配置, + // 但是此测试代码的 jboot.properties 文件关闭了,这里需要开启下 + JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); + JbootApplication.setBootArg("jboot.rpc.type", "dubbo"); + + + + // dubbo 的注册中心的协议,支持的类型有 dubbo, multicast, zookeeper, redis, consul(2.7.1), sofa(2.7.2), etcd(2.7.2), nacos(2.7.2) + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.protocol", "zookeeper"); + //注册中心地址,即zookeeper的地址 + JbootApplication.setBootArg("jboot.rpc.dubbo.registry.address", "127.0.0.1:2181"); + + + //dubbo 的通信协议配置,name 可以不用配置,默认值为 dubbo + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.name", "dubbo"); + //dubbo 的通信协议配置,如果port配置为-1,则会分配一个没有被占用的端口。 + JbootApplication.setBootArg("jboot.rpc.dubbo.protocol.port", "-1"); + + JbootApplication.run(args); + + + + System.out.println("DubboServer2NacosDemo started..."); + + + } +} diff --git a/src/test/java/io/jboot/test/seata/account/AccountServiceProvider.java b/src/test/java/io/jboot/test/seata/account/AccountServiceProvider.java index 4f9037c905697c69a59b9ef604e842de2190e57d..6e595dd20d0df67ef4fa04ed8a0e6903ba895098 100644 --- a/src/test/java/io/jboot/test/seata/account/AccountServiceProvider.java +++ b/src/test/java/io/jboot/test/seata/account/AccountServiceProvider.java @@ -1,24 +1,47 @@ package io.jboot.test.seata.account; +import io.jboot.aop.annotation.Bean; import io.jboot.components.rpc.annotation.RPCBean; import io.jboot.service.JbootServiceBase; import io.jboot.test.seata.commons.Account; @RPCBean +@Bean public class AccountServiceProvider extends JbootServiceBase implements IAccountService { - private static Account dao = new Account().dao(); + private static Account dao = new Account(); public boolean deposit(Integer accountId, Integer money) { Account account = dao.findById(accountId); - account.set("Money", account.getInt("Money") + money); + account.set("money", account.getInt("money") + money); if (money > 1000) { - throw new RuntimeException(AccountServiceProvider.class.getSimpleName()+"Dubbo Seata Exception By Hobbit"); + throw new RuntimeException(AccountServiceProvider.class.getSimpleName()+"Dubbo Seata Exception By Hobbit "); } - return account.update(); + return account.saveOrUpdate(); } + @Override + public boolean updateStore(String account, Integer money) { + Account account1 = dao.findFirst("select * from seata_account where account = ? ", account); + account1.set("store", account1.getInt("store") + money); + return account1.saveOrUpdate(); + } + + @Override + public boolean updateRollbackStore(String account, Integer money) { + Account account1 = dao.findFirst("select * from seata_account where account = ? ", account); + account1.set("store", account1.getInt("store") - money); + return account1.saveOrUpdate(); + } + + @Override + public boolean update(String account, Integer money) { + Account account1 = dao.findFirst("select * from seata_account where account = ? ", account); + account1.set("store", account1.getInt("store") - money); + account1.set("money", account1.getInt("money") + money); + return account1.saveOrUpdate(); + } } diff --git a/src/test/java/io/jboot/test/seata/account/IAccountService.java b/src/test/java/io/jboot/test/seata/account/IAccountService.java index 90d5f37eb8fd8503d06d927941a36f4776287ee7..656ef2d49936409203453537dc279e4c9bf86281 100644 --- a/src/test/java/io/jboot/test/seata/account/IAccountService.java +++ b/src/test/java/io/jboot/test/seata/account/IAccountService.java @@ -1,5 +1,13 @@ package io.jboot.test.seata.account; public interface IAccountService { + public boolean deposit(Integer accountId, Integer money); + + public boolean updateStore(String account, Integer money); + + public boolean updateRollbackStore(String account, Integer money); + + public boolean update(String accountId, Integer money); + } diff --git a/src/test/java/io/jboot/test/seata/business/BusinessServiceProvider.java b/src/test/java/io/jboot/test/seata/business/BusinessServiceProvider.java index 690c6660988db346b2f018a56dd14333f1478d1e..43db9dfaf970f7787b1fef51156b1d9220835a70 100644 --- a/src/test/java/io/jboot/test/seata/business/BusinessServiceProvider.java +++ b/src/test/java/io/jboot/test/seata/business/BusinessServiceProvider.java @@ -1,6 +1,7 @@ package io.jboot.test.seata.business; +import com.jfinal.aop.Inject; import io.jboot.components.rpc.annotation.RPCInject; import io.jboot.service.JbootServiceBase; import io.jboot.support.seata.annotation.SeataGlobalTransactional; @@ -10,15 +11,14 @@ import io.jboot.test.seata.stock.IStockService; public class BusinessServiceProvider extends JbootServiceBase { - @RPCInject + @Inject private IAccountService accountService; - @RPCInject + @Inject private IStockService stockService; - @SeataGlobalTransactional(timeoutMills = 300000, name = "Dubbo_Seata_Business_Transactional") public boolean deposit(Integer accountId) { - accountService.deposit(accountId, 1000); - stockService.deposit(accountId, 2000); + accountService.deposit(accountId, 100); + stockService.deposit(accountId, 200); return true; } diff --git a/src/test/java/io/jboot/test/seata/commons/Account.java b/src/test/java/io/jboot/test/seata/commons/Account.java index 8c26ca42499c366cc1b8b00ed854e57ad0dd6ded..0b535ca1eef0d337d38f5d0ff3bad40c48fa7774 100644 --- a/src/test/java/io/jboot/test/seata/commons/Account.java +++ b/src/test/java/io/jboot/test/seata/commons/Account.java @@ -5,11 +5,12 @@ import com.jfinal.plugin.activerecord.IBean; import io.jboot.db.annotation.Table; import io.jboot.db.model.JbootModel; -@Table(tableName = "seata_account", primaryKey = "ID") +@Table(tableName = "seata_account", primaryKey = "id") public class Account extends JbootModel implements IBean { /** * */ private static final long serialVersionUID = 1L; + } diff --git a/src/test/java/io/jboot/test/seata/commons/fescar_.sql b/src/test/java/io/jboot/test/seata/commons/fescar_.sql index 1c69d007b61edf875261466ab0a612838b85f0ce..4e37b9063ddd306dd737909331aa929405ed8bca 100644 --- a/src/test/java/io/jboot/test/seata/commons/fescar_.sql +++ b/src/test/java/io/jboot/test/seata/commons/fescar_.sql @@ -10,6 +10,7 @@ CREATE TABLE `seata_account` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `Account` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `Money` int(11) NULL DEFAULT NULL, + `store` int(10) NULL DEFAULT NULL, PRIMARY KEY (`ID`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; @@ -48,3 +49,15 @@ CREATE TABLE `undo_log` ( PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; + +CREATE TABLE `tcc_fence_log` ( + `xid` varchar(128) NOT NULL COMMENT 'global id', + `branch_id` bigint NOT NULL COMMENT 'branch id', + `action_name` varchar(64) NOT NULL COMMENT 'action name', + `status` tinyint NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)', + `gmt_create` datetime(3) NOT NULL COMMENT 'create time', + `gmt_modified` datetime(3) NOT NULL COMMENT 'update time', + PRIMARY KEY (`xid`,`branch_id`), + KEY `idx_gmt_modified` (`gmt_modified`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; \ No newline at end of file diff --git a/src/test/java/io/jboot/test/seata/interceptor/ExceptionInterceptor.java b/src/test/java/io/jboot/test/seata/interceptor/ExceptionInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..c594ee88debdd22e92e2ebf160fb0197316e79a3 --- /dev/null +++ b/src/test/java/io/jboot/test/seata/interceptor/ExceptionInterceptor.java @@ -0,0 +1,24 @@ +package io.jboot.test.seata.interceptor; + +import com.jfinal.aop.Interceptor; +import com.jfinal.aop.Invocation; +import com.jfinal.kit.StrKit; +import io.jboot.web.controller.JbootController; + +/** + * @program: jboot + * @description: ${description} + * @author: zxn + * @create: 2021-01-12 23:45 + **/ +public class ExceptionInterceptor implements Interceptor { + + @Override + public void intercept(Invocation invocation) { + invocation.invoke(); + JbootController jbootController = (JbootController) invocation.getController(); + if (StrKit.notBlank(jbootController.getPara("flag"))){ + int i = 1/0; + } + } +} diff --git a/src/test/java/io/jboot/test/seata/service/TccActionOneService.java b/src/test/java/io/jboot/test/seata/service/TccActionOneService.java new file mode 100644 index 0000000000000000000000000000000000000000..69be518bc33e68e2d807a0ba6964982b885b5030 --- /dev/null +++ b/src/test/java/io/jboot/test/seata/service/TccActionOneService.java @@ -0,0 +1,40 @@ +package io.jboot.test.seata.service; + +import io.seata.rm.tcc.api.BusinessActionContext; +import io.seata.rm.tcc.api.BusinessActionContextParameter; + +/** + * program: seata + * description: ${description} + * author: zxn + * create: 2020-07-25 22:29 + **/ +public interface TccActionOneService { + + /** + * Prepare boolean. + * + * @param actionContext the action context + * @param account + * @return the boolean + */ + + public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "account") String account, @BusinessActionContextParameter(paramName = "money") int money, + @BusinessActionContextParameter(paramName = "flag") boolean flag); + + /** + * Commit boolean. + * + * @param actionContext the action context + * @return the boolean + */ + public boolean commit(BusinessActionContext actionContext); + + /** + * Rollback boolean. + * + * @param actionContext the action context + * @return the boolean + */ + public boolean rollback(BusinessActionContext actionContext); +} diff --git a/src/test/java/io/jboot/test/seata/service/impl/TccActionOneServiceImpl.java b/src/test/java/io/jboot/test/seata/service/impl/TccActionOneServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..1225d2963bb9fd06f8c01465da46f93e1bf60314 --- /dev/null +++ b/src/test/java/io/jboot/test/seata/service/impl/TccActionOneServiceImpl.java @@ -0,0 +1,55 @@ +package io.jboot.test.seata.service.impl; + +import com.jfinal.aop.Inject; +import com.jfinal.kit.StrKit; +import io.jboot.aop.annotation.Bean; +import io.jboot.test.seata.account.IAccountService; +import io.jboot.test.seata.service.TccActionOneService; +import io.seata.rm.tcc.api.BusinessActionContext; +import io.seata.rm.tcc.api.BusinessActionContextParameter; +import io.seata.rm.tcc.api.TwoPhaseBusinessAction; + +/** + * program: seata + * description: ${description} + * author: zxn + * create: 2020-07-25 22:39 + **/ +@Bean +public class TccActionOneServiceImpl implements TccActionOneService { + + @Inject + private IAccountService accountService; + + @Override + @TwoPhaseBusinessAction(name = "TccActionOne" , commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true) + public boolean prepare(BusinessActionContext actionContext, String account,int money, boolean flag) { + System.out.println("actionContext获取Xid prepare>>> "+actionContext.getXid()); + System.out.println("actionContext获取TCC参数 prepare>>> "+actionContext.getActionContext("account")); + accountService.updateStore(account, money); + if (flag) { + throw new RuntimeException("you have fail"); + } + return true; + } + + @Override + public boolean commit(BusinessActionContext actionContext) { + System.out.println("actionContext获取TCC参数 commit >>> "+ actionContext.getActionContext("account") + ": " + + actionContext.getActionContext("money")); + String account = (String) actionContext.getActionContext("account"); + int money = (int) actionContext.getActionContext("money"); + accountService.update(account, money); + return true; + } + + @Override + public boolean rollback(BusinessActionContext actionContext) { + System.out.println("actionContext获取TCC参数 rollback >>> "+ actionContext.getActionContext("account") + ": " + + actionContext.getActionContext("money")); + String account = (String) actionContext.getActionContext("account"); + int money = (int) actionContext.getActionContext("money"); + accountService.updateRollbackStore(account, money); + return true; + } +} diff --git a/src/test/java/io/jboot/test/seata/starter/AccountApplicaiton.java b/src/test/java/io/jboot/test/seata/starter/AccountApplicaiton.java index a5ff01e51c8a887170739c114eb1c371ae38ab4b..0370ba94f57e9651c5e0ddbb75a4ccda6ee1e5da 100644 --- a/src/test/java/io/jboot/test/seata/starter/AccountApplicaiton.java +++ b/src/test/java/io/jboot/test/seata/starter/AccountApplicaiton.java @@ -19,13 +19,13 @@ public class AccountApplicaiton { JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); JbootApplication.setBootArg("jboot.seata.enable", true); - JbootApplication.setBootArg("jboot.seata.failureHandler", "com.alibaba.io.seata.tm.api.DefaultFailureHandlerImpl"); + JbootApplication.setBootArg("jboot.seata.failureHandler", "io.seata.tm.api.DefaultFailureHandlerImpl"); JbootApplication.setBootArg("jboot.seata.applicationId", "Dubbo_Seata_Account_Service"); JbootApplication.setBootArg("jboot.seata.txServiceGroup", "dubbo_seata_tx_group"); JbootApplication.setBootArg("jboot.datasource.type", "mysql"); - JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://192.168.0.100:3306/mini?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull"); + JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://127.0.0.1:3306/mini?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull"); JbootApplication.setBootArg("jboot.datasource.user", "root"); - JbootApplication.setBootArg("jboot.datasource.password", "123456"); + JbootApplication.setBootArg("jboot.datasource.password", "zxn123"); JbootApplication.setBootArg("jboot.model.unscanPackage", "*"); JbootApplication.setBootArg("jboot.model.scanPackage", "io.jboot.test.seata.commons"); diff --git a/src/test/java/io/jboot/test/seata/starter/StockApplication.java b/src/test/java/io/jboot/test/seata/starter/StockApplication.java index e54a8ec096a30a66d9525e71a322f4040123cfbd..788efbc7288fc9d82fb6a19de6543f2b7562255c 100644 --- a/src/test/java/io/jboot/test/seata/starter/StockApplication.java +++ b/src/test/java/io/jboot/test/seata/starter/StockApplication.java @@ -18,14 +18,14 @@ public class StockApplication { JbootApplication.setBootArg("jboot.rpc.autoExportEnable", true); JbootApplication.setBootArg("jboot.rpc.filter", "seata"); JbootApplication.setBootArg("jboot.seata.enable", true); - JbootApplication.setBootArg("jboot.seata.failureHandler", "com.alibaba.io.seata.tm.api.DefaultFailureHandlerImpl"); + JbootApplication.setBootArg("jboot.seata.failureHandler", "io.seata.tm.api.DefaultFailureHandlerImpl"); JbootApplication.setBootArg("jboot.seata.applicationId", "Dubbo_Seata_Stock_Service"); JbootApplication.setBootArg("jboot.seata.txServiceGroup", "dubbo_seata_tx_group"); JbootApplication.setBootArg("jboot.datasource.type", "mysql"); - JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://192.168.0.100:3306/mini?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull"); + JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://127.0.0.1:3306/mini?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull"); JbootApplication.setBootArg("jboot.datasource.user", "root"); - JbootApplication.setBootArg("jboot.datasource.password", "123456"); + JbootApplication.setBootArg("jboot.datasource.password", "zhang123"); JbootApplication.setBootArg("jboot.model.unscanPackage", "*"); JbootApplication.setBootArg("jboot.model.scanPackage", "io.jboot.test.seata.commons"); diff --git a/src/test/java/io/jboot/test/seata/starter/WebApplication.java b/src/test/java/io/jboot/test/seata/starter/WebApplication.java index ac1201df8847e142fe5691f7fd8cdfca49ceec09..fa16a14b40a0208a7832178f36b71f118befe105 100644 --- a/src/test/java/io/jboot/test/seata/starter/WebApplication.java +++ b/src/test/java/io/jboot/test/seata/starter/WebApplication.java @@ -1,8 +1,12 @@ package io.jboot.test.seata.starter; +import com.jfinal.aop.Before; import com.jfinal.aop.Inject; import io.jboot.app.JbootApplication; +import io.jboot.support.seata.annotation.SeataGlobalTransactional; import io.jboot.test.seata.business.BusinessServiceProvider; +import io.jboot.test.seata.interceptor.ExceptionInterceptor; +import io.jboot.test.seata.service.TccActionOneService; import io.jboot.web.controller.JbootController; import io.jboot.web.controller.annotation.RequestMapping; @@ -26,6 +30,13 @@ public class WebApplication extends JbootController { JbootApplication.setBootArg("jboot.seata.applicationId", "Dubbo_Seata_Business_Service"); JbootApplication.setBootArg("jboot.seata.txServiceGroup", "dubbo_seata_tx_group"); + JbootApplication.setBootArg("jboot.datasource.type", "mysql"); + JbootApplication.setBootArg("jboot.datasource.url", "jdbc:mysql://127.0.0.1:3306/mini?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull"); + JbootApplication.setBootArg("jboot.datasource.user", "root"); + JbootApplication.setBootArg("jboot.datasource.password", "zxn123"); + JbootApplication.setBootArg("jboot.model.unscanPackage", "*"); + JbootApplication.setBootArg("jboot.model.scanPackage", "io.jboot.test.seata.commons"); + JbootApplication.run(args); } @@ -33,6 +44,11 @@ public class WebApplication extends JbootController { @Inject private BusinessServiceProvider businessServiceProvider; + @Inject + private TccActionOneService tccActionOneService; + + @Before(ExceptionInterceptor.class) + @SeataGlobalTransactional(timeoutMills = 300000, name = "Dubbo_Seata_Business_Transactional") public void index() { System.out.println("WebApplication.index()"); @@ -40,4 +56,14 @@ public class WebApplication extends JbootController { businessServiceProvider.deposit(1); renderText("ok"); } + + @SeataGlobalTransactional(timeoutMills = 300000, name = "Seata_Business_Transactional_TccOne") + public void tccOne(){ + tccActionOneService.prepare(null, "Hobbit", 10, getParaToBoolean("flag")); + /*if (getParaToBoolean("flag")) { + throw new RuntimeException("you have fail"); + }*/ + renderJson("you are sucess"); + } + } diff --git a/src/test/java/io/jboot/test/seata/stock/StockServiceProvider.java b/src/test/java/io/jboot/test/seata/stock/StockServiceProvider.java index 6561c11360f00704c79e00a90b2cd6a9bf894939..3cd874db7455901ea4a26035ede204ef0c55f014 100644 --- a/src/test/java/io/jboot/test/seata/stock/StockServiceProvider.java +++ b/src/test/java/io/jboot/test/seata/stock/StockServiceProvider.java @@ -1,11 +1,13 @@ package io.jboot.test.seata.stock; +import io.jboot.aop.annotation.Bean; import io.jboot.components.rpc.annotation.RPCBean; import io.jboot.service.JbootServiceBase; import io.jboot.test.seata.commons.Stock; @RPCBean +@Bean public class StockServiceProvider extends JbootServiceBase implements IStockService { private static Stock dao = new Stock().dao(); diff --git a/src/test/java/io/jboot/test/sentinel/SentinelController.java b/src/test/java/io/jboot/test/sentinel/SentinelController.java index c6a3c9cf3dd67865d6dcf0b7c88a93bec0d42883..00a358cfcd44638313d79bd5b6958eb508055029 100644 --- a/src/test/java/io/jboot/test/sentinel/SentinelController.java +++ b/src/test/java/io/jboot/test/sentinel/SentinelController.java @@ -1,11 +1,11 @@ /** - * Copyright (c) 2016-2020, Michael Yang 杨福海 (fuhai999@gmail.com). + * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). *

- * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * 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.gnu.org/licenses/lgpl-3.0.txt + * 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, @@ -16,6 +16,7 @@ package io.jboot.test.sentinel; import com.alibaba.csp.sentinel.annotation.SentinelResource; +import com.alibaba.csp.sentinel.slots.block.BlockException; import io.jboot.web.controller.JbootController; import io.jboot.web.controller.annotation.RequestMapping; @@ -24,16 +25,22 @@ import io.jboot.web.controller.annotation.RequestMapping; * @Date: 2020/2/3 * * 使用方法: - * 第一步,启动 sentinel dashboard - * java -jar sentinel-dashboard-1.7.1.jar - * - * 第二步:在 resource/sentinel.properties 配置相关信息 + * http://jbootprojects.gitee.io/docs/docs/sentinel.html */ @RequestMapping("/sentinel") public class SentinelController extends JbootController { - @SentinelResource + @SentinelResource(blockHandler = "block") public void index(){ renderText("sentinel index..."); } + + + /** + * 注意:这个降级方法里的参数,必须是 SentinelResource 方法 index() 参数最后多出一个 BlockException 参数 + * @param ex + */ + public void block(BlockException ex){ + renderText("sentinel block"); + } } diff --git a/src/test/java/io/jboot/test/serializer/FastJsonSerializerTester.java b/src/test/java/io/jboot/test/serializer/FastJsonSerializerTester.java new file mode 100644 index 0000000000000000000000000000000000000000..016e754da072247811c7725492d3f4f2d94c5409 --- /dev/null +++ b/src/test/java/io/jboot/test/serializer/FastJsonSerializerTester.java @@ -0,0 +1,26 @@ +package io.jboot.test.serializer; + +import com.jfinal.captcha.Captcha; +import io.jboot.components.serializer.FastJsonSerializer; + +public class FastJsonSerializerTester { + + public static void main(String[] args) { + FastJsonSerializer fjs = new FastJsonSerializer(); + + TestObject object = new TestObject("aaaa10001",18); + byte[] bytes = fjs.serialize(object); + + TestObject dObj = (TestObject) fjs.deserialize(bytes); + + System.out.println(object); + + + Captcha captcha = new Captcha("testKey","testValue",1000); + byte[] cbytes = fjs.serialize(captcha); + + Captcha newCaptcha = (Captcha) fjs.deserialize(cbytes); + + System.out.println(captcha); + } +} diff --git a/src/test/java/io/jboot/test/serializer/TestObject.java b/src/test/java/io/jboot/test/serializer/TestObject.java new file mode 100644 index 0000000000000000000000000000000000000000..b0d40ec82b23c398c6c42f79b098540e145d4a6c --- /dev/null +++ b/src/test/java/io/jboot/test/serializer/TestObject.java @@ -0,0 +1,38 @@ +package io.jboot.test.serializer; + +public class TestObject { + private String id; + private int age; + + public TestObject() { + } + + public TestObject(String id, int age) { + this.id = id; + this.age = age; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public String toString() { + return "TestObject{" + + "id='" + id + '\'' + + ", age=" + age + + '}'; + } +} diff --git a/src/test/java/io/jboot/test/shiro/ShiroController.java b/src/test/java/io/jboot/test/shiro/ShiroController.java index 87292940f829b6af927df71a584ec4fb09800c1b..6375bcf408b3f40a3d9cb8472431d97ac30d3fe5 100644 --- a/src/test/java/io/jboot/test/shiro/ShiroController.java +++ b/src/test/java/io/jboot/test/shiro/ShiroController.java @@ -3,12 +3,16 @@ package io.jboot.test.shiro; import io.jboot.web.controller.JbootController; import io.jboot.web.controller.annotation.RequestMapping; +import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.apache.shiro.authz.annotation.RequiresGuest; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.RequiresRoles; import org.apache.shiro.subject.Subject; -@RequestMapping(value = "/shiro",viewPath = "/htmls/shiro") +@RequestMapping(value = "/shiro", viewPath = "/htmls/shiro") public class ShiroController extends JbootController { @@ -16,37 +20,56 @@ public class ShiroController extends JbootController { renderText("index"); } - public void login(){ + public void login() { renderText("login"); } - public void doLogin(){ - + public void doLogin() { Subject subject = SecurityUtils.getSubject(); - subject.login(new TestAuthenticationToken()); + // 默认为admin登陆 + UsernamePasswordToken token = new UsernamePasswordToken(getPara("username", "admin"), "123"); + subject.login(token); // subject.isAuthenticated(); // subject.isPermitted() renderText("logined success"); - } - public void logout(){ + public void logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); renderText("logouted success"); } @RequiresAuthentication - public void usercenter(){ + public void usercenter() { renderText("usercenter"); } @RequiresGuest - public void guest(){ + public void guest() { renderText("guest"); } + @RequiresRoles("editor") + public void editor() { + renderText("editor"); + } + + @RequiresRoles("admin") + public void admin() { + renderText("admin"); + } + + @RequiresPermissions("all:read") + public void readAll() { + renderText("all"); + } + + @RequiresPermissions("news:read") + public void readNews() { + renderText("news"); + } } diff --git a/src/test/java/io/jboot/test/shiro/ShiroSupportTest.java b/src/test/java/io/jboot/test/shiro/ShiroSupportTest.java new file mode 100644 index 0000000000000000000000000000000000000000..71127132e90a8d709fd63cd7671004df057068a8 --- /dev/null +++ b/src/test/java/io/jboot/test/shiro/ShiroSupportTest.java @@ -0,0 +1,79 @@ +package io.jboot.test.shiro; + +import io.jboot.components.http.JbootHttpRequest; +import io.jboot.components.http.JbootHttpResponse; +import io.jboot.test.base.JbootTestBase; +import io.jboot.utils.HttpUtil; +import io.jboot.utils.StrUtil; +import org.junit.Test; + + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + + +public class ShiroSupportTest extends JbootTestBase { + + + @Test + public void test() { + // 不需要登录的页面 + assertEquals("index", httpGet("/shiro")); + + // 未登陆,重定向到 /shiro/loing + assertEquals("login", httpGet("/shiro/usercenter")); + + // 使用 admin 账号登录 + String userCookie = loginWith("admin"); + + // 使用 cookie 访问 usercenter + assertEquals("usercenter", getWithCookie("/shiro/usercenter", userCookie)); + + // 访问 admin 相关的页面 + assertEquals("admin", getWithCookie("/shiro/admin", userCookie)); + assertEquals("all", getWithCookie("/shiro/readAll", userCookie)); + + // 访问 editor 页面受限 + assertTrue(getWithCookie("/shiro/editor", userCookie).contains("403")); + assertTrue(getWithCookie("/shiro/readNews", userCookie).contains("403")); + + // 退出登陆 + assertEquals("logouted success", getWithCookie("/shiro/logout", userCookie)); + + // 再次访问 usercenter,重定向到 login + assertEquals("login", getWithCookie("/shiro/usercenter", userCookie)); + + // 使用 editor 登陆 + userCookie = loginWith("editor"); + assertEquals("editor", getWithCookie("/shiro/editor", userCookie)); + assertEquals("news", getWithCookie("/shiro/readNews", userCookie)); + + assertTrue(getWithCookie("/shiro/admin", userCookie).contains("403")); + assertTrue(getWithCookie("/shiro/readAll", userCookie).contains("403")); + } + + /** + * 使用指定的cookie信息,访问url + */ + private String getWithCookie(String url, String cookie) { + JbootHttpRequest req = JbootHttpRequest.create(BASE_URL + url, null, JbootHttpRequest.METHOD_GET); + req.addHeader("Cookie", cookie); + JbootHttpResponse rsp = HttpUtil.handle(req); + return rsp.getContent(); + } + + /** + * 使用指定用户名登录,返回cookie信息 + */ + private String loginWith(String username) { + Map params = new HashMap<>(); + params.put("username", username); + JbootHttpRequest req = JbootHttpRequest.create(BASE_URL + "/shiro/doLogin", params, JbootHttpRequest.METHOD_GET); + + JbootHttpResponse rsp = HttpUtil.handle(req); + assertEquals("logined success", rsp.getContent()); + return StrUtil.join(rsp.getHeaders().get("Set-Cookie"), ";"); + } +} diff --git a/src/test/java/io/jboot/test/shiro/TestAuthenticationToken.java b/src/test/java/io/jboot/test/shiro/TestAuthenticationToken.java deleted file mode 100644 index e43081812a42bc90f3e457b627273be61542b1f0..0000000000000000000000000000000000000000 --- a/src/test/java/io/jboot/test/shiro/TestAuthenticationToken.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.jboot.test.shiro; - -import org.apache.shiro.authc.AuthenticationToken; - - -public class TestAuthenticationToken implements AuthenticationToken { - @Override - public Object getPrincipal() { - return "Principal"; - } - - @Override - public Object getCredentials() { - return "Credentials"; - } -} diff --git a/src/test/java/io/jboot/test/shiro/TestShiroReam.java b/src/test/java/io/jboot/test/shiro/TestShiroReam.java index a653c248c5c2f4ce26caa35be0979af9610cceab..d884903142d97448219227116f02b95632f46620 100644 --- a/src/test/java/io/jboot/test/shiro/TestShiroReam.java +++ b/src/test/java/io/jboot/test/shiro/TestShiroReam.java @@ -4,15 +4,19 @@ import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.realm.Realm; +import org.apache.shiro.subject.PrincipalCollection; -public class TestShiroReam implements Realm { +public class TestShiroReam extends AuthorizingRealm { @Override public String getName() { System.out.println("MyRealm.getName()"); - return null; + return "TestShiroReam"; } @Override @@ -22,9 +26,20 @@ public class TestShiroReam implements Realm { } @Override - public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { - System.out.println("MyRealm.getAuthenticationInfo" + token); - return new SimpleAuthenticationInfo("1", "2", "3"); + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { + return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName()); } + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { + String username = (String) principalCollection.getPrimaryPrincipal(); + SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); + info.addRole(username); // admin | editor + if ("admin".equalsIgnoreCase(username)) { + info.addStringPermission("all:read"); + } else if ("editor".equalsIgnoreCase(username)) { + info.addStringPermission("news:read"); + } + return info; + } } \ No newline at end of file diff --git a/src/test/java/io/jboot/test/utils/ClassUtilTest.java b/src/test/java/io/jboot/test/utils/ClassUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7cb50c4180ac2207c6cc2c0f6fbf3bfa1eff9a80 --- /dev/null +++ b/src/test/java/io/jboot/test/utils/ClassUtilTest.java @@ -0,0 +1,57 @@ +package io.jboot.test.utils; + +import org.junit.Test; + +public class ClassUtilTest { + + + public static class TestObject{ + private String a; + private String b; + + public TestObject() { + } + + public TestObject(String a) { + this.a = a; + } + + public TestObject(String a, String b) { + this.a = a; + this.b = b; + } + + @Override + public String toString() { + return "TestObject{" + + "a='" + a + '\'' + + ", b='" + b + '\'' + + '}'; + } + } + + + @Test + public void testNewInstance(){ +// TestObject testObject0 = ClassUtil.newInstance(TestObject.class); +// System.out.println(testObject0); +// +// TestObject testObject1 = ClassUtil.newInstance(TestObject.class,"a"); +// System.out.println(testObject1); +// +// TestObject testObject2 = ClassUtil.newInstance(TestObject.class,null); +// System.out.println(testObject2); +// +// TestObject testObject3 = ClassUtil.newInstance(TestObject.class,"aa","bbb"); +// System.out.println(testObject3); +// +// TestObject testObject4 = ClassUtil.newInstance(TestObject.class,null,"bbb"); +// System.out.println(testObject4); +// +// TestObject testObject5 = ClassUtil.newInstance(TestObject.class,null,"bbb","ccc"); +// System.out.println(testObject5); + } + + + +} diff --git a/src/test/java/io/jboot/test/utils/FileUtilTest.java b/src/test/java/io/jboot/test/utils/FileUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f1af92d9c9eaba69f01e2671b1e64f52819eb69f --- /dev/null +++ b/src/test/java/io/jboot/test/utils/FileUtilTest.java @@ -0,0 +1,13 @@ +package io.jboot.test.utils; + +import io.jboot.utils.FileUtil; +import org.junit.Test; + +public class FileUtilTest { + + @Test + public void testGetSuffix(){ + String suffix = FileUtil.getSuffix("aaa/bbb/ccc.jpg"); + System.out.println(suffix); + } +} diff --git a/src/test/java/io/jboot/test/utils/ReflectBean.java b/src/test/java/io/jboot/test/utils/ReflectBean.java new file mode 100644 index 0000000000000000000000000000000000000000..a6fb27ab2c7d1b31cf40050c10a0f60a7fe30419 --- /dev/null +++ b/src/test/java/io/jboot/test/utils/ReflectBean.java @@ -0,0 +1,37 @@ +package io.jboot.test.utils; + +public class ReflectBean { + + private static String someStaticValue = "staticvalue"; + private String id; + private int age; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + private void doSomeThing(){ + System.out.println("doSomeThing invoked!!"); + } + + @Override + public String toString() { + return "ReflectBean{" + + "id='" + id + '\'' + + ", age=" + age + + ", someStaticValue=" + someStaticValue + + '}'; + } +} diff --git a/src/test/java/io/jboot/test/utils/ReflectUtilTest.java b/src/test/java/io/jboot/test/utils/ReflectUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..828523a02c219099f9b03b9d039e2bf44840fedb --- /dev/null +++ b/src/test/java/io/jboot/test/utils/ReflectUtilTest.java @@ -0,0 +1,33 @@ +package io.jboot.test.utils; + +import io.jboot.utils.ReflectUtil; +import org.junit.Assert; +import org.junit.Test; + +public class ReflectUtilTest { + + + @Test + public void testSetFieldValue(){ + ReflectBean bean = new ReflectBean(); + bean.setAge(18); + bean.setId("myid"); + + Assert.assertEquals((int)ReflectUtil.getFieldValue(bean,"age"),18); + Assert.assertEquals(ReflectUtil.getFieldValue(bean,"id"),"myid"); + Assert.assertEquals(ReflectUtil.getStaticFieldValue(ReflectBean.class,"someStaticValue"),"staticvalue"); + + ReflectUtil.setFieldValue(bean,"id","123"); + ReflectUtil.setFieldValue(bean,"age",45); + Assert.assertEquals("123",bean.getId()); + Assert.assertEquals(45,bean.getAge()); + + ReflectUtil.invokeMethod(bean,"doSomeThing"); + + System.out.println(bean); + + ReflectUtil.setStaticFieldValue(ReflectBean.class,"someStaticValue","vvvv1111"); + + System.out.println(bean); + } +} diff --git a/src/test/java/io/jboot/test/validate/TestValideService.java b/src/test/java/io/jboot/test/validate/TestValideService.java new file mode 100644 index 0000000000000000000000000000000000000000..e22fda5eb69589344fe0396490cf5a750ad545e5 --- /dev/null +++ b/src/test/java/io/jboot/test/validate/TestValideService.java @@ -0,0 +1,10 @@ +package io.jboot.test.validate; + +import javax.validation.constraints.NotNull; + +public class TestValideService { + + public void calc(@NotNull Integer value){ + return; + } +} diff --git a/src/test/java/io/jboot/test/validate/ValidateController.java b/src/test/java/io/jboot/test/validate/ValidateController.java index 9b759be773f23145126d91cedbc5364eee81e63a..a78ee562867691fb180b8fc44802ff91a24e7f35 100644 --- a/src/test/java/io/jboot/test/validate/ValidateController.java +++ b/src/test/java/io/jboot/test/validate/ValidateController.java @@ -1,32 +1,97 @@ package io.jboot.test.validate; +import com.jfinal.aop.Aop; import com.jfinal.core.Controller; import io.jboot.web.controller.annotation.RequestMapping; -import io.jboot.web.validate.EmptyValidate; -import io.jboot.web.validate.Form; -import io.jboot.web.validate.UrlParaValidate; -import io.jboot.web.validate.ValidateRenderType; +import io.jboot.web.json.JsonBody; +import io.jboot.web.validate.*; + +import javax.validation.constraints.*; @RequestMapping("/validate") public class ValidateController extends Controller { - public void index(){ + public void index() { renderText("index"); } - @UrlParaValidate - public void test1(){ + + @EmptyValidate(value = @Form(name = "form"), renderType = ValidateRenderType.JSON) + public void test1() { renderText("test1"); } - @UrlParaValidate(renderType = ValidateRenderType.TEXT,message = "test2 was verification failed") - public void test2(){ + + @RegexValidate(value = @RegexForm(name = "email", regex = Regex.EMAIL, message = "请输入正确的邮箱地址")) + public void test2() { renderText("test2"); } - @EmptyValidate(value = @Form(name = "form"),renderType = ValidateRenderType.JSON) - public void test3(){ + @RegexValidate(value = @RegexForm(name = "email", regex = Regex.EMAIL)) + public void test3() { renderText("test3"); } + + + @RegexValidate(value = @RegexForm(name = "email", regex = Regex.EMAIL), renderType = ValidateRenderType.JSON) + public void test4() { + renderText("test4"); + } + + public void test5(@NotBlank String para1) { + renderText("test5"); + } + + public void test6(@NotNull String para1) { + renderText("test6"); + } + + public void test7(@Pattern(regexp = Regex.EMAIL) String para1) { + renderText("test7"); + } + + + public void test8(@Size(min = 100,max = 200) int para1) { + renderText("test8"); + } + + + public void test9(@Email String para1) { + renderText("test9"); + } + + + public void test10(@Max(value = 10,message = "错误的 para1") int para1) { + renderText("test10"); + } + + public void test11(@Min(10) int para1) { + renderText("test11"); + } + + public void test12(@Min(10) @JsonBody("aaa.bbb.age") int para1) { + renderText("test12"); + } + + public void test13(@Size(min = 10,max = 20) @JsonBody("aaa.bbb.age") int para1) { + renderText("test13"); + } + + public void test14(@Size(min = 10,max = 20) @JsonBody("aaa.bbb.name") String name) { + renderText("test14"); + } + + public void test15(@Digits(integer = 2,fraction = 3) @JsonBody("aaa.bbb.fff") float name) { + renderText("test15"); + } + + public void test16() { + Aop.get(TestValideService.class).calc(getInt("para")); + renderText("test16" ); + } + + public void test17(@DecimalMax("200") @DecimalMin("100") int age) { + renderText("test17" ); + } } diff --git a/src/test/java/io/jboot/test/xss/XSSController.java b/src/test/java/io/jboot/test/xss/XSSController.java new file mode 100644 index 0000000000000000000000000000000000000000..753a0f95bed5faebc401ae8175a56a825ce7374b --- /dev/null +++ b/src/test/java/io/jboot/test/xss/XSSController.java @@ -0,0 +1,24 @@ +package io.jboot.test.xss; + +import com.jfinal.core.Path; +import io.jboot.app.JbootApplication; +import io.jboot.web.controller.JbootController; + +@Path("/xss") +public class XSSController extends JbootController { + + public void index() { + System.out.println("------para:" + getPara("para")); + System.out.println("------paras:" + getParas().get("para")); + System.out.println("------getParaMap:" + getParaMap().get("para")[0]); + System.out.println("------originalPara:" + getOriginalPara("para")); + + renderText("ok"); + } + + public static void main(String[] args) { + JbootApplication.setBootArg("jboot.web.escapeParas", true); + JbootApplication.run(args); + } +} + diff --git a/src/test/resources/api-mock.json b/src/test/resources/api-mock.json new file mode 100644 index 0000000000000000000000000000000000000000..87b9fa3adb494056e38864d7771892942c2579eb --- /dev/null +++ b/src/test/resources/api-mock.json @@ -0,0 +1,10 @@ +{ + "ApiModel1": { + "key1": "aaa" + }, + + "ApiModel2": { + "key2": "aaa" + } + +} \ No newline at end of file diff --git a/src/test/resources/api-remarks.json b/src/test/resources/api-remarks.json new file mode 100644 index 0000000000000000000000000000000000000000..74abba4dc212b3ffd0111aba0c3cf452f9d3df38 --- /dev/null +++ b/src/test/resources/api-remarks.json @@ -0,0 +1,10 @@ +{ + "ApiModel1": { + "aaa": "字段说明1" + }, + + "ApiModel2": { + "aaa": "字段说明2" + } + +} \ No newline at end of file diff --git a/src/test/resources/enum.html b/src/test/resources/enum.html new file mode 100644 index 0000000000000000000000000000000000000000..021bcac268559d42e3f0f7934ffbefe04049bd26 --- /dev/null +++ b/src/test/resources/enum.html @@ -0,0 +1,34 @@ + + + + + enum + + + +test enum.html...
+ +1 isMaster -----> #(JfinalEnum.isMaster(1))
+2 isMaster-----> #(JfinalEnum.isMaster(2))
+ +1 isClient -----> #(JfinalEnum.isClient(1))
+20 isClient-----> #(JfinalEnum.isClient(20))
+ + + +1 isDev(1) -----> #(JfinalEnum.isDev(1))
+1 isDev(1,"1") -----> #(JfinalEnum.isDev(1,"1"))
+1 str() -----> #(JfinalEnum.str())
+ + + +


+#for(v : JfinalEnum.values()) + text-----> #(v.text)
+#end + + + + + + \ No newline at end of file diff --git a/src/test/resources/file.conf b/src/test/resources/file.conf index 0660bf818fa4d40ce3a63d49de3b58e32c7b4920..03888e60bff67edc6199a41a1ea4a9037a06a1ea 100644 --- a/src/test/resources/file.conf +++ b/src/test/resources/file.conf @@ -28,7 +28,7 @@ transport { } service { #vgroup->rgroup - vgroup_mapping.dubbo_seata_tx_group = "default" + vgroupMapping.dubbo_seata_tx_group ="default" #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support diff --git a/src/test/resources/jboot-product.properties b/src/test/resources/jboot-product.properties new file mode 100644 index 0000000000000000000000000000000000000000..0d39dd57a16efd004fd421c1d16d7ef1c2a037fe --- /dev/null +++ b/src/test/resources/jboot-product.properties @@ -0,0 +1,2 @@ +undertow.host=0.0.0.0 +undertow.port=8866 \ No newline at end of file diff --git a/src/test/resources/jboot.properties b/src/test/resources/jboot.properties index 4e21b773053c95addd4b3f3b9150258e330018dc..2c788a17714c3884a10a952e76bc5cc6aeb354d8 100644 --- a/src/test/resources/jboot.properties +++ b/src/test/resources/jboot.properties @@ -1,6 +1,7 @@ -undertow.host=127.0.0.1 +undertow.host=0.0.0.0 undertow.port=9999 undertow.resourcePath = src/test/resources +#undertow.devMode = false #undertow.hotSwapClassPrefix= #undertow.unHotSwapClassPrefix= @@ -10,7 +11,9 @@ jboot.rpc.autoExportEnable=false jboot.limit.enable = true -jboot.limit.rule = /*?a=b*c:tb:1,io.jboot.test.aop*.get*(*):tb:1 +#jboot.limit.rule = /*?a=b*c:tb:1,io.jboot.test.aop*.get*(*):tb:1 +jboot.limit.rule = /:iptb:1,io.jboot.test.aop*.get*(*):tb:1 +jboot.limit.ipWhitelist = 127.0.0.1 jboot.model.unscanPackage = * @@ -23,4 +26,42 @@ jboot.config.nacos.group = jboot jboot.config.apollo.enable = false jboot.config.apollo.appId = SampleApp -jboot.config.apollo.meta = http://106.54.227.205:8080 \ No newline at end of file +jboot.config.apollo.meta = http://106.54.227.205:8080 + + +config.test.test.name = name1 +config.test.test.type = type1 + +config.test.test.bbb.name = ${config.test.test.name}222 +config.test.test.bbb.type = type2 + +config.test.test.ccc.name = name3${config.test.test.bbb.name}--dd--${config.test.test.bbb.name}--xx${}--dd${a:123}${config.test.test.bbb.name} +config.test.test.ccc.type = type3 + + +# 配置 gateway ,当访问 /gateway 的时候,自动路由到 /gateway/render +#jboot.gateway.enable = true +#jboot.gateway.uri = http://127.0.0.1:9901/gateway/render,http://127.0.0.1:9902/gateway/render,http://127.0.0.1:9903/gateway/render +#jboot.gateway.interceptors = io.jboot.test.gateway.TestInterceptor +jboot.gateway.uriHealthCheckEnable = true +jboot.gateway.uriHealthCheckPath = / +jboot.gateway.pathEquals = /gateway +#jboot.gateway.proxyContentType = image/jpeg + + +jboot.rpc.dubbo.consumer.timeout = 55555 +jboot.rpc.dubbo.consumer.default = true + + + + +jboot.rpc.type = local + +jboot.web.jwt.secret = 123 + +jboot.sentinel.enable = false +#jboot.sentinel.datasource = redis +#jboot.sentinel.datasource.redis.host = 127.0.0.1 +#jboot.sentinel.datasource.redis.database = 1 +jboot.shiro.ini=shiro.ini +jboot.shiro.loginUrl=/shiro/login \ No newline at end of file diff --git a/src/test/resources/undertow.txt b/src/test/resources/undertow.txt index 5a07c80a5743d0fe5032f46384d3ba274daa498b..dc12b7ac9303a984fde992db052b2614f9cb1a42 100644 --- a/src/test/resources/undertow.txt +++ b/src/test/resources/undertow.txt @@ -1,3 +1,3 @@ -undertow.devMode = true -undertow.host = localhost -undertow.port = 8080 \ No newline at end of file +#undertow.devMode = true +#undertow.host = localhost +#undertow.port = 8080 \ No newline at end of file