-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 332 KB
/
content.json
1
{"meta":{"title":"答案","subtitle":"答案的个人博客","description":"答案的个人博客,记录学习成长之路。","author":"Volantis Team","url":"http://www.aquestian.cn","root":"/"},"pages":[{"title":"","date":"2022-12-11T03:04:07.978Z","updated":"2022-12-11T03:04:07.978Z","comments":true,"path":"404.html","permalink":"http://www.aquestian.cn/404","excerpt":"","text":"404 很抱歉,您访问的页面不存在 可能是输入地址有误或该地址已被删除"},{"title":"","date":"2022-12-11T03:04:07.984Z","updated":"2022-12-11T03:04:07.984Z","comments":true,"path":"about/index.html","permalink":"http://www.aquestian.cn/about/","excerpt":"","text":"菜逼博主 本来想着入了这行有点高级,收入也应该不菲,原来都是错觉! 入行到现在,钱鸡毛没赚下,b装的不少,人胖了不少,关键枸杞也没少买!还好头发尚在! 也欢迎各位程序媛小姐姐来骚扰,首页加入我们,直接加我好友就更好了。 言归正传,当然也希望自己在iT这一行业上,天天都能更上一层楼!!! 新的QQ群 901950146"},{"title":"如何正确地更新主题","date":"2022-12-11T03:04:07.994Z","updated":"2022-12-11T03:04:07.994Z","comments":true,"path":"how-to-update/index.html","permalink":"http://www.aquestian.cn/how-to-update/","excerpt":"","text":"如何正确地更新主题https://github.com/volantis-x/hexo-theme-volantis/issues/459 稳定版本如果您以内容创作为主,推荐使用稳定版本:npm i hexo-theme-volantis 更新时,把 package.json 中的版本号改为 * 再执行 npm i 就可以了。 如果您需要对主题的源文件进行修改,推荐 fork引用并修改自己 fork 的那份,当主题有更新时,合并到自己的分支。 如果您不 fork 而直接修改主题源码,是没办法获得更新的!Fork 篇本文以 GitKraken 软件的使用展开,相关链接:GitKraken: Free Git GUI Client - Windows, Mac, Linux 如果您按照主题文章中的 设置子模块 已经克隆了一份主题并添加到自己的博客仓库中,那么本篇文章将极大的帮助到您,如果您还没有如此操作,不妨尝试一番。这里是本文的仓库环境:博客仓库 Hexo-Blog 、主题仓库 volantis 。 一、GitKraken 的简单操作在 GitKraken 的软件界面中,位于正中间面积最大的区域是仓库的历史提交信息,右边为选中提交记录的详情,左边则放有一些仓库相关的信息,将目光集中到左边的 SUBMODULES 选项栏,如果您已经正常的将 Fork 的主题仓库添加到博客仓库中,您便可以在这里看到。展开 SUBMODULES 选项卡,右键并选择 Open this submodlue 打开子模块: 博客仓库 打开子模块 如此进入的仓库为您的主题仓库,可以在当前页面中查看到所有提交的历史记录等等。为了避免一些拗口的称呼所带来的不良影响,这里设定如下:将 Fork 的仓库称为 主题仓库 ,将 hexo-theme-volantis 仓库称为 volantis 仓库。 主题仓库 在图中,当前 Fork 的主题仓库所处的分支为 master-theme ,图中右侧展示的是个人主题仓库的最后一次提交信息。中间区域,较上部分在写有 master 标记的为 volantis 仓库的分支(您可以通过右侧的 Logo 图片进行区别)。显而易见的,当前主题仓库已经落后 Volantis 仓库,下面我们便需要合并代码到自己的主题仓库中。如果您打开后的界面并没有看到 Volantis 的仓库信息,意味着当前没有添加 Volantis 仓库为远端,您可以按照如下操作添加: 添加 Volantis 远端仓库信息 在左侧面板的 REMOTE 选项卡处,点击加号,进入如下图所示界面,选中 volantis-x/hexo-theme-volantis 后添加即可。 二、GitKraken 的合并操作1. Merge在 volantis 仓库的 master 分支处右键,选择 Merge volantis/master into xxxx,进行合并操作。至于为什么不选择变基(Rebase),个人认为保留仓库的提交历史比修改历史更好。通常,合并操作会自动完成,但是如若冲突时,会收到如此提醒:Merge Failed ,There are merge conflicts that need tobe resolved. 如它所说存在需要解决的冲突,此时右侧选项卡会展示 Merge conflicted detected 窗口,已解决的和冲突文件会显示在其中。 点击待解决冲突的窗口,在这个页面中,上半部分为本地和远端的代码,下半部分为合并后的内容。您可以根据实际情况,如回忆修改历史,选择是选中左边本地,还是右边远端,抑或是两边都选择,如果对选择后的结果不满意,您还可以手动修改 Output 窗口中的内容,当一切结束后,点击 Save 结束操作。(原则上您必须选择其中的一方,而不是直接修改 Output 的内容) 有时,可能遇到远端删除了某个文件,收到如下提示:GitKraken was unable to determine whether to keep source/css/_plugins/gitalkstyl, would you like to keep it? GitKraken 不会主动删除您的文件,不过一般情形下无需保留,Delete The File 即可。 最后,在解决完所有冲突文件后,回到仓库列表界面,点击 Commit and Merge 完成提交。 A. 合并操作 B. 合并冲突检测 C. 选择合适的内容 D. 提交内容 2. Rebase简言之,Rebase 将你的所有修改(提交)重新放到了公共分支的最后面,当然后果是可能会经常面临是否强制提交,而且不太适合与 Merge 操作共同使用。以下内容摘抄自:Rebase - 廖雪峰的官方网站 多人在同一个分支上协作时,很容易出现冲突。后 Push 的童鞋不得不先 Pull ,在本地合并,然后才能 Push 成功。 总之看上去很乱,有强迫症的童鞋会问:为什么 Git 的提交历史不能是一条干净的直线?其实是可以做到的!Git 有一种称为 Rebase 的操作,有人把它翻译成“变基”。 Rebase 操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。 Rebase 操作可以把本地未push的分叉提交历史整理成直线; Rebase 的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。 三、冲突的产生与避免冲突一般产生于同一处被不同人修改时,Git 无法自动处理,抛出错误让用户解决。由于主题目前仍处于青少年阶段,更新迭代速度比较快,冲突现象可能会比较明显,下面提供一些思路减少这类情况。 1.首先是配置文件,根据 Hexo 的规则,所有对配置的修改都可以独立出来,无需直接修改主题仓库下的 config.yml ,这里可以参阅:创建主题配置文件。配置类文件是最不该产生冲突的地方。 2.样式文件,根据 css 的覆盖规则,使用样式覆盖比直接修改样式来的欢快,例如主题中的光标便是采用的样式覆盖的思路。 四、代码历史维护您可以对单个文件进行历史查看操作,以此来对比您所做出的个人修改,最大程度上的避免代码丢失。正所谓熟能生巧,多加操作后主题更新将不再是一件麻烦的事情,末尾愿您一路走来,最终回归创建博客的初心,完结撒花 ★,°:.☆( ̄▽ ̄)/$:.°★ 。 历史记录"},{"title":"文章分类","date":"2022-12-11T03:04:07.984Z","updated":"2022-12-11T03:04:07.984Z","comments":true,"path":"blog/categories/index.html","permalink":"http://www.aquestian.cn/blog/categories/","excerpt":"","text":""},{"title":"","date":"2022-12-11T03:04:08.018Z","updated":"2022-12-11T03:04:08.018Z","comments":true,"path":"mylist/article/index.html","permalink":"http://www.aquestian.cn/mylist/article/","excerpt":"","text":""},{"title":"","date":"2022-12-11T03:04:08.019Z","updated":"2022-12-11T03:04:08.019Z","comments":true,"path":"mylist/codes/index.html","permalink":"http://www.aquestian.cn/mylist/codes/","excerpt":"","text":""},{"title":"所有标签","date":"2022-12-11T03:04:07.984Z","updated":"2022-12-11T03:04:07.984Z","comments":true,"path":"blog/tags/index.html","permalink":"http://www.aquestian.cn/blog/tags/","excerpt":"","text":""},{"title":"","date":"2022-12-11T03:04:08.019Z","updated":"2022-12-11T03:04:08.019Z","comments":true,"path":"mylist/life/index.html","permalink":"http://www.aquestian.cn/mylist/life/","excerpt":"","text":""}],"posts":[{"title":"我的程序人生——第六年","slug":"life/Life-Six-Year","date":"2024-01-29T16:00:00.000Z","updated":"2024-01-29T16:00:00.000Z","comments":true,"path":"life/Life-Six-Year/","link":"","permalink":"http://www.aquestian.cn/life/Life-Six-Year/","excerpt":"","text":"程序人生第六年承蒙公司信任,今年我开始独立负责项目了,也挂上了项目执行经理的名头。 三月初,我把项目执行计划做完在公司开了项目启动会,又做了详细设计报告交付甲方。这些东西虽然不是头回做了,但还是反反复复改了好几版,总算是应付过去了。三月中旬,我到了现场开始实施。第一件难事,甲方找的设计院的施工蓝图半个月都没有交稿,反而甩锅给我们交付的现场实际设计图标点位置不明确,没办法我只能找甲方项目交接人,也是信息中心部门的甄总组织一下会议,确定一下双方责任以及图纸交付时间。大家都明白的,求人办事,就得请人吃饭,这也是我头一次在商务上请人吃饭,两人600多,其中300多是额外点餐打包带回家的,不过花钱真能办事。期间还发生一件有意思的事儿,甲方组织了一个故障分析会,眼跟前了才通知我会议地点,甄总迟到了,结果会后特地给我来电话给我臭骂一顿说我没通知他害他在领导面前迟到…… 4月初,图纸总算是出来了,第二个难事也来了,搬货!我们的货是寄给甲方项目交接人甄总的。其实呢,是施工队顺手的事,但甄总要求必须得有施工队和使用部门签一份验收单,目的是为了责任划分,此话一出,施工队和使用部门都退避三舍,谁都不想担责,设备损坏作为项目负责人是可以对责任人进行索赔的。就这样拖了一周,施工队天天催我什么时候可以拉货,但使用部门又迟迟不肯签字。项目陷入停滞,我作为负责人也只能干着急,最后我们领导给我分析说现在的矛盾点是甲方企业内部矛盾,他说搬货咱就去人配合,不搬就拖着,看谁着急,但要确定一下搬货日期,并通知他们项目实施计划的实施节点,延期跟咱们没关系。于是,我悟了,再和甄总确认好搬货日期后我就通知施工队准时到仓库搬货。釜底抽薪呗,搬货卡车到院里停着,没人签字就等着,施工队也发话了,就来这一次后续自己找人搬货去。终于等了一个晌午,使用部门领导珊珊来迟,这收货单签了字!这段时间我跟甄总关系还不错,我请他吃饭送他汾酒,请他打羽毛球,给他报销油费,甚至请他儿子看电影,最过分的是我花钱租的车转头被他借跑了,真是无利不起早,苍蝇肉再小也不放过!反过来我跟他打羽毛球手机还搁旁边放着,不知道被哪个臭小子把后壳敲碎了。说到这我又想起一件特别无语的事儿,他儿子在球馆打球看到有个球拍没人管,臭小子直接顺回家了,这臭小子还把他同学叫过来一起打球,他跟他爹一队,他两同学一队,我特么出钱坐场下喊加油!真是难伺候,只能用狗篮子来形容我,我快卑微到尘埃里了,得跟他处好关系他才能帮我推进项目呀!整个4月好就好在,两个点中的港口点开始施工,虽然施工磨磨唧唧,10的天的活楞干到月底,其中我也主动包揽一部分接线的活儿。 我明白广东人为啥发展好了,真他么扣啊!我多说几个事儿就明白了,比如说还是这个甄总,他请他们单位部门吃饭,美其名曰是给我们公司铺路,打通关系了后续可以拿项目,我就在当地却不带上我,吃完了给我丢过来总计3600的发票!再比如,我和仪控部的开会,蔡总就说了你们五岳和保运的也是时候团建了哈,完事他就张罗把仪控的人也叫上也来舔着个大脸蹭我们团建聚餐!再比如,保运的一个臭小子吴工,他可是我的下属,某天中午我寻思一块吃个午饭,随便吃口,这臭小子开车居然把我带到了西餐厅,大言不惭的说我这段时间估计吃不惯,吃饭贵就不说啥了,臭小子居然还点咖啡喝。我中途问他这地方适合带你对象来啊带我算怎么个事,臭小子也是毫不避讳说他是经常带着对象来,所以感觉味道不错你肯定能接受……我没打他真是证明我素质高,只能硬着头皮请呗!还有个混蛋专门给我打电话说我说话口气有点大,我寻思我天天好爱好喝供着你们哪得罪人了?没想这家伙说我嘴里有异味让我买他的调理中药茶包,我说我天天刷牙不需要,下次再见我口气太重我躲你远点就行了!我在广东半年啊,三人请回过我,蔡班长,罗工,杨工,真没法表达这种操蛋玩意,逮住机会就往死里耗羊毛! 到了5月份,第三件难事,由于4月份石化企业出现安全事故,突然要求所有施工作业暂停先进行安全培训,这就导致我们单位同事到场之后迟迟不能进入厂区干活儿,整整半个月,只干了些零碎的小事,反而时间全耗费到培训跑手续签字上!第四件难事,因为一个点已经施工完成了,可网络ip甄总却一直拖着不给我,为此我和计量部门负责人陈总打了一个配合,在项目大群里陈总问工期进度,我回答网络ip甄总还没申请下来,陈总于是直接艾特甄总抓紧落实!虽然这样确实起到了催促效果,但把甄总惹恼了!直接给发消息炮轰我——ip没下来不能私信跟我说吗?我说我已经在钉钉私信给您发了,他说我没看到消息……但打羽毛球定场地的消息他是一条没错过!整个五月份是彻底陷入停滞了,但期间公司冷哥和任总给我上了一课,项目为啥停滞?除了安全培训,甄总为啥一直使绊子?项目背景得了解清楚,另外关键客户图啥?要么图钱,要么图权,要么就是啥也不图怕担责。了解过后,确实是我们单位回款没回甄总手里,我让我们领导也催公司抓紧给人汇款! 6月份,第五件难事,因为前期各种事情,导致现在就得压缩工期,就得加班加点的干,我也是服了我们公司柜机设计,接线、拆卸超级麻烦,设备也是三天两头烧个零件,而且还有外部车辆借磅业务。我一个人这么长时间也开始情绪崩溃,中间我一度想放弃了。好在经过不懈努力,6月底总算是投入使用了。 7月我终于回去休息一个月,8月又来甲方这里办上线手续。本来半个月,好死不死,百年难遇,地磅被雷击了,只能被强留着配合修复,最讽刺的是,我六月份因为没按规定着装考核我们公司1000,而反过来因为不可抗因素导致地磅损坏,我们加班加点维修才奖励200块。 9月份回了天津之后,是一个项目空窗期,狠狠歇了一把,期间我们做了项目总结会,会上我也深刻反思了自己的问题。一直到年底,我才跟着我领导但云南开启另外一个项目。这个项目的原型设计,详细设计,数据库设计,都出自我手(领导也指点迷津了),也确实是具备一定的项目管理能力了,我自己都明显感知自己进步了,这值得骄傲,但这份骄傲或者说自负,也导致了我跟领导在言语上的冲突。 云南这个项目,唯一问题就是工期紧只有一个月,到了现场时间进一步压缩为二十天,我们一行三人,都在高强度的工作,发生了好几回争吵。因为公司要实行什么积分制,每个人工资要从中划两千出来,一分10块,如果这个月积分为0,那么这两千没了,超过200分则多给。我强烈反对,话敢话吵架,直接把我从云南现场直接撤回天津了。 一直以来我挺感谢我们公司的,公司也一直很信任我,但这次我跟领导吵架,终归是希望我个人和公司之间有一个良性平衡的关系,达成一种默契,好的时候多给些,不好的时候公司也需要尽力拖底。我付出我的幸苦劳作,公司兑现给我的相应报酬。同享福,大家一起赚钱!不共患难,不代表我不与公司度过难光关,但也不能是公司反过来绑架我,要求我付出劳动的同时还要求我经济利益作出让步! 某天下班拼车回宿舍,后座的两个刚毕业的男生讨论着工资的事儿,跟他们年纪相仿的同事做开发一个月个税缴纳三百,而他们搞测试想找领导涨工资却始终犹豫不决,我很想扭头告诉他们方向选错了,在天津测试岗是有瓶颈的。经过这几年的成长,我的方向是正确的,从开发中慢慢转变为业务,这路我觉得是越走越远宽了。我中途面试过别的公司,面试官问了我一个问题直接让我陷入了沉思——你做生产系统这么久了,突然换个领域,这种业务上和思维上的变化,你怎么快速切换或者适应!是的,从头探索一个行业,了解它的模式也是需要长时间吸纳的。因此,28岁的我临近年关,又站在十字路口,面临抉择,但开发转业务这条路,应该坚定不移!","categories":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/categories/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"tags":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/tags/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"author":"aqian666"},{"title":"对于Oracle,MySQL,SQL Server重复数据去重,只保留一条数据。","slug":"blogs/SQL-De-Duplicate","date":"2023-09-18T16:00:00.000Z","updated":"2023-09-18T16:00:00.000Z","comments":true,"path":"blogs/SQL-De-Duplicate/","link":"","permalink":"http://www.aquestian.cn/blogs/SQL-De-Duplicate/","excerpt":"","text":"问题前提之前做过数据入湖,建表的时候匆忙,没有做主键,导致入湖出现了重复数据。举个例子: id name age sex 1 张三 23 男 1 张三 23 男 1 张三 23 男 2 李四 23 男 存在了如上两条及两条数据,目的是要去除重复数据,只保留一条,从而设置id为主键。 OracleOracle如果存在重复数据,id设置主键时,会有02437报错。 对于Oracle去处重复数据是最简单的,每行自带rowid。 DELETE FROM user WHERE id IN ( SELECT id FROM user GROUP BY id HAVING count( id ) > 1 ) AND rowid NOT IN ( SELECT min( rowid ) FROM user GROUP BY id HAVING count( id )> 1) 执行如上语句即可删除重复数据。 因为本地没有Oracle数据库,就不做演示了。 MySQLMySQL没有rowid,那么MySQL解决办法只有一种,把A表的数据去重添加到B表中,在B表中设置id为主键,最后把B表重命名为A表。 INSERT INTO user1 ( SELECT DISTINCT * FROM user ); 暂不清楚,数据量大的情况下会不会出现崩溃,可以通过limit截取。 如果不确定A表的数据是否全部添加到B表,可以添加完成后,执行 DELETE FROM user WHERE user.id IN (SELECT user1.id FROM user1) 这种方式当然也适用于其他数据库。 当然MySQL还有另外一种方式,就是新增一个字段为自增字段且不为null,让其自动填充,类似充当Orcal中的rowid。 填充完成后。 DELETE FROM user WHERE user.rowid NOT IN ( SELECT dt.minid FROM ( SELECT MIN( user.rowid ) AS minid FROM user GROUP BY name ) dt ) 有多种方式,可参考【mysql】mysql删除重复记录并且只保留一条_mysql删除完全重复数据只保留一条_千g的博客-CSDN博客 SQL ServerSQL Server 和MySQL逻辑是一样的,但语法上稍有变化 SELECT DISTINCT * INTO [dbo].[user1] FROM [dbo].[user] SQL Server是不需要创建user1表的,会自动创建,数据导入到新表后再设置主键即可。 另外一种设置自增rowid,执行: DELETE FROM [dbo].[user] WHERE [dbo].[user].rowid NOT IN ( SELECT dt.minid FROM ( SELECT MIN( [dbo].[user].rowid ) AS minid FROM [dbo].[user] GROUP BY name ) dt ) 后续后续研究其他数据库,mongo等其他用到的数据库再做更新。","categories":[{"name":"数据处理","slug":"数据处理","permalink":"http://www.aquestian.cn/categories/%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86/"}],"tags":[{"name":"MySQL","slug":"mysql","permalink":"http://www.aquestian.cn/tags/mysql/"},{"name":"Oracle","slug":"oracle","permalink":"http://www.aquestian.cn/tags/oracle/"},{"name":"SQL Server","slug":"sql-server","permalink":"http://www.aquestian.cn/tags/sql-server/"}],"author":"aqian666"},{"title":"springboot出入库管理系统源码分享","slug":"code/Code-Wms","date":"2023-09-15T16:00:00.000Z","updated":"2023-09-15T16:00:00.000Z","comments":true,"path":"code/Code-Wms/","link":"","permalink":"http://www.aquestian.cn/code/Code-Wms/","excerpt":"","text":"项目描述springboot出入库管理系统源码分享,前端使用vue-element,后端使用springboot+mybatis-plus+redis+JWT。 运行环境jdk8+tomcat8+mysql5.7+IntelliJ IDEA+maven 项目技术spring boot+mybatis-plus+vue-element+redis+JWT 项目截图 运行截图左侧菜单数据库可配置 首页 首页 管理员管理 管理员管理 用户管理 只有出库,入库权限 用户管理 仓库管理 仓库管理 类别管理 类别管理 物品管理 出入库操作 物品管理 记录管理 记录管理 线上演示地址http://43.138.127.183/# 其它说明白嫖党 请绕道","categories":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/categories/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"tags":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/tags/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"author":"aqian666"},{"title":"springboot医疗管理系统系统","slug":"code/Code-Hospital","date":"2023-09-12T16:00:00.000Z","updated":"2020-09-12T16:00:00.000Z","comments":true,"path":"code/Code-Hospital/","link":"","permalink":"http://www.aquestian.cn/code/Code-Hospital/","excerpt":"","text":"写在前面本项目是基于Spring Boot 2.x 开发的医疗管理系统系统。本项目也可以当作毕业设计,期末课程作业等,也可以当作学习、进阶Spring Boot 的资料。 功能描述本项目主要包含以下功能模块 系统管理 医生管理 患者管理 药品管理 管理员管理 预约管理 病史管理 住院信息管理 登录/注销功能 ... 开发环境(运行环境) 系统环境:Windows 11 开发工具:IntelliJ IDEA 2023.1.2 Java版本:JDK 1.8 Mysql版本:8.0 Maven版本:3.6.3 项目技术栈 Spring Boot 2.1.4.RELEASE Mybatis Maven 3.X Mysql layui Jquery freemarker ... 登录地址项目访问路径:http://localhost:8088 管理员 用户名 / 密码 admin1/ 123456 项目截图 项目演示视频链接: https://pan.baidu.com/s/1w0wkTAupDs4_qi0Wn3zRag 提取码: mhib 联系我们如有需要源码可以通过QQ 搜索:289373410联系我! 请备注:医疗管理系统 注意事项获取代码之后,使用IDEA导入本项目前,请确保你本地环境是已经含有代码所需要运行环境的条件了。 接着找到对应的sql文件,将其导入到你本地的数据库即可。 最后修改项目中配置文件中的数据库对应的信息,确认修改完毕,找到对应的Application直接运行吧! 其它说明白嫖怪,伸手党 请绕道!!! The end.","categories":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/categories/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"tags":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/tags/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"author":"aqian666"},{"title":"利用kettle实现数据库之间的数据同步","slug":"blogs/Kettle-Data-Transmission","date":"2023-05-02T16:00:00.000Z","updated":"2023-05-02T16:00:00.000Z","comments":true,"path":"blogs/Kettle-Data-Transmission/","link":"https://blog.csdn.net/dream_girl5/article/details/120782397","permalink":"http://www.aquestian.cn/blogs/Kettle-Data-Transmission/","excerpt":"","text":"利用kettle实现数据库之间的数据同步如果你需要做不同数据库之间的数据迁移或者抽取部分数据到另一个数据库,并实现定时数据同步(非实时),那么kettle是一个很好的选择。 以mysql数据库—> oracle数据库的定时同步作为案例: 工具安装和环境准备安装mysql数据库和oracle数据库下载pdi-ce-8.2.0.0-342打开目录下data-integration\\Spoon.bat ,即可打开keetle软件 清楚数据抽取需求抽取数据的mysql数据库表和oracle的表一致,单表对单表抽取根据oracle需求,在mysql写sql,再抽取相应数据到Oracle。是否定时。全量抽取 or 增量抽取。案例需求:根据需求在mysql写sql,增量抽取其数据到Oracle,实现按天定时抽取。 备注:(按月增量),查询当前月份为全量抽取,非当前月份数据增量抽取。例如:今天是10月15日,到月底之前每天全量抽取当月数据,每天更新10月份的数据;11月1号开始,10月份数据不动,只是每天全量抽取更新11月份的数据。所以当月是全量抽取,但增量是基于前一个月的基础上。 首先在mysql数据库 根据需求编写sql。 新建转换主对象树-转换-右键新建 主对象树-DB连接-右键新建 连接oracle数据库: 连接mysql数据库: 核心对象—输入—表输入-拖入界面即可 双击打开表输入 核心对象—输出—插入/更新—拖入界面即可 双击打开表插入/更新 核心对象—>转换—>字段选择—拖入界面即可按住shift连接三者 双击打开字段选择(点击元数据—获取改变的字段即可) 注意Encoding设置为UTF-8,否则抽取的数据会乱码 核心对象—>脚本—>执行sql脚本—拖入界面即可 双击打开执行sql脚本由于我们需要按月做增量抽取,本月数据做全量抽取。所以在抽本月数据之前要先删除oracle库目标表中之前抽取的本月数据。 点击运行此转换—即可完成一次数据抽取 新建作业主对象树-作业-右键新建 核心对象—Start/作业/成功—拖入界面并连接 双击打开作业浏览—选择上一步的转换文件目录 双击打开Start设置按天 定时抽取(重复) 点击run 定时抽取数据到oracle,即可完成","categories":[{"name":"数据处理","slug":"数据处理","permalink":"http://www.aquestian.cn/categories/%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86/"}],"tags":[{"name":"kettle","slug":"kettle","permalink":"http://www.aquestian.cn/tags/kettle/"}],"author":"aqian666"},{"title":"netty websocket ssl Received fatal alert:certificate_unknown","slug":"blogs/Netty-WebSocket","date":"2023-03-01T16:00:00.000Z","updated":"2023-03-01T16:00:00.000Z","comments":true,"path":"blogs/Netty-WebSocket/","link":"","permalink":"http://www.aquestian.cn/blogs/Netty-WebSocket/","excerpt":"","text":"自签证书win+r cmd 生成自己jks文件,指向自己要生成jks的文件位置下,我直接生成到项目resources下 #换成自己的本地ipkeytool -genkey -alias server -keyalg RSA -validity 3650 -keystore D:\\code\\my_code\\netty-demo-m\\src\\main\\resources\\mystore.jks -ext san=ip:192.168.3.7,ip:127.0.0.1,dns:localhost -storepass 1234567 keytool -list -keystore mystore.jks -v //查看信息 生成证书 keytool -alias server -exportcert -keystore D:\\code\\my_code\\netty-demo-m\\src\\main\\resources\\mystore.jks -file D:\\code\\my_code\\netty-demo-m\\src\\main\\resources\\mystore.cer -storepass 1234567 成功生成证书 项目运行将jks文件考入项目resources下 yaml配置:server: port: 8080 ssl: key-store: classpath:mystore.jks key-store-password: 1234567 key-store-type: JKS# key-alias: server enabled: true netty证书加载这里我就只上关键代码了 @Overrideprotected void initChannel(SocketChannel ch) throws Exception { if (openssl){ //true SSLEngine sslEngine = getServerSslContext().createSSLEngine(); sslEngine.setNeedClientAuth(false); sslEngine.setUseClientMode(false); SslHandler sslHandler = new SslHandler(sslEngine); ch.pipeline().addLast(sslHandler); } ch.pipeline().addLast("http-codec", new HttpServerCodec()); // HTTP编码解码器 ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536)); // 把HTTP头、HTTP体拼成完整的HTTP请求 ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler()); // 方便大文件传输,不过实质上都是短的文本数据 ch.pipeline().addLast("websocket-handler",webSocketServerHandler); ch.pipeline().addLast("http-handler",websocketNettyRequestHandler);} public SSLContext getServerSslContext() throws Exception { DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); org.springframework.core.io.Resource resource = resourceLoader.getResource("classpath:mystore.jks"); InputStream inputStream = resource.getInputStream(); log.info("加载了密码: {}", sslPassword); char[] passArray = sslPassword.toCharArray(); SSLContext sslContext = SSLContext.getInstance("SSLv3"); KeyStore ks = KeyStore.getInstance("JKS"); //加载keytool 生成的文件 ks.load(inputStream, passArray); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, passArray); sslContext.init(kmf.getKeyManagers(), null, null); inputStream.close(); return sslContext;} 不添加信任netty websocket ssl Received fatal alert: certificate_unknown。 错误原因这并不是程序的问题,这是证书本身的问题,主机并不承认这个证书导致的。 对证书添加信任打开证书 计算机添加信任 点击安装证书 - 选择本地计算机 - 将所有的证书都放入下列存储 - 受信任的根证书颁发机构 添加完成后就不会有不信任了。 导入证书到信任库中 keytool -import -alias server -keystore mycacert -file D:\\code\\my_code\\netty-demo-m\\src\\main\\resources\\mystore.cer -storepass changeit 在java bin 目录下会出现 再次运行 注意值得一提的是,自签的证书有且之能在本机使用,如将A机生成的证书拷贝B机使用也会出现同样的错误。","categories":[{"name":"Netty","slug":"netty","permalink":"http://www.aquestian.cn/categories/netty/"}],"tags":[{"name":"Netty","slug":"netty","permalink":"http://www.aquestian.cn/tags/netty/"},{"name":"WebSocket","slug":"websocket","permalink":"http://www.aquestian.cn/tags/websocket/"}],"author":"aqian666"},{"title":"我的程序人生——第五年","slug":"life/Life-Five-Year","date":"2022-12-30T16:00:00.000Z","updated":"2022-12-30T16:00:00.000Z","comments":true,"path":"life/Life-Five-Year/","link":"","permalink":"http://www.aquestian.cn/life/Life-Five-Year/","excerpt":"","text":"程序人生第五年 我一直很喜欢《我的团长我的团》这部影视作品,几乎每年都会重温一到两遍,故事尤为精彩的一段就是虞师三堂会审团长的这一段。开头团长讲述着自己的生平从北颠沛流离,一直到禅达。团长说他从戎以来,是从败仗中学会的打战。团长说他只是想让事情有他本来该有的样子。团长还说他从戎前,招魂的生意却是个好营生,除此之外好像也没有什么可以做的了。看似油嘴滑舌,胡搅蛮缠的表演,正如他们如尘土般轻薄。他有什么罪?烦啦说他真正的带领他们抗击日寇冲锋陷阵;兽医说他不知天命;马大志大喊冤枉;不辣说中华要灭亡湖南人先死绝;迷龙说有些瘪犊子给他安了莫须有的罪名;阿译说要向他一样犯下这样的“罪”。是的,虽然你我生于和平的年底,但在现实的碎片里,折射出的星星点点,还是过去的模样。大厦崩塌,随波逐流成了唯一的选择。我尊敬邓小平同志,更崇拜毛主席,我们慢慢老去,他们就更加光芒万丈。 利用出差之便,我带着他乡遇故知的情怀去了这三座城市——郑州,杭州,深圳。中原绿城的中庸之道,江南月色断桥残血般的惆怅,改革开放时不我待的世界之窗。郑州——我见到了我高中的大哥,他带我尝了河南特色,有一说一,烩面我确实不太喜欢,胡辣汤配油条那是一绝。赶得巧啊,大玉米没黑灯 :) 。灯火通明的城市里不难看出郑州想要彰显出传统的华夏文化,这种文化的底蕴是流淌的黄河,古往今来的血脉传承。杭州——我的另一个老哥,我到了已经晚上了,虽然时间晚但不能耽误我们吃西湖醋鱼和东坡肉啊!吃完饭,趁着朦朦夜色,西湖边上溜溜食儿吧。上有天堂下有苏杭不是没有道理的,日益月薪的城市变化依旧保留着一抹天青色。深圳——另外俩哥们,我们并没有找到能彰显本地特色的美食 :( 。我特地去了莲花山公园去看了尊敬的邓小平同志。我还没有去过上海,但深圳就是一座与时俱进,包罗万象的城市,最能体现邓小平同志思想格局的城市。 在北海的石化疫情爆发了,铁栅栏围满了整个村落,人们哄抢超市的各种吃食,俩天一次核酸检测。开始的时候还好,但过了三天后,事情变得不简单了,餐馆全部歇业了,持续的高温天气超市买回来的东西变质了,政府送来的物资也是越来越少了,我只能找饭店老板每天蹭一顿饭。是的,我都躺在睡觉,如果能睡着的话,睡着会让我忘记饥饿,或者说饿感来的慢一些。最让我气愤的就是我的通行证明,半个月办不下来。 8月1号,我给政府打电话办通行手续。 “8月5号就会解封,届时就封控半个月了就解封了。” 8月5号没有解封。 8月6号我又继续拨通了政府的电话。 政府登记了我的信息,让我等消息。 “8号政府说我的核酸过期了办不了。” 我7号的核酸中午1点,我的电话在8号下午4点,电话那丫头片子跟我卡bug。 8月8号我再申请,手续不受理,我一气之下通过各种渠道举报了村主任,镇政府,区政府。 8月9号在举报的加持下,政府有了回应,是镇长亲自来电,并要安排通行车辆。 8月10号我又又提交申请。 “没有提供随行通行车辆司机的信息,办不了。” 8月11号我又通过各种渠道联系到一个车队司机。并且我们单位领导把公司担保手续通知到政府。 8月12号,核酸结果一出,携带其他相关手续我一并给到政府,继续申请。 8月12号晚上,屋外滂沱大雨,终于在晚上9点时候,通行证办理成功了。令人无语的是,通行证仅限今天使用,晚上9点,外边下大雨,就三小时我怎么走!我又又又联系了政府。 “不能保证给你办俩天的通行证!” 我紧急联系了司机,司机也怕下雨路上危险不愿意来。软磨硬泡之下,于晚上11点,通行证变为限12,13号使用。 13号一早我预约了上午10点的车,为了避免出村大门时核酸过期,我特地7点趟着到膝盖的雨水到大门口问了。一会我核酸8点过期,我9点出门是否可以,如果不可以那我现在就走。 政府说没事一会过来就行,先登记了。 9点再到大门口,哼!我真是信了邪了,还是以核酸过期一小时不然出门,又卡bug。上午10点,在门口群众怨声载道中可算是脱离这个鬼村子。 这一年,如空中飞鸟,美丽自由;地上蝼蚁,不值一提;水中游鱼,随波逐流。","categories":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/categories/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"tags":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/tags/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"author":"aqian666"},{"title":"mongodb索引及运用。","slug":"blogs/Docker-mongoDBIndexes","date":"2022-08-22T16:00:00.000Z","updated":"2022-08-22T16:00:00.000Z","comments":true,"path":"blogs/Docker-mongoDBIndexes/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-mongoDBIndexes/","excerpt":"","text":"查看索引# getIndexes() 查看集合的所有索引 db.col.getIndexes() # getIndexKeys() 查看集合中的所有索引键 db.col.getIndexKeys() # totalIndexSize() 查看集合中索引的总大小 db.col.totalIndexSize() # getIndexSpecs() 查看集合各索引的详细信息 db.col.getIndexSpecs() 创建索引(mongo3.0以上版本) createIndex() 创建索引 db.col.createIndex(keys, options) col 为你自己的集合名 Key 值为你要创建的索引字段,1 为指定按升序创建索引,-1 按降序来创建索引。 createIndex() 接收可选参数,可选参数列表如下: Parameter Type Description background Boolean 建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加"background" 可选参数。 “background” 默认值为false。 unique Boolean 建立的索引是否唯一。指定为true创建唯一索引。默认值为false。 name string 索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。 dropDup Boolean 3.0+版本已废弃。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false. sparse Boolean 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档,默认值为 false。 expireAfterSeconds integer 指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。 v index version 索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。 weights document 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。 default_language string 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语。 language_override string 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language。 实例 创建一个普通索引,key表示字段名,1表示升序排序,-1表示降序。 # 创建一个普通索引,key表示字段名,1表示升序排序,-1表示降序。 db.col.createIndex({"name":1}) # 设置使用多个字段创建聚合索引(关系型数据库中称作复合索引) db.col.createIndex({"name":1,"age":-1}) # 在后台创建索引,通过在创建索引时加 background:true 的选项,让创建工作在后台执行 db.col.createIndex({"name":1,"age":-1}, {background: true}) 在后台创建索引的原因: 在前台创建索引期间会锁定数据库,会导致其它操作无法进行数据读写,在后台创建索引,会定期释放写锁,从而保证其它操作的运行,但是后台操作会在耗时更长,尤其是在频繁进行写入的服务器上。 所以创建索引应该注意以下几点:(MongoDB:创建索引需要注意的事项 - 简书) 数据前把索引创建好 如果已有数据在,要在后台创建索引 创建索引最好创建索引名称 复杂索引创建复合索引在上边创建索引已经提到。 多键索引为了索引保存数组值的字段,MongoDB为数组中的每个元素创建一个索引键。这些多键索引支持对数组字段的有效查询。可以在既包含标量值[1](例如字符串,数字)又包含嵌套文档的数组上构造多键索引。 #多键索引创建 <field> 表示数组 db.coll.createIndex( { <field>: < 1 or -1 > } ) 间隙索引创建稀疏索引仅包含具有索引字段的文档条目,即使索引字段包含空值也是如此。索引会跳过缺少索引字段的所有文档。索引是“稀疏的”,因为它不包括集合的所有文档。相反,非稀疏索引包含集合中的所有文档,为那些不包含索引字段的文档存储空值。 # 创建一个间隙索引 db.col.createIndex({"name":1}, { sparse: true }) 部分索引创建部分索引仅索引集合中符合指定过滤器表达式的文档。 # 创建age大于5的部分索引 db.col.createIndex( { sex: 1, name: 1 }, { partialFilterExpression: { age: { $gt: 5 } } } ) 更新索引# reIndex() 在name字段上重建倒序索引 db.col.reIndex({"name":-1}) 删除索引#dropIndex() 删除集合指定的索引 db.col.dropIndex("索引名称") #dropIndexes() 删除集合全部的索引 db.col.dropIndexes() Springboot 创建mongodb 索引注解创建索引例子: 单个字段索引创建 @Data @Document("person") public class Person extends MongoEntity { @Indexed private String name; private String age; private String sex; } 多字段创建索引 @Data @Document("person") @CompoundIndexes({ //创建一个名为compound的复合索引 @CompoundIndex(name = "compound", def = "{'age' : 1, 'sex': 1}") }) public class Person extends MongoEntity { @Indexed private String name; private String age; private String sex; } 间隙索引 @CompoundIndexes({ //创建一个名为compound的复合索引 @CompoundIndex(name = "compound", def = "{'age' : 1, 'sex': 1}",sparse = true) }) 部分索引 官方文档没有提供有关于partialFilterExpression构造的说明。 mongoTemplate构建索引例子 单个索引 @Test public void template() { Index index = new Index(); index.on("name", Sort.Direction.ASC); mongoTemplate.indexOps("person").ensureIndex(index); for (int i=0;i<5;i++){ Person person = new Person(); person.setName("赵"+i); person.setAge("2"+i); person.setSex("男"); personService.save(person); } } 多个索引,我百度之后连续.on()即可以创建,但我尝试之后只有第一个索引成功。 @Test public void template() { Index index = new Index(); index.on("name", Sort.Direction.ASC) .on("age", Sort.Direction.ASC) //不生效 .on("sex", Sort.Direction.ASC).sparse(); //不生效 mongoTemplate.indexOps("person").ensureIndex(index); for (int i=0;i<5;i++){ Person person = new Person(); person.setName("赵"+i); person.setAge("2"+i); person.setSex("男"); personService.save(person); } } 间隙索引即.sarse() 部分索引 暂无。 mongoTemplate提供了增,删,删除全部,重建,查询这个五种接口。可以逐个尝试。 public interface IndexOperations { void ensureIndex(IndexDefinition indexDefinition); void dropIndex(String name); void dropAllIndexes(); void resetIndexCache(); List<IndexInfo> getIndexInfo();} ","categories":[{"name":"MongoDB","slug":"mongodb","permalink":"http://www.aquestian.cn/categories/mongodb/"}],"tags":[{"name":"MongoDB","slug":"mongodb","permalink":"http://www.aquestian.cn/tags/mongodb/"}],"author":"aqian666"},{"title":"Java连接MongoDB聚合分组查询。","slug":"blogs/Docker-mongoDBJointQuestion","date":"2022-07-12T16:00:00.000Z","updated":"2022-07-12T16:00:00.000Z","comments":true,"path":"blogs/Docker-mongoDBJointQuestion/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-mongoDBJointQuestion/","excerpt":"","text":"之前的文章说到MongoDB聚合查询实现多表联查。其实是还是有一些遗留问题的,这个文章就是在之前的文章上做个补存。咱们通过MySql和MongoDB做个对比。 单条件分组求和MySql数据库java代码此处就忽略了 类似MySql的失去了语句如下 select goodsName,sum(net)from weightingDataDetail group by goodsName sql执行成功后返回的数据应该是 goodsName net 物料1 300 物料2 500 是可以直接映射到对象返回给前端直接渲染的。 MongoDB数据库Java代码 @Test public void sum() { Aggregation agg = null; agg = Aggregation.newAggregation( group(\"goodsName\")//设置分组字段 .sum(\"net\").as(\"net\"), project(\"goodsName\",\"net\") ); AggregationResults<WeightingDataDetail> results = mongoTemplate.aggregate(agg, \"weightingDataDetail\", WeightingDataDetail.class); WeightingDataDetail weightingDataDetail = results.getMappedResults().get(0); System.err.println(JSON.toJSONString(weightingDataDetail)); } MongoDB这时候的sql为: db.weightingDataDetail.aggregate( [{ \"$group\": { \"_id\":\"$goodsName\", \"net\": { \"$sum\": \"$net\" } } }, { \"$project\": { \"goodsName\": \"$_id.goodsName\", \"net\": 1, } }] ) sql执行成功后返回的数据是 _id net 物料1 300 物料2 500 这时候映射到对象上物料在id上,这样前端是不能直接渲染,而且也不符合现实所需要的,但多条件分组时,就不会映射在id上了,很坑。 需要把mongDB的sql改为: db.weightingDataDetail.aggregate( [{ \"$group\": { \"_id\": { \"goodsName\": \"$goodsName\", }, \"net\": { \"$sum\": \"$net\" } } }, { \"$project\": { \"goodsName\": \"$_id.goodsName\", \"net\": 1, } }] ) 这时候运行结果就对了: _id net goodsName Document 300 物料1 Document 500 物料2 这样才能完全映射到对象上。 怎么才能用java构建出 “_id”: { “goodsName”: “$goodsName”, } 这种条件,就成了关键。 于是通过“曲线救国”的方式找到了如下办法: 方式一将"_id"起个别名,换成要返回的字段。 sql如下: db.weightingDataDetail.aggregate( [{ \"$group\": { \"_id\": \"$goodsName\", \"net\": { \"$sum\": \"$net\" } } }, { \"$project\": { \"goodsName\": \"$_id\", \"net\": \"$net\" } }] ) 运行结果如下 _id net goodsName 物料1 300 物料1 物料2 500 物料2 Java代码如下 @Test public void sum() { Aggregation agg = null; agg = Aggregation.newAggregation( group(\"goodsName\")//设置分组字段 .sum(\"net\").as(\"net\"), project(\"goodsName\",\"net\") .andExpression(\"_id\").as(\"goodsName\") //将id起别名 .andExpression(\"net\").as(\"net\") //或者这样起别名 .and(\"_id\").as(\"goodsName\") ); AggregationResults<WeightingDataDetail> results = mongoTemplate.aggregate(agg, \"weightingDataDetail\", WeightingDataDetail.class); WeightingDataDetail weightingDataDetail = results.getMappedResults().get(0); System.err.println(JSON.toJSONString(weightingDataDetail)); } 起别名还可以使用previousOperation,一般配合and使用,这里就不展示sql了;官方解释: 选择n字段并为ID字段创建一个别名,该别名是由前一个组操作(因此调用previousOperation())生成的,其名称为标记。 java代码如下 @Test public void sum() { Aggregation agg = null; agg = Aggregation.newAggregation( group(\"goodsName\")//设置分组字段 .sum(\"net\").as(\"net\"), project(\"goodsName\",\"net\").and(\"goodsName\").previousOperation() ); AggregationResults<WeightingDataDetail> results = mongoTemplate.aggregate(agg, \"weightingDataDetail\", WeightingDataDetail.class); WeightingDataDetail weightingDataDetail = results.getMappedResults().get(0); System.err.println(JSON.toJSONString(weightingDataDetail)); } 方式二将分组条件变为多条件,分组一个压根不存在的条件。 sql如下: db.weightingDataDetail.aggregate( [ { \"$group\": { \"_id\": { \"goodsName\": \"$goodsName\", \"1\": \"$1\" }, \"net\": { \"$sum\": \"$net\" } } }, { \"$project\": { \"goodsName\": \"$_id.goodsName\", \"net\": 1, \"1\": \"$_id.1\" } }] ) 这样也是满足的,但是在MySql中这么做肯定是会报错的,不知道这个"1"是个啥。 _id net goodsName Document 300 物料1 Document 500 物料2 java代码如下: @Test public void sum() { Aggregation agg = null; agg = Aggregation.newAggregation( group(\"goodsName\",\"1\")//设置分组字段 .sum(\"net\").as(\"net\"), project(\"goodsName\",\"net\") ); AggregationResults<WeightingDataDetail> results = mongoTemplate.aggregate(agg, \"weightingDataDetail\", WeightingDataDetail.class); WeightingDataDetail weightingDataDetail = results.getMappedResults().get(0); System.err.println(JSON.toJSONString(weightingDataDetail)); } 方式三可以使用first和last,前提是不适用sort的前提下,不适用排序,first和$last,会默认取出所有的,也会指定到返回的字段上。 $frist 返回将表达式应用到按键共享同一组文档的一组文档中的第一个文档所得到的值。仅在文档按定义的顺序排列时才有意义。 $last 返回将表达式应用于在一组按字段共享同一组文档的最后一个文档中得出的值。仅在文档按定义的 Sequences 排列时才有意义。 sql如下: db.weightingDataDetail.aggregate( [{ \"$group\": { \"_id\": \"$goodsName\", \"net\": { \"$sum\": \"$net\" }, \"goodsName\": { \"$first\": \"$goodsName\" // \"$last\": \"$goodsName\" } } }, { \"$project\": { \"goodsName\": 1, \"net\": 1 } }] ) _id net goodsName Document 300 物料1 Document 500 物料2 java代码如下: @Test public void sum() { Aggregation agg = null; agg = Aggregation.newAggregation( group(\"goodsName\")//设置分组字段 .sum(\"net\").as(\"net\") .first(\"goodsName\").as(\"goodsName\"), project(\"goodsName\",\"net\") ); AggregationResults<WeightingDataDetail> results = mongoTemplate.aggregate(agg, \"weightingDataDetail\", WeightingDataDetail.class); WeightingDataDetail weightingDataDetail = results.getMappedResults().get(0); System.err.println(JSON.toJSONString(weightingDataDetail)); } 其实这三种方式我倾向第一种起别名。 返回指定字段特殊情况多返回一个字段,前提是这个字段在分组条件里是唯一的,比如goodsName对应的orderType只有一种,不会出现一种goodsName有俩个orderType。 MySql sql如下 select goodsName,orderType,sum(net)from weightingDataDetail group by goodsName 值得注意的是mongo要像MySql这样返回分组以外的字段,这个字段必须在group条件下,才能作为返回条件。 可以使用返回指定字段方式三,不排序的情况下多加一种,也是可以的。 mongo sql如下, db.weightingDataDetail.aggregate( [{ \"$group\": { \"_id\": \"$goodsName\", \"net\": { \"$sum\": \"$net\" }, \"orderType\": { \"$first\": \"$orderType\" // \"$last\": \"$goodsName\" } } }, { \"$project\": { \"goodsName\": 1, \"net\": 1, \"orderType\":1 } }] ) _id net orderType 物料1 300 1 物料2 500 2 结合一二种方式就可以返回指定的字段。 java代码如下 @Test public void sum() { Aggregation agg = null; agg = Aggregation.newAggregation( group(\"goodsName\")//设置分组字段 .sum(\"net\").as(\"net\") .first(\"orderType\").as(\"orderType\"), project(\"goodsName\",\"net\",\"orderType\") .and(\"_id\").as(\"goodsName\") .and(\"orderType\").as(\"orderType\") ); AggregationResults<WeightingDataDetail> results = mongoTemplate.aggregate(agg, \"weightingDataDetail\", WeightingDataDetail.class); WeightingDataDetail weightingDataDetail = results.getMappedResults().get(0); System.err.println(JSON.toJSONString(weightingDataDetail)); } MongoDB是很强大的,我只是用了其中一种方式去构建聚合查询,它还有别的构建方式,后续再一点点学习。","categories":[{"name":"MongoDB","slug":"mongodb","permalink":"http://www.aquestian.cn/categories/mongodb/"}],"tags":[{"name":"MongoDB","slug":"mongodb","permalink":"http://www.aquestian.cn/tags/mongodb/"}],"author":"aqian666"},{"title":"MongoDB聚合查询。","slug":"blogs/Docker-mongoDBJoint","date":"2022-05-11T16:00:00.000Z","updated":"2022-05-11T16:00:00.000Z","comments":true,"path":"blogs/Docker-mongoDBJoint/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-mongoDBJoint/","excerpt":"","text":"查询场景mongodb 字段的参数类型不一致不能进行联查的,比如,id默认为ObjectId,另外一张表存的id为String类型,这时候不可以联查;比如存的数据是BigDecimal类型,那么java里聚合查询sum也是不可以的。所以如果表之间,或者构造器构造的字段与数据库的字段类型不一致,那么数据是查不出的。 数据结构从表1(车牌表) @Data public class Truck{ @Id protected String id; /** * 运输公司ID(主表id) * * @notExist */ private String transportId; /** * 车牌号 * * @condition * @notExist */ private String truckNo; /** * 创建时间 * * @notView */ private String createTime; /** * 运输单位(关联表的字段) */ private String unitName; /** * 邀请码(关联表的字段) */ private String inviteCode; } 我只显示关键字段,多余字段不展示,这次处理的是三表联查。上边这个表是从1表,不是主表,把它放在第一个是因为这个表是作为返回使用的。 从表2(邀请码表)注:邀请人员进入运输单位的表,无需关注我的实际业务。 /** * 邀请码管理 * @access=inviteCode * @parent=appcommon * @parentName=日常业务管理 */ @Data public class InviteCode{ @Id protected String id; /** * 邀请码 * @condition * @notExist */ private String code; /** * 所属运输单位id */ private String belongTransportId; } 主表(运输单位表)/** * 运输单位 */ @Data public class TransportUnit{ @Id protected String id; /** * 单位名称 * * @condition * @notExist */ private String unitName; /** * 创建人(邀请人) * * @notView */ private String creator; } 三个表我去了一些没用的内容,保留了三表联查的关键字段。 MongoDB sql语句实现从表结构中可以看出,运输单位表作为主表需要关联其他俩个表。从返回表里可以看到我们想要返回内容。 要注意一点的是,为啥以运输单位作为主表,不仅仅是因为主id在这个表中,而且ObjectId转String好转换,反之处理比较麻烦。 db.transportUnit.aggregate([ { $project: { id: { $toString: \"$_id\" }, unitName: 1, } }, { $lookup: { from: \"truck\", localField: \"id\", foreignField: \"transportId\", as: \"truck\" } }, { $unwind: \"$truck\" }, { $lookup: { from: \"inviteCode\", localField: \"id\", foreignField: \"belongTransportId\", as: \"inviteCode\" } }, { $unwind: \"$inviteCode\" }, { $project:{ title:1, truck:{ unitName:\"$unitName\", truckNo:1, code: '$inviteCode.code', transportId:1, creator: 1, createTime: 1, } } }, ]); 解释一下sql: 第一个lookup后使用了unwind将单个Bson拆为Bson数组,这点不可缺少,不然第二层lookup会关联不上。这里使用了project来将ObjectId转为String,当然也是通过这个返回指定字段的。因为之前使用了unwind,最后使用了group再进行一次压缩聚合。 查询结果: Java实现public PageInfo<Truck> findAllByLike(Truck truck, int page, int size) throws GenericException { String truckNo = truck.getTruckNo(); String transportName = truck.getTransportName(); Criteria criteria; criteria = Criteria.where(\"id\").not(); if (StringUtils.isNotEmpty(truckNo)){ criteria.and(\"truckNo\").regex(truckNo); } if (StringUtils.isNotEmpty(transportName)){ criteria.and(\"unitName\").regex(transportName); } Aggregation agg = Aggregation.newAggregation( project(\"id\").andExpression(\"toString(_id)\").as(\"id\") .and(\"unitName\").as(\"unitName\"), lookup(Fields.field(\"truck\"),Fields.field(\"id\"),Fields.field(\"transportId\"),Fields.field(\"truck\")), unwind(\"truck\"), lookup(\"inviteCode\",\"id\",\"belongTransportId\",\"inviteCode\"), unwind(\"inviteCode\"), project(\"unitName\") .and(\"truck.transportId\").as(\"transportId\") .and(\"inviteCode.code\").as(\"inviteCode\") .and(\"truck.creator\").as(\"creator\") .and(\"truck.createTime\").as(\"createTime\") .and(\"truck.truckNo\").as(\"truckNo\"), match(criteria), skip((page)*size), limit(size) ); Aggregation agg1 = Aggregation.newAggregation( project(\"id\").andExpression(\"toString(_id)\").as(\"id\") .and(\"unitName\").as(\"unitName\"), lookup(Fields.field(\"truck\"),Fields.field(\"id\"),Fields.field(\"transportId\"),Fields.field(\"truck\")), unwind(\"truck\"), lookup(\"inviteCode\",\"id\",\"belongTransportId\",\"inviteCode\"), unwind(\"inviteCode\"), project(\"unitName\") .and(\"truck.transportId\").as(\"transportId\") .and(\"inviteCode.code\").as(\"inviteCode\") .and(\"truck.creator\").as(\"creator\") .and(\"truck.createTime\").as(\"createTime\") .and(\"truck.truckNo\").as(\"truckNo\"), match(criteria) ); AggregationResults<Truck> results = mongoTemplate.aggregate(agg, \"transportUnit\", Truck.class); AggregationResults<Truck> results1 = mongoTemplate.aggregate(agg1, \"transportUnit\", Truck.class); List<Truck> trucks = results.getMappedResults(); List<Truck> mappedResults = results1.getMappedResults(); System.err.println(results.getMappedResults().size()); PageInfo<Truck> pageInfo = new PageInfo<>(trucks); pageInfo.setTotal(mappedResults.size()); return pageInfo; } 这里有个问题,就是聚合查询,分页的情况下无法返回总条数,所以得通过相同的条件,部分也单独查一次总条数。 注意:match查询条件必须放查询联查之后,好比sql where条件放查询结果之后。 另外一种方式查询不使用project的方式查询进行类型转换比较麻烦,使用addFields也可以实现。 sql: db.transportUnit.aggregate([ { $addFields: { id: { $toString: '$_id' } } }, { $lookup: { from: \"truck\", localField: \"id\", foreignField: \"transportId\", as: \"truck\" } }, { $unwind: \"$truck\" }, { $lookup: { from: \"inviteCode\", localField: \"id\", foreignField: \"belongTransportId\", as: \"inviteCode\" } }, { $unwind: \"$inviteCode\" }, { $project: { title: 1, truck: { unitName: \"$unitName\", truckNo: 1, code: '$inviteCode.code', transportId: 1, creator: 1, createTime: 1, } } }, ]); 查询结果一致,我就不展示了。 可能是我对mongodb不太熟悉,另外一种虽然查出来了但是构造起来有点麻烦,我只实现了个大概。 MongoCollection<Document> collection= mongoTemplate.getCollection(\"transportUnit\"); List<Document> documentArrayList= new ArrayList<>(); collection.aggregate( Arrays.asList( // Aggregates.match(Filters.eq(\"_id\", new ObjectId())), Aggregates.addFields(new Field<>(\"id\",new Document(\"$toString\",\"$_id\"))), Aggregates.lookup(\"truck\",\"id\",\"transportId\",\"truck\"), Aggregates.unwind(\"$truck\"), Aggregates.lookup(\"inviteCode\",\"id\",\"belongTransportId\",\"inviteCode\")) ).forEach((Block<? super Document>) documentArrayList::add); if (documentArrayList.size()>0){ System.err.println(documentArrayList); } 能够写出这篇文章全归功于:https://blog.csdn.net/nyzzht123/article/details/109209847 感谢","categories":[{"name":"MongoDB","slug":"mongodb","permalink":"http://www.aquestian.cn/categories/mongodb/"}],"tags":[{"name":"MongoDB","slug":"mongodb","permalink":"http://www.aquestian.cn/tags/mongodb/"}],"author":"aqian666"},{"title":"Docker搭建MongoDB副本集","slug":"blogs/Docker-mongoDB","date":"2022-04-09T16:00:00.000Z","updated":"2022-04-09T16:00:00.000Z","comments":true,"path":"blogs/Docker-mongoDB/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-mongoDB/","excerpt":"","text":"docker拉取mongodocker pull mongo 安装mongo副本集配置及端口号配置信息,启动三个以上mongo服务,节点指向一个"rs" docker run -itd --name m0 -p 27000:27017 mongo --replSet \"rs\" docker run -itd --name m1 -p 27001:27017 mongo --replSet "rs" docker run -itd --name m2 -p 27002:27017 mongo --replSet "rs" 启动成功后可以通过docker ps查看容器id docker ps 可以通过俩种方式进入容器中的mongo内部。 进入容器方式一docker exec -it 容器ID /bin/bash mongo 进入容器方式二docker exec -it m0 mongo admin 如上图则进入容器内部。 注意我这里选了m0作为我的主数据库,其余m1,m2为从数据库。 对节点进行配置var config={ _id:\"rs\", members:[ {_id:0,host:\"你服务器ip:27000\"}, {_id:1,host:\"你服务器ip:27001\"}, {_id:2,host:\"你服务器ip:27002\"} ]}; 如果你是本地安装,这里建议不安装副本集,一旦IP发送变化,副本集数据库就不好使了。 执行配置rs.initiate(config) 执行成功之后,可以通过 rs.conf() 查看是否配置成功和信息, rs.status() 查看各节点状态。 创建数据库并设置访问数据库用户权限创建数据库 use ylgroup 添加数据 db.ylgroup.insert({\"name\":\"张三\"}) 配置数据库用户访问权限 db.createUser({user:'root',pwd:'admin',roles:[{role:'userAdmin',db:'ylgroup'}]}); 附:MongoDB基本的角色 1.数据库用户角色:read、readWrite; 2.数据库管理角色:dbAdmin、dbOwner、userAdmin; 3.集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager; 4.备份恢复角色:backup、restore; 5.所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabase 6.超级用户角色:root 客户端连接","categories":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/categories/docker/"}],"tags":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/tags/docker/"},{"name":"MongoDB","slug":"mongodb","permalink":"http://www.aquestian.cn/tags/mongodb/"}],"author":"aqian666"},{"title":"Java阻塞队列与非阻塞队列","slug":"blogs/Java-Queue","date":"2022-02-23T16:00:00.000Z","updated":"2022-02-23T16:00:00.000Z","comments":true,"path":"blogs/Java-Queue/","link":"","permalink":"http://www.aquestian.cn/blogs/Java-Queue/","excerpt":"","text":"什么是阻塞与非阻塞阻塞和非阻塞指的是调用者在等待返回结果时的状态。阻塞时,在调用结果返回前,当前线程会被挂起,并在得到结果之后返回。非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前线程。因此对应非阻塞的情况,调用者需要定时轮询查看处理状态。同步和异步指具体的通信机制。同步时调用者等待返回结果。异步时,被调用者通过回调等形式通知调用者。 Java阻塞和释放阻塞的几种实现方式 sleep() 方法 sleep(毫秒),指定以毫秒为单位的时间,使线程在该时间内进入线程阻塞状态,期间得不到cpu的时间片,等到时间过去了,线程重新进入可执行状态。(暂停线程,不会释放锁) suspend() 和 resume() 方法 挂起和唤醒线程,suspend e()使线程进入阻塞状态,只有对应的resume e()被调用的时候,线程才会进入可执行状态。(不建议用,容易发生死锁) yield() 方法 会使的线程放弃当前分得的cpu时间片,但此时线程任然处于可执行状态,随时可以再次分得cpu时间片。yield()方法只能使同优先级的线程有执行的机会。调用 yield()的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。(暂停当前正在执行的线程,并执行其他线程,且让出的时间不可知) join()方法 也叫线程加入。是当前线程A调用另一个线程B的join()方法,当前线程转A入阻塞状态,直到线程B运行结束,线程A才由阻塞状态转为可执行状态。 wait() 和 notify() 方法 两个方法搭配使用,wait()使线程进入阻塞状态,调用notify()时,线程进入可执行状态。wait()内可加或不加参数,加参数时是以毫秒为单位,当到了指定时间或调用notify()方法时,进入可执行状态。属于Object类,而不属于Thread类,wait()会先释放锁住的对象,然后再执行等待的动作。由于wait()所等待的对象必须先锁住,因此,它只能用在同步化程序段或者同步化方法内,否则,会抛出异常IllegalMonitorStateException。 Java阻塞和释放阻塞的例子Java BlockingQueue 详解 抛出异常 特殊值 阻塞 超时 插入 add(e) offer(e) put(e) offer(e, time, unit) 移除 remove() poll() take() poll(time, unit) 检查 element() peek() 不可用 不可用 这里使用重点使用了wait() 和 notify() 方法, BlockingQueue里的put(),take()方法。 import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class BlockingQueueTest { private static final int count = 10; //生产者 public static class ProductThread implements Runnable { private BlockingQueue<Integer> queue; public ProductThread(BlockingQueue<Integer> queue) { this.queue = queue; } public void run(){ while(true){ synchronized (queue) { try { while (queue.size() == 10) { System.out.println("队列已满"); queue.notify(); queue.wait(); } queue.offer(5); System.out.println(Thread.currentThread()+"生产了一个产品---队列已有元素:"+queue.size()+"个,剩余:"+ (count - queue.size())); Thread.sleep(1000); }catch (InterruptedException e) { queue.notify(); } } } } } //消费者 public static class ConsumeThread implements Runnable { private BlockingQueue<Integer> queue; public ConsumeThread(BlockingQueue<Integer> queue) { this.queue = queue; } public void run(){ while(true){ synchronized (queue) { try { while (queue.size() == 0) { System.out.println("队列为空"); queue.notify(); queue.wait(); } queue.take(); System.out.println(Thread.currentThread()+"消费了一个产品---队列已有元素:"+queue.size()+"个,剩余:"+ (count - queue.size())); Thread.sleep(1000); }catch (InterruptedException e) { queue.notify(); } } } } } public static void main(String[] args) { //大小为10的循环数组阻塞队列 BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(count); new Thread(new ProductThread(queue)).start(); new Thread(new ConsumeThread(queue)).start(); } } 它会依次建创建好的队列中加入元素,元素已满时,生产者ProductThread阻塞,释放消费者ConsumeThread释放,依次交替阻塞释放。如果想更好的理解阻塞还是释放,可以注释掉其中一个queue.notify();,或者注释掉queue.wait();。","categories":[{"name":"Java实例","slug":"java实例","permalink":"http://www.aquestian.cn/categories/java%E5%AE%9E%E4%BE%8B/"}],"tags":[{"name":"队列","slug":"队列","permalink":"http://www.aquestian.cn/tags/%E9%98%9F%E5%88%97/"}],"author":"aqian666"},{"title":"我的程序人生——第四年","slug":"life/Life-Fourth-Year","date":"2021-11-25T16:00:00.000Z","updated":"2021-11-25T16:00:00.000Z","comments":true,"path":"life/Life-Fourth-Year/","link":"","permalink":"http://www.aquestian.cn/life/Life-Fourth-Year/","excerpt":"","text":"程序人生第四年21年11月25日上午,在出差所在地,辽阔的大草原上,发生了一起车祸。嗯~~,往事放心底吧!有感而发,也给经常出门在外的自己写个总结。 去年11月来的新公司,一直到去年过年,都很平淡,期间我忙里偷闲拿到了驾照。年后,工作一下变得紧张匆忙起来,项目得在3月份中旬上线,而年前写的业务逻辑与现实逻辑大相径庭,基本需要推翻重新写了,也怪自己大意忽略了很多细节上的内容。没办法,上线日期不变,只能是加班加点的改。到了约定日期,也只能是硬着头皮强行上线了。不出所料,现场出了大状况,都乱成一锅粥了,电话都快被打爆了,第一次感到这种扑面而来的压力,当时我都快头皮抓破了。每况愈下,项目只能是停止上线,择日改好了再重新上线。经过几天的查漏补缺,项目总算上线了,那段时间我最怕的就是电话铃声响起来,但现场还是有不同的业务,于是忽,就有了我生平第一次出差的经历。出差的第一站———河北平权,说实话,刚开始我很抵触出差的,不过还好第一次出差也就三天,但这新环境着实让人有点无所适从。在这里我也见识到了我所面临的客户群体是谁,除了司机还有本地矿业的计量员,他们的文化水平是比较低的,有的甚至键盘都不会用。一开始,我个人的想法就是满足客户递交上来的技术协议即可,至于你们用的方便不方便跟我没有关系。现在来看,自己的这种自私行为不能说完全错,但这很明显,有能力解决不去解决,这种行为真是令人不齿。 第二站我出差的目的地是河北迁安,距离第一次出差过了一个月吧。有了第一次后,第二次出差就没有那么反感了,但多少还是会反感的。这一次实打实的来解决问题来了,本来已经是做了一路准备的,刚来下午,我就想跑路了。一个大姐笔记本上好几篇洋洋洒洒一堆要让我改的,不停的说着:“还有···,还有···”,我真是受够了这股唐山话了。话不投机半句多,把本放那儿我一个个记下啦!迁安这个地方很有意思,全国前五的县级市,跟我汾阳市一个级别的。我们领导跟我说,迁安市是把城边的一条河,挖成个湖,又把湖中心填上改了别墅,别墅呢是迁安市首富的府邸,上他们家得经过层层检查,这湖比河宽呐,又再湖上建了几座“跨江大桥”。汾阳市,山鸡岂能配凤凰,不配相提并论。接近尾声准备打道回府的时候,半路杀出个程咬金!“运维?什么运维,别把这拖油瓶挂我脖子上。”这是鄙人原话,此话一出,给我们领导都整无语了。没错,甲方单位招了一个“运维”,来跟我对接,我还得负责教她一些运维上的东西,话不投机半句多! 这里我想多用一点笔墨描写一下我的领导们—— 一群对钓鱼这个活动的“狂热份子”。这些领导身上我是可以看到他们独具特色的闪光点,但总而言之,他们是职场上经历过风雨的人,都是被社会或者生活抹平了棱角的人。他们的脾性值得我去学习,有人说这样会导致个性不足,其实普通人就做自己力所能及的事情就可以了,没必要彰显自己的个性,更不需要成为焦点。当然钓鱼确实是一件乐事,垂钓者需要是太公钓鱼的心态。 七月,我奔赴厦门,这是我第一次去到这么远的地方。这次远行,我能感受到公司对于新人培养的重视程度,一是让我熟悉不同地方不同客户的业务需求,二是邀请我加入阿米巴经验管理课程学习班,我这个新人做的一般,在学习方面确实没有落实到位。除去工作,我感触最深的就是我与宝岛台湾隔海相望。乘坐厦门地铁,听着闽南话报站名,驶过一座大桥才能到达厦门岛上,要上鼓浪屿还得再买一张登岛的船票。鼓浪屿我最先去到的是日光岩,它是鼓浪屿的最高点,也是隔海望向台湾海峡的最好的瞭望点,也是郑成功亲自提名的宝地。皓月园也令人印象深刻,毕竟我这种远离大海的“旱鸭子”,这次我勇敢地尝试了在太平洋畔洗脚。而后我又惬意的坐上“海上看金门”的游轮,一路喝着茶,海鸥保驾护航,临近金门岛时,远远的看见岛上醒目的八个大红字——三民主义,统一中国,当然了,当年我们也毫不示弱在对岸写了八个大字——一国两制,统一中国。事实证明,孙中山这一套理论,不能说一窍不通,起码是一地鸡毛吧。如果我是一位勇士,我应该纵身一跃扎进海里,一个猛子冲过去再来一个鲤鱼打挺翻身上岸,给这八个字来个大翻新—— 一国两制,统一中国。岛内的某些人士,我可去你的吧。我是坐高铁一路北上的,可以说是从最南边到最北边了吧,我证明了,祖国幅员辽阔! 在内蒙古,我又领略了美丽的大草原,这才是牛羊它们真正的天堂啊,随便吃,吃饱躺着消化。拿起手机随手一拍,都是可以作为电脑桌面的背景,处处是镜头。这一次来内蒙主要是为了调研需求,我也没想到我干起了产品的事情,也是第一次做流程图,第一次出方案,第一次做设计报告,冥冥中路又走宽了。此后我还在秋季,冬季也就是现在,统共三次踏上这片草原,领略三季的草原变化,也见识了大自然的鬼斧神工。 这里我想感谢那些积极配合客户,在抛出问题的同时,还能一起想解决问题的方案!(我还是得呼吁一下,客户少找我一次。大姐。听到了吗?话不投机半句多,如果能有一礼拜不找我,我就把你的铃声还回去!) 回顾一下这一年,我对技术倒是没有特别高的要求了,反而在项目中锻炼了自己的业务能力。涨了不少见识,对事对物也更有见地,可以从宏观角度看待问题,言行举止也成熟了,给自己打个分的话能给一个75分吧,值得给自己点个赞! 回到文章开头,这个司机师傅还是离开了这个世界。有感而发,希望为了生活出门在外,一路奔波的你我,都能平安回家。","categories":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/categories/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"tags":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/tags/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"author":"aqian666"},{"title":"MySQL 递归查询","slug":"blogs/MySQL-Recursion","date":"2021-11-09T16:00:00.000Z","updated":"2021-11-09T16:00:00.000Z","comments":true,"path":"blogs/MySQL-Recursion/","link":"","permalink":"http://www.aquestian.cn/blogs/MySQL-Recursion/","excerpt":"","text":"写在前面众所周知,java中递归查询,需要和数据库进行多次交互,不论是向上查询还是向下查询,所以不如进行一次交互就完成查询。据我了解,Oracle实现递归查询非常的方便,但mysql不行,需要自定义函数来完成。 创建表(Dept)DROP TABLE IF EXISTS `dept`; CREATE TABLE `dept` ( `id` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `pid` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1000', '总公司', NULL); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1001', '北京分公司', '1000'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1002', '上海分公司', '1000'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1003', '北京研发部', '1001'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1004', '北京财务部', '1001'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1005', '北京市场部', '1001'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1006', '北京研发一部', '1003'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1007', '北京研发二部', '1003'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1008', '北京研发一部一小组', '1006'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1009', '北京研发一部二小组', '1006'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1010', '北京研发二部一小组', '1007'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1011', '北京研发二部二小组', '1007'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1012', '北京市场一部', '1005'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1013', '上海研发部', '1002'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1014', '上海研发一部', '1013'); INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1015', '上海研发二部', '1013'); 创建完成后入下图 首先熟悉一下,mysql find_in_set函数。 find_in_set 函数函数语法:find_in_set(str,strlist) str 代表要查询的字符串 , strlist 是一个以逗号分隔的字符串,如 (‘a,b,c’)。 此函数用于查找 str 字符串在字符串 strlist 中的位置,返回结果为 1 ~ n 。若没有找到,则返回0。 举个例子: select FIND_IN_SET('b','a,b,c,d'); 此外,在对表数据进行查询时,它还有一种用法,如下: select * from dept where FIND_IN_SET(id,'1000,1001,1002'); 以向下递归查询所有子节点为例,可以找到一个包含当前节点和所有子节点的以逗号拼接的字符串 strlist,传进 find_in_set 函数。就可以查询出所有需要的递归数据了。 那么,现在问题就转化为怎样构造这样的一个字符串 strlist。 ### concat concat_ws group_concat函数 一、字符串拼接函数中,最基本的就是 concat 了。它用于连接N个字符串,如, select CONCAT('M','Y','S','Q','L') from dual; 结果为 ‘MYSQL’ 字符串。 二、concat 是以逗号为默认的分隔符,而 concat_ws 则可以指定分隔符,第一个参数传入分隔符,如以下划线分隔。 select concat_ws('_','M','Y','S','Q','L') from dual; 三、group_concat 函数更强大,可以分组的同时,把字段以特定分隔符拼接成字符串。 用法:group_concat( [distinct] 要连接的字段 [order by 排序字段 asc/desc ] [separator ‘分隔符’] ) 可以看到有可选参数,可以对将要拼接的字段值去重,也可以排序,指定分隔符。若没有指定,默认以逗号分隔。 对于 dept 表,我们可以把表中的所有 id 以逗号拼接。(这里没有用到 group by 分组字段,则可以认为只有一组) select group_concat(id) from dept; MySQL 自定义函数实现递归查询可以发现以上已经把字符串拼接的问题也解决了。那么,问题就变成怎样构造有递归关系的字符串了。 我们可以自定义一个函数,通过传入根节点id,找到它的所有子节点。 向下递归。创建自定义函数 create function get_child_list(in_id varchar(10)) returns varchar(1000) begin declare ids varchar(1000) default ''; declare tempids varchar(1000); set tempids = in_id;while tempids is not null doset ids = CONCAT_WS(',',ids,tempids);select GROUP_CONCAT(id) into tempids from dept where FIND_IN_SET(pid,tempids)>0;end while;return ids;end create function get_child_list 创建函数。并且参数传入一个根节点的子节点id,需要注意一定要注明参数的类型和长度,如这里是 varchar(10)。returns varchar(1000) 用来定义返回值参数类型。 begin 和 end 中间包围的就是函数体。用来写具体的逻辑。 declare 用来声明变量,并且可以用 default 设置默认值。这里定义的 ids 即作为整个函数的返回值,是用来拼接成最终我们需要的以逗号分隔的递归串的。而 tempids 是为了记录下边 while 循环中临时生成的所有子节点以逗号拼接成的字符串。 set 用来给变量赋值。此处把传进来的根节点赋值给 tempids 。 while do … end while; 循环语句,循环逻辑包含在内。注意,end while 末尾需要加上分号。 循环体内,先用 CONCAT_WS 函数把最终结果 ids 和 临时生成的 tempids 用逗号拼接起来。然后以FIND_IN_SET(pid,tempids)>0 为条件,遍历在 tempids 中的所有 pid ,寻找以此为父节点的所有子节点 id ,并且通过 GROUP_CONCAT(id) into tempids 把这些子节点 id 都用逗号拼接起来,并覆盖更新 tempids 。等下次循环进来时,就会再次拼接 ids ,并再次查找所有子节点的所有子节点。循环往复,一层一层的向下递归遍历子节点。直到判断 tempids 为空,说明所有子节点都已经遍历完了,就结束整个循环。 return ids; 用于把 ids 作为函数返回值返回。 自定义函数做好之后,我们就可以用它来递归查询我们需要的数据了。如,我查询北京研发部的所有子节点。 向上递归创建自定义函数 create function get_parent_list(in_id varchar(10)) returns varchar(1000) begin declare ids varchar(1000); declare tempid varchar(10); set tempid = in_id;while tempid is not null doset ids = CONCAT_WS(',',ids,tempid);select pid into tempid from dept where id=tempid;end while;return ids;end 查找北京研发二部一小组,以及它的递归父节点,如下: 注意事项我们用到了 group_concat 函数来拼接字符串。但是,需要注意它是有长度限制的,默认为 1024 字节。可以通过 show variables like “group_concat_max_len”; 来查看。 注意,单位是字节,不是字符。在 MySQL 中,单个字母占1个字节,而我们平时用的 utf-8下,一个汉字占3个字节。 这个对于递归查询还是非常致命的。因为一般递归的话,关系层级都比较深,很有可能超过最大长度。(尽管一般拼接的都是数字字符串,即单字节) 所以,我们有两种方法解决这个问题: 修改 MySQL 配置文件 my.cnf ,增加 group_concat_max_len = 102400 #你要的最大长度 。 执行以下任意一个语句。SET GLOBAL group_concat_max_len=102400; 或者 SET SESSION group_concat_max_len=102400; 他们的区别在于,global是全局的,任意打开一个新的会话都会生效,但是注意,已经打开的当前会话并不会生效。而 session 是只会在当前会话生效,其他会话不生效。 共同点是,它们都会在 MySQL 重启之后失效,以配置文件中的配置为准。所以,建议直接修改配置文件。102400 的长度一般也够用了。假设一个id的长度为10个字节,也能拼上一万个id了。 除此之外,使用 group_concat 函数还有一个限制,就是不能同时使用 limit 。如, 本来只想查5条数据来拼接,现在不生效了。 不过,如果需要的话,可以通过子查询来实现: 转载于:IT牧场","categories":[{"name":"MySQL","slug":"mysql","permalink":"http://www.aquestian.cn/categories/mysql/"}],"tags":[{"name":"MySQL","slug":"mysql","permalink":"http://www.aquestian.cn/tags/mysql/"}],"author":"aqian666"},{"title":"面试官:Spring和SpringBoot有哪些区别?","slug":"blogs/Question-Spring-SpringBoot","date":"2021-08-10T16:00:00.000Z","updated":"2021-08-10T16:00:00.000Z","comments":true,"path":"blogs/Question-Spring-SpringBoot/","link":"","permalink":"http://www.aquestian.cn/blogs/Question-Spring-SpringBoot/","excerpt":"","text":"什么是Spring作为Java开发人员,大家都Spring都不陌生,简而言之,Spring框架为开发Java应用程序提供了全面的基础架构支持。它包含一些很好的功能,如依赖注入和开箱即用的模块,如:Spring JDBC 、Spring MVC 、Spring Security、 Spring AOP 、Spring ORM 、Spring Test,这些模块缩短应用程序的开发时间,提高了应用开发的效率例如,在Java Web开发的早期阶段,我们需要编写大量的代码来将记录插入到数据库中。但是通过使用Spring JDBC模块的JDBCTemplate,我们可以将操作简化为几行代码。 ## 什么是Spring Boot Spring Boot基本上是Spring框架的扩展,它消除了设置Spring应用程序所需的XML配置,为更快,更高效的开发生态系统铺平了道路。 Spring Boot中的一些特征 创建独立的Spring应用。 嵌入式Tomcat、Jetty、 Undertow容器(无需部署war文件)。 提供的starters 简化构建配置 尽可能自动配置spring应用。 提供生产指标,例如指标、健壮检查和外部化配置 完全没有代码生成和XML配置要求 从配置分析区别Maven依赖首先,让我们看一下使用Spring创建Web应用程序所需的最小依赖项 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.1.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.0.RELEASE</version> </dependency> 与Spring不同,Spring Boot只需要一个依赖项来启动和运行Web应用程序: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.0.6.RELEASE</version> </dependency> 在进行构建期间,所有其他依赖项将自动添加到项目中。 另一个很好的例子就是测试库。我们通常使用Spring Test,JUnit,Hamcrest和Mockito库。在Spring项目中,我们应该将所有这些库添加为依赖项。但是在Spring Boot中,我们只需要添加spring-boot-starter-test依赖项来自动包含这些库。 Spring Boot为不同的Spring模块提供了许多依赖项。一些最常用的是: spring-boot-starter-data-jpa spring-boot-starter-security spring-boot-starter-test spring-boot-starter-web spring-boot-starter-thymeleaf 有关starter的完整列表,请查看Spring文档。 MVC配置让我们来看一下Spring和Spring Boot创建JSP Web应用程序所需的配置。 Spring需要定义调度程序servlet,映射和其他支持配置。我们可以使用 web.xml 文件或Initializer类来完成此操作: web.xml <?xml version=\"1.0\" encoding=\"UTF-8\"?> <web-app xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://java.sun.com/xml/ns/javaee\" xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd\" id=\"WebApp_ID\" version=\"3.0\"> <servlet> <!--名称 --> <servlet-name>springmvc</servlet-name> <!-- Servlet类 --> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 启动顺序,数字越小,启动越早 --> <load-on-startup>1</load-on-startup> <init-param> <!--SpringMVC配置参数文件的位置 --> <param-name>contextConfigLocation</param-name> <!--默认名称为ServletName-servlet.xml --> <param-value>classpath*:springmvc-servlet.xml</param-value> </init-param> </servlet> <!--所有请求都会被springmvc拦截 --> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app> <?xml version=\"1.0\" encoding=\"UTF-8\"?> <beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:context=\"http://www.springframework.org/schema/context\" xmlns:mvc=\"http://www.springframework.org/schema/mvc\" xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd\"> <!-- 自动扫描包,实现支持注解的IOC --> <context:component-scan base-package=\"com.xxx.xxx\" /> <!-- Spring MVC不处理静态资源 --> <mvc:default-servlet-handler /> <!-- 支持mvc注解驱动 --> <mvc:annotation-driven /> <!-- 视图解析器 --> <bean class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\" id=\"internalResourceViewResolver\"> <!-- 前缀 --> <property name=\"prefix\" value=\"/WEB-INF/view/\" /> <!-- 后缀 --> <property name=\"suffix\" value=\".jsp\" /> </bean> </beans> Initializer类 public class MyWebAppInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext container) { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.setConfigLocation("com.xxx.xxx"); container.addListener(new ContextLoaderListener(context)); ServletRegistration.Dynamic dispatcher = container .addServlet("dispatcher", new DispatcherServlet(context)); dispatcher.setLoadOnStartup(1); dispatcher.addMapping("/"); } } 还需要将@EnableWebMvc注释添加到@Configuration类,并定义一个视图解析器来解析从控制器返回的视图: @EnableWebMvc @Configuration public class ClientWebConfig implements WebMvcConfigurer { @Bean public ViewResolver viewResolver() { InternalResourceViewResolver bean = new InternalResourceViewResolver(); bean.setViewClass(JstlView.class); bean.setPrefix("/WEB-INF/view/"); bean.setSuffix(".jsp"); return bean; } } 再来看SpringBoot一旦我们添加了Web启动程序,Spring Boot只需要在application配置文件中配置几个属性来完成如上操作: spring.mvc.view.prefix=/WEB-INF/jsp/ spring.mvc.view.suffix=.jsp 上面的所有Spring配置都是通过一个名为auto-configuration的过程添加Boot web starter来自动包含的。 这意味着Spring Boot将查看应用程序中存在的依赖项,属性和bean,并根据这些依赖项,对属性和bean进行配置。当然,如果我们想要添加自己的自定义配置,那么Spring Boot自动配置将会退回。 配置模板引擎现在我们来看下如何在Spring和Spring Boot中配置Thymeleaf模板引擎。 在Spring中,我们需要为视图解析器添加thymeleaf-spring5依赖项和一些配置: @Configuration @EnableWebMvc public class MvcWebConfig implements WebMvcConfigurer { @Autowired private ApplicationContext applicationContext; @Bean public SpringResourceTemplateResolver templateResolver() { SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); templateResolver.setApplicationContext(applicationContext); templateResolver.setPrefix("/WEB-INF/views/"); templateResolver.setSuffix(".html"); return templateResolver; } @Bean public SpringTemplateEngine templateEngine() { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver()); templateEngine.setEnableSpringELCompiler(true); return templateEngine; } @Override public void configureViewResolvers(ViewResolverRegistry registry) { ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setTemplateEngine(templateEngine()); registry.viewResolver(resolver); } } SpringBoot1X只需要spring-boot-starter-thymeleaf的依赖项来启用Web应用程序中的Thymeleaf支持。 但是由于Thymeleaf3.0中的新功能,我们必须将thymeleaf-layout-dialect 添加为SpringBoot2XWeb应用程序中的依赖项。配置好依赖,我们就可以将模板添加到src/main/resources/templates文件夹中,SpringBoot将自动显示它们。 应用程序启动引导配置Spring和Spring Boot中应用程序引导的基本区别在于servlet。 Spring使用web.xml 或SpringServletContainerInitializer作为其引导入口点。 Spring Boot仅使用Servlet 3功能来引导应用程序,下面让我们详细来了解下 Spring 引导配置 Spring支持传统的web.xml引导方式以及最新的Servlet 3+方法。 配置web.xml方法启动的步骤 Servlet容器(服务器)读取web.xml web.xml中定义的DispatcherServlet由容器实例化 DispatcherServlet通过读取WEB-INF / {servletName} -servlet.xml来创建WebApplicationContext。最后,DispatcherServlet注册在应用程序上下文中定义的bean 使用Servlet 3+方法的Spring启动步骤 容器搜索实现ServletContainerInitializer的类并执行SpringServletContainerInitializer找到实现所有类WebApplicationInitializer``WebApplicationInitializer创建具有XML或上下文@Configuration类WebApplicationInitializer创建DispatcherServlet与先前创建的上下文。 SpringBoot 引导配置 Spring Boot应用程序的入口点是使用@SpringBootApplication注释的类 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 默认情况下,Spring Boot使用嵌入式容器来运行应用程序。在这种情况下,Spring Boot使用public static void main入口点来启动嵌入式Web服务器。此外,它还负责将Servlet,Filter和ServletContextInitializer bean从应用程序上下文绑定到嵌入式servlet容器。 Spring Boot的另一个特性是它会自动扫描同一个包中的所有类或Main类的子包中的组件。 Spring Boot提供了将其部署到外部容器的方式。我们只需要扩展SpringBootServletInitializer即可: /** * War部署 * * @author SanLi * Created by [email protected] on 2018/4/15 */ public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(Application.class); } @Override public void onStartup(ServletContext servletContext) throws ServletException { super.onStartup(servletContext); servletContext.addListener(new HttpSessionEventPublisher()); } } 这里外部servlet容器查找在war包下的META-INF文件夹下MANIFEST.MF文件中定义的Main-class,SpringBootServletInitializer将负责绑定Servlet,Filter和ServletContextInitializer。 打包和部署最后,让我们看看如何打包和部署应用程序。这两个框架都支持Maven和Gradle等通用包管理技术。但是在部署方面,这些框架差异很大。例如,Spring Boot Maven插件在Maven中提供Spring Boot支持。它还允许打包可执行jar或war包并就地运行应用程序。 在部署环境中Spring Boot 对比Spring的一些优点包括: 提供嵌入式容器支持 使用命令java -jar独立运行jar 在外部容器中部署时,可以选择排除依赖关系以避免潜在的jar冲突 部署时灵活指定配置文件的选项 用于集成测试的随机端口生成 结论简而言之,我们可以说Spring Boot只是Spring本身的扩展,使开发,测试和部署更加方便。 转载:https://www.jianshu.com/p/ffe5ebe17c3a","categories":[{"name":"面试题","slug":"面试题","permalink":"http://www.aquestian.cn/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/"}],"tags":[{"name":"SpringBoot","slug":"springboot","permalink":"http://www.aquestian.cn/tags/springboot/"},{"name":"Spring","slug":"spring","permalink":"http://www.aquestian.cn/tags/spring/"}],"author":"aqian666"},{"title":"Linux服务器启动后自动启动Jar包","slug":"blogs/Linux-Start-Jar","date":"2021-06-27T16:00:00.000Z","updated":"2021-06-27T16:00:00.000Z","comments":true,"path":"blogs/Linux-Start-Jar/","link":"","permalink":"http://www.aquestian.cn/blogs/Linux-Start-Jar/","excerpt":"","text":"在任意一个路径中创建startup.sh创建startup.shvim /home/startup.sh # 将环境配置写进去 不清楚自己配置的可以用 more /etc/profile 在最下方查看 脚本如下: #将环境配置写进去 不清楚自己配置的可以用 more /etc/profile查看 JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.232.b09-0.el7_7.x86_64 PATH=$PATH:$JAVA_HOME/bin #服务器启动10秒后运行 sleep 10s #jar包启动命令 找到jar包对应路径 nohup java -jar /gbq/boot-gbq-web-0.0.1-SNAPSHOT.jar >/gbq/daan.log 2>1& 给startup.sh添加权限chmod +x /home/startup.sh 添加开机启动 vim /etc/rc.local 在rc.local中加上一行 /home/startup.sh 添加完之后可以reboot重启服务器,之后再使用ps -ef | grep java查看是否启动成功 为啥会在服务器启动10秒之后才执行这个脚本有一些环境如Docker搭建的环境,Docker服务也是需要时间启动,包括内部容器等,等它们启动完成后再启动Jar包,这样可以确保Jar包启动时环境都已经存在。","categories":[{"name":"Linux","slug":"linux","permalink":"http://www.aquestian.cn/categories/linux/"}],"tags":[{"name":"Linux","slug":"linux","permalink":"http://www.aquestian.cn/tags/linux/"}],"author":"aqian666"},{"title":"Docker搭建SqlServer2017","slug":"blogs/Docker-SqlServer2017","date":"2021-06-10T16:00:00.000Z","updated":"2021-06-10T16:00:00.000Z","comments":true,"path":"blogs/Docker-SqlServer2017/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-SqlServer2017/","excerpt":"","text":"前提条件前提条件(至少2 GB的磁盘空间。至少2 GB的RAM)。 详见:https://docs.microsoft.com/zh-cn/sql/linux/sql-server-linux-docker-container-deployment?view=sql-server-ver15&pivots=cs1-bash 安装部署拉取镜像 docker pull mcr.microsoft.com/mssql/server:2017-latest 运行镜像 docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=MyPassWord123" -p 1433:1433 --name sql1 -d mcr.microsoft.com/mssql/server:2017-latest 这里设置了密码为MyPassWord123 查看镜像是否启动成功 docker ps","categories":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/categories/docker/"}],"tags":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/tags/docker/"},{"name":"SqlServer","slug":"sqlserver","permalink":"http://www.aquestian.cn/tags/sqlserver/"}],"author":"aqian666"},{"title":"在Window上彻底卸载Docker","slug":"blogs/Windows-Delete-Docker","date":"2021-05-16T16:00:00.000Z","updated":"2021-05-16T16:00:00.000Z","comments":true,"path":"blogs/Windows-Delete-Docker/","link":"","permalink":"http://www.aquestian.cn/blogs/Windows-Delete-Docker/","excerpt":"","text":"在windows10上卸载docker后再重装会提示已安装,但之前已经卸载了,为什么还是会这样提示呢? 是因为docker卸载之后并没有将注册表信息删掉,所以在重装的时候还是会索引到的。 下面告诉大家一个卸载干净的方法 新建一个 a.ps1 的文件 $ErrorActionPreference = \"SilentlyContinue\" kill -force -processname 'Docker for Windows', com.docker.db, vpnkit, com.docker.proxy, com.docker.9pdb, moby-diag-dl, dockerd try { ./MobyLinux.ps1 -Destroy } Catch {} service = Get-WmiObject -Class Win32_Service -Filter "Name='com.docker.service'" if (service) { service.StopService() } if (service) { $service.Delete() } Start-Sleep -s 5 Remove-Item -Recurse -Force “~/AppData/Local/Docker” Remove-Item -Recurse -Force “~/AppData/Roaming/Docker” if (Test-Path “C:\\ProgramData\\Docker”) { takeown.exe /F “C:\\ProgramData\\Docker” /R /A /D Y } if (Test-Path “C:\\ProgramData\\Docker”) { icacls "C:\\ProgramData\\Docker&quot; /T /C /grant Administrators:F } Remove-Item -Recurse -Force “C:\\ProgramData\\Docker” Remove-Item -Recurse -Force “C:\\Program Files\\Docker” Remove-Item -Recurse -Force “C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Docker” Remove-Item -Force “C:\\Users\\Public\\Desktop\\Docker for Windows.lnk” Get-ChildItem HKLM:\\software\\microsoft\\windows\\currentversion\\uninstall | % {Get-ItemProperty $.PSPath} | ? { $.DisplayName -eq “Docker” } | Remove-Item -Recurse -Force Get-ChildItem HKLM:\\software\\classes\\installer\\products | % {Get-ItemProperty $.pspath} | ? { $.ProductName -eq “Docker” } | Remove-Item -Recurse -Force Get-Item ‘HKLM:\\software\\Docker Inc.’ | Remove-Item -Recurse -Force Get-ItemProperty HKCU:\\software\\microsoft\\windows\\currentversion\\Run -name “Docker for Windows” | Remove-Item -Recurse -Force #Get-ItemProperty HKCU:\\software\\microsoft\\windows\\currentversion\\UFH\\SHC | ForEach-Object {Get-ItemProperty $.PSPath} | Where-Object { $.ToString().Contains(“Docker for Windows.exe”) } | Remove-Item -Recurse -Force $.PSPath #Get-ItemProperty HKCU:\\software\\microsoft\\windows\\currentversion\\UFH\\SHC | Where-Object { $(Get-ItemPropertyValue $) -Contains “Docker” } 执行a.ps1将以上内容写进去,然后以管理员身份打开powershell ,并执行 ./a.ps1 如果提示如下错误: 执行 set-executionpolicy remotesigned 再次执行a.ps1,成功删除。","categories":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/categories/docker/"}],"tags":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/tags/docker/"}],"author":"aqian666"},{"title":"springboot校园社团管理系统源码分享","slug":"code/Code-Seven","date":"2021-04-22T16:00:00.000Z","updated":"2021-04-22T16:00:00.000Z","comments":true,"path":"code/Code-Seven/","link":"","permalink":"http://www.aquestian.cn/code/Code-Seven/","excerpt":"","text":"项目描述springboot校园社团管理系统源码分享,前端使用layui.js,后端使用springboot+mybatis。 运行环境jdk8+tomcat8+mysql5.7+IntelliJ IDEA+maven 项目技术spring boot+spring mvc+mybatis+jquery+layui 项目截图 运行截图localhost:8080 下边为部分截图","categories":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/categories/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"tags":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/tags/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"author":"aqian666"},{"title":"SpringCloud 学习——网关服务Zuul","slug":"blogs/SpringCould-Zuul","date":"2021-04-19T16:00:00.000Z","updated":"2021-04-19T16:00:00.000Z","comments":true,"path":"blogs/SpringCould-Zuul/","link":"","permalink":"http://www.aquestian.cn/blogs/SpringCould-Zuul/","excerpt":"","text":"Spring Cloud Zuul介绍Zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix等组件配合使用,Spring Cloud对Zuul进行了整合与增强,Zuul默认使用的HTTP客户端是Apache HTTPClient,也可以使用RestClient或okhttp3.OkHttpClient。 Zuul的主要功能是路由转发和过滤器。路由功能是微服务的一部分,如下图所示,/test/*转发到到demo服务。zuul默认和Ribbon结合实现了负载均衡的功能.。 Zuul使用一系列不同类型的过滤器,使我们能够快速灵活地将功能应用于我们的边缘服务。这些过滤器可帮助我们执行以下功能 身份验证和安全性 - 确定每个资源的身份验证要求并拒绝不满足这些要求的请求 洞察和监控 - 在边缘跟踪有意义的数据和统计数据,以便为我们提供准确的生产视图 动态路由 - 根据需要动态地将请求路由到不同的后端群集 压力测试 - 逐渐增加群集的流量以衡量性能。 Load Shedding - 为每种类型的请求分配容量并删除超过限制的请求静态响应处理 - 直接在边缘构建一些响应,而不是将它们转发到内部集群 Zuul实现路由转发和过滤器consumer-zuul子项目搭建继续在之前的聚合工程中创建子项目,consumer-zuul 项目结构 pom文件 只需要引入zuul包即可 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> yml配置 server: port: 13000 eureka: client: service-url: defaultZone: http://eureka01:8800/eureka/,http://eureka02:8810/eureka/ #构建路由地址zuul:routes:#这里可以自定义demo1:#匹配的路由规则path: /consumer-zuul-a/**#路由的目标服务名serviceId: providerdemo2:#匹配的路由规则path: /consumer-zuul-b/**#路由的目标服务名serviceId: consumer-feign spring:application:name: consumer-zuulmain:allow-bean-definition-overriding: true 启动类 加入注解 @EnableZuulProxy @SpringBootApplication @EnableZuulProxy public class ConsumerZuulApplication { public static void main(String[] args) { SpringApplication.run(ConsumerZuulApplication.class, args); } } 启动 同时启动yml对应的服务 Zuul实现过滤器ServiceFilter类 @Component public class ServiceFilter extends ZuulFilter { @Override public String filterType() { //filterType 为过滤类型,可选值有 pre(路由之前)、routing(路由之时)、post(路由之后)、error(发生错误时调用)。 return "pre"; } @Override public int filterOrder() { //filterOrdery 为过滤的顺序,如果有多个过滤器,则数字越小越先执行 return 0; } @Override public boolean shouldFilter() { //shouldFilter 表示是否过滤,这里可以做逻辑判断,true 为过滤,false 不过滤 return true; } @Override public Object run() throws ZuulException { //run 为过滤器执行的具体逻辑,在这里可以做很多事情,比如:权限判断、合法性校验等。 //这里写校验代码 RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); String name = request.getParameter("name"); if(!"12345".equals(name)){ context.setSendZuulResponse(false); context.setResponseStatusCode(401); try { context.getResponse().setCharacterEncoding("UTF-8"); context.getResponse().getWriter().write("名字错了"); }catch (Exception e){} } return null; } } 运行","categories":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/categories/springcloud/"}],"tags":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/tags/springcloud/"},{"name":"Zuul","slug":"zuul","permalink":"http://www.aquestian.cn/tags/zuul/"}],"author":"aqian666"},{"title":"SpringCloud 学习——熔断监控Turbine","slug":"blogs/SpringCould-Turbine","date":"2021-04-14T16:00:00.000Z","updated":"2021-04-14T16:00:00.000Z","comments":true,"path":"blogs/SpringCould-Turbine/","link":"","permalink":"http://www.aquestian.cn/blogs/SpringCould-Turbine/","excerpt":"","text":"Spring Cloud Turbine介绍在复杂的分布式系统中,相同服务的节点经常需要部署上百甚至上千个,很多时候,运维人员希望能够把相同服务的节点状态以一个整体集群的形式展现出来,这样可以更好的把握整个系统的状态。 为此,Netflix提供了一个开源项目(Turbine)来提供把多个hystrix.stream的内容聚合为一个数据源供Dashboard展示。 监测接口(仪表盘)加入仪表盘紧接上一篇,加入仪表盘,ribbon和feign中同时加入。 pom文件 <!--暴露各种指标--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-javanica</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-hystrix-dashboard</artifactId> </dependency> yml配置 management: endpoints: web: exposure: include: 'hystrix.stream' 启动类 加入注解 @EnableHystrix @EnableHystrixDashboard 启动访问 http://localhost:port/hystrix 效果为 同理端口11001也是如此! Turbine实现监控HystrixTurbine子项目搭建继续之前的maven工程创建,这个和上边不一样的是,可以实现整体服务的监控。 项目结构 pom文件 <!--暴露各种指标--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-turbine</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-hystrix-dashboard</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> yml配置 server: port: 12000 eureka: client: service-url: defaultZone: http://eureka01:8800/eureka/,http://eureka02:8810/eureka/ turbine:aggregator:#指定聚合哪些集群,多个使用","分割,默认为default。可使用http://.../turbine.stream?cluster={clusterConfig之一}访问clusterConfig: default#配置Eureka中的serviceId列表,表明监控哪些服务appConfig: consumer-ribbon-hystrix,consumer-feign-hystrix#设置监控的表达式,通过此表达式表示要获取监控信息名称clusterNameExpression: new String("default") spring:application:name: service-turbinemain:allow-bean-definition-overriding: true 启动类 @SpringBootApplication @EnableTurbine @EnableHystrixDashboard//启用Hystrix Dashboard public class ServiceTurbineApplication { public static void main(String[] args) { SpringApplication.run(ServiceTurbineApplication.class, args); } } 启动 这样就可以检测到需要检测的服务了","categories":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/categories/springcloud/"}],"tags":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/tags/springcloud/"},{"name":"Turbine","slug":"turbine","permalink":"http://www.aquestian.cn/tags/turbine/"}],"author":"aqian666"},{"title":"SpringCloud 学习——自保熔断Hystrix","slug":"blogs/SpringCould-Hystrix","date":"2021-04-11T16:00:00.000Z","updated":"2021-04-11T16:00:00.000Z","comments":true,"path":"blogs/SpringCould-Hystrix/","link":"","permalink":"http://www.aquestian.cn/blogs/SpringCould-Hystrix/","excerpt":"","text":"Spring Cloud Hystrix介绍在分布式环境中,许多服务依赖关系中的一些必然会失败。Hystrix是一个库,它通过添加延迟容忍和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点、停止跨服务的级联故障并提供回退选项来实现这一点,所有这些选项都提高了系统的总体弹性。 简单来说,就是分布式项目中,有很多微服务之间不听的项目调用,如果出现了被调用者出现问题,宕机了,那么这时候熔断器就要发挥它的作用了。 Ribbon实现熔断ribbon-hystrix子项目搭建继续在之前的聚合工程中创建子项目,consumer-ribbon-hystrix pom文件 新加入cloud,hystrix配置文件即可 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> yml配置 eureka: client: serviceUrl: defaultZone: http://eureka01:8800/eureka/,http://eureka02:8810/eureka/ server: tomcat: uri-encoding: UTF-8 port: 11001 spring:application:name: consumer-ribbon-hystrixmain:allow-bean-definition-overriding: true 启动类 @EnableHystrix //在启动类上添加@EnableHystrix注解开启Hystrix的熔断器功能。 @SpringBootApplication //@EnableDiscoveryClient //从Spring Cloud Edgware开始,@EnableDiscoveryClient 或@EnableEurekaClient 可省略。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。 public class ConsumerRibbonHystrixApplication { public static void main(String[] args) { SpringApplication.run(ConsumerRibbonHystrixApplication.class, args); } //开启restTemplate负载均衡 @Bean @LoadBalanced RestTemplate restTemplate(){ return new RestTemplate(); } /** * 配置随机负载策略,需要配置属性service-B.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule */ @Bean public IRule ribbonRule() { return new RandomRule(); } } Controller @RestController public class HiController { @Autowired private HiService hiService; @GetMapping("/hi") public String sayHello(String name){ return hiService.sayHello(name); } } Service @Service public class HiServiceImpl implements HiService { @Autowired private RestTemplate restTemplate; @Override @HystrixCommand(fallbackMethod = "getDefaultUser") public String sayHello(String name) { if(null != name){ String url = "http://provider/hello?name="+name; System.err.println(url); return restTemplate.getForObject(url,String.class); }else { //名字为空请求一个不存在的地址 String url = "http://provider/null"; return restTemplate.getForObject(url,String.class); } } public String getDefaultUser(String name) { return "熔断,默认回调--名称是:"+name; } } 运行 Feign实现熔断feign-hystrix子项目搭建 新建一个consumer-feign-hystrix子项目,目录结构如下 yml配置文件 eureka: client: serviceUrl: defaultZone: http://localhost:8800/eureka/,http://localhost:8810/eureka/ server: tomcat: uri-encoding: UTF-8 port: 11002 feign: hystrix: enabled: true #启用hystrix # servlet: # context-path: /gbq_consumer spring: main: allow-bean-definition-overriding: true #避免相同名字的Feign注册会导致重复注册 application: name: consumer-feign-hystrix 启动类 @SpringBootApplication @EnableFeignClients //@EnableDiscoveryClient public class ConsumerFeignHystrixApplication { public static void main(String[] args) { SpringApplication.run(ConsumerFeignHystrixApplication.class, args); } } 不需要加其他的东西 Controller @RestController public class HiController { @Resource private GetHello getHello; @GetMapping("/test") public User getPostUser(User user){ return getHello.getPostUser(user); } } //GetHello @FeignClient(name = "provider",fallback = UserFeignClientFallback.class) //@FeignClient(name = "provider") public interface GetHello { @RequestMapping(value = "/getPostUser",method = RequestMethod.POST) User getPostUser(@RequestBody User user); } Comment @Component public class UserFeignClientFallback implements GetHello{ @Override public User getPostUser(User user) { return new User(1,"熔断用户1"); } } 对应的provider接口 @PostMapping("/getPostUser") public User getPostUser(@RequestBody User user) throws InterruptedException { if (user.getId()!=1){ System.err.println("1"); return user; //正常返回 }else { Thread.sleep(10000L);//休息10秒,意味着会出现请求超时 return null; } } 请求超时就会发生熔断 运行","categories":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/categories/springcloud/"}],"tags":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/tags/springcloud/"},{"name":"Hystrix","slug":"hystrix","permalink":"http://www.aquestian.cn/tags/hystrix/"}],"author":"aqian666"},{"title":"SpringCloud 学习——服务调用Feign","slug":"blogs/SpringCould-Feign","date":"2021-04-02T16:00:00.000Z","updated":"2021-04-02T16:00:00.000Z","comments":true,"path":"blogs/SpringCould-Feign/","link":"","permalink":"http://www.aquestian.cn/blogs/SpringCould-Feign/","excerpt":"","text":"Spring Cloud Feign 介绍Feign是一个声明式的Web服务客户端。这使得Web服务客户端的写入更加方便 要使用Feign创建一个界面并对其进行注释。它具有可插拔注释支持,包括Feign注释和JAX-RS注释。Feign还支持可插拔编码器和解码器。Spring Cloud添加了对Spring MVC注释的支持,并在Spring Web中使用默认使用的HttpMessageConverters。Spring Cloud集成Ribbon和Eureka以在使用Feign时提供负载均衡的http客户端。 声明式的WebService客户端,微服务之间调用API更加方便! Feign 设计图 使用Feign实现服务之间调用Feign子项目搭建接着上一篇再创建一个子项目consumer-feign pom文件 引入feign所需的jar包即可 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> yml配置 eureka: client: serviceUrl: defaultZone: http://localhost:8800/eureka/,http://localhost:8810/eureka/ server: tomcat: uri-encoding: UTF-8 port: 10002 # servlet: # context-path: /gbq_consumer spring: main: allow-bean-definition-overriding: true #避免相同名字的Feign注册会导致重复注册 application: name: consumer-feign 启动类 @SpringBootApplication @EnableFeignClients //@EnableDiscoveryClient public class ConsumerFeignApplication { public static void main(String[] args) { SpringApplication.run(ConsumerFeignApplication.class, args); } } Controller @RestController public class HiController { @Autowired private GetHello getHello; @GetMapping("/hi") public String sayHello(String name){ return getHello.sayHello(name); } @GetMapping("/test") public User getPostUser(User user){ return getHello.getPostUser(user); } @GetMapping("/getUser") public User getUser(User user){ return getHello.getUser(user); } } 此处写了三个方法,为的就是踩坑,填坑!具体坑我已经在代码注释中写明了! GetHello接口类 @FeignClient(name = "provider",path = "gbq_provider") //@FeignClient(name = "provider") public interface GetHello { //坑一 //@GetMapping 不支持 @RequestMapping(value = "/hello",method = RequestMethod.GET) String sayHello(@RequestParam("name")String name); //坑二 传递参数必须使用对应注解 @RequestMapping(value = "/getPostUser",method = RequestMethod.POST) User getPostUser(@RequestBody User user); //坑三 传递参数为复杂参数是请求方式即便设置为get,但仍然以post请求发送 @RequestMapping(value = "/getUser",method = RequestMethod.GET) User getUser(@RequestBody User user); } User类 public class User { private Integer id; private String name; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } provider的Controller类 也就之前文章中创建的provider,provider-copy俩个子项目,为了一会更好的体现feign和ribbon整合实现负载均衡,所以俩个项目的Controller和上一篇一样,写一样的方法。 注意注释 @RestController public class HelloController { @Value("${server.port}") private int port; @Resource private HelloService helloService; @GetMapping("/hello") public String sayHello(String name){ return "你好,cloud"+"name:"+name+"port:"+port; } @PostMapping("/getPostUser") public User getPostUser(@RequestBody User user){ return user; } /** * 此处就可以看到,我是使用get请求发送,却用了post接收 **/ @PostMapping("/getUser") public User getUser(@RequestBody User user){ return user; } } 启动 此处因为我启动了俩个相同名称的提供者服务,也就是provider,provider-copy,请求的时候,会按照顺序请求! 这几个请求之间建议好好对比一下,你也可以在Feign调用接口类里边,多尝试不同请求方式,以及不同的参数格式,发送请求! 上一篇文章提到一个坑,就是配置了context-path或者server-path之后,就请求不到了,而feign解决了这个问题 @FeignClient(name = “provider”,path = “gbq_provider”) //配置这个path就可以了 feign结合ribbon进行服务之间调用其实这个很简单。 feign结合ribbon子项目搭建直接复制一份feign。 pom文件 加入ribbon即可 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> yml配置 eureka: client: serviceUrl: defaultZone: http://localhost:8800/eureka/,http://localhost:8810/eureka/ server: tomcat: uri-encoding: UTF-8 port: 10003 # servlet: # context-path: /gbq_consumer spring: main: allow-bean-definition-overriding: true #避免相同名字的Feign注册会导致重复注册 application: name: consumer-feign-ribbon 启动类 @SpringBootApplication @EnableFeignClients //@EnableDiscoveryClient public class ConsumerFeignRibbonApplication { public static void main(String[] args) { SpringApplication.run(ConsumerFeignRibbonApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate(){ return new RestTemplate(); } /** * 配置随机负载策略,需要配置属性service-B.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule */ @Bean public IRule ribbonRule() { return new RandomRule(); } } 其他类和feign子项目一样 启动 同样实现了负载均衡!","categories":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/categories/springcloud/"}],"tags":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/tags/springcloud/"},{"name":"Feign","slug":"feign","permalink":"http://www.aquestian.cn/tags/feign/"}],"author":"aqian666"},{"title":"SpringCloud 学习——负载均衡Ribbon","slug":"blogs/SpringCould-Ribbon","date":"2021-03-28T16:00:00.000Z","updated":"2021-03-28T16:00:00.000Z","comments":true,"path":"blogs/SpringCould-Ribbon/","link":"","permalink":"http://www.aquestian.cn/blogs/SpringCould-Ribbon/","excerpt":"","text":"上一篇总结了Eureka,并且创建了一个客户端provider,这次总结一下ribbon,实现微服务负载均衡。 Spring Cloud Ribbon介绍一个客户端的负载均衡器,它提供对大量的HTTP和TCP客户端的访问控制。 客户端负载均衡即是当浏览器向后台发出请求的时候,客户端会向 Eureka Server 读取注册到服务器的可用服务信息列表,然后根据设定的负载均衡策略(没有设置即用默认的),抉择出向哪台服务器发送请求。 负载均衡服务器端和客户端的区别服务器端通常使用nginx实现负载均衡,如下图 nginx代理服务器,然后选择可用的服务器发送请求。 客户端也就是咱们Ribbon,如下图 Ribbon负载均衡实现Ribbon子项目搭建接着上一篇再创建一个子项目consumer-ribbon pom文件 其他都一样,引入ribbon <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> yml配置 eureka: client: serviceUrl: defaultZone: http://eureka01:8800/eureka/,http://eureka02:8810/eureka/ server: tomcat: uri-encoding: UTF-8 port: 10001 # servlet: # context-path: /gbq_consumer_ribbon spring: application: name: consumer_ribbon 注意,不要配置context-path 或者server-path,这样当你的微服务被调用时,会出现找不到的报错!如果非要加,请自行百度一下,我查到的并不太好使! 启动类 @SpringBootApplication //@EnableDiscoveryClient //从Spring Cloud Edgware开始,@EnableDiscoveryClient 或@EnableEurekaClient 可省略。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。 public class ConsumerRibbonApplication { public static void main(String[] args) { SpringApplication.run(ConsumerRibbonApplication.class, args); } //开启restTemplate负载均衡 @Bean @LoadBalanced RestTemplate restTemplate(){ return new RestTemplate(); } /** * 配置随机负载策略,需要配置属性service-B.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule */ @Bean public IRule ribbonRule() { return new RandomRule(); } } Controller @RestController public class HiController { @Autowired private HiService hiService; @GetMapping("/hi") public String sayHello(String name){ return hiService.sayHello(name); } } Service实现类@Service public class HiServiceImpl implements HiService { @Autowired private RestTemplate restTemplate; @Override public String sayHello(String name) { String url = "http://provider/hello?name="+name; return restTemplate.getForObject(url,String.class); } } 这样ribbon就新建完毕了。 那么ribbon需要调用provider提供者服务的hello接口,我们需要再provider这个项目中写这么一个接口! provider子项目接口实现controller @RestController public class HelloController { @Value("${server.port}") private int port; @GetMapping("/hello") public String sayHello(String name){ return "你好,cloud"+"name:"+name+"port:"+port; } } 你要实现负载均衡,那么还需要一个provider提供者服务作为参照,结果才能明显 provider-copy子项目搭建直接复制一份provider,修改名字和yml配置即可 yml配置 eureka: client: serviceUrl: defaultZone: http://eureka01:8800/eureka/,http://eureka02:8810/eureka/ instance: prefer-ip-address: true #将自己的ip地址注册到Eureka服务中 server: tomcat: uri-encoding: UTF-8 port: 9902 #改变端口号即可 # servlet: # context-path: /gbq_provider spring: application: name: provider #名称需要一样 注意:只要修改端口即可 其他都一样。 启动 咱们看一下负载均衡的效果,这里拿postman测试! 端口号,看到变化了吧!!!","categories":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/categories/springcloud/"}],"tags":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/tags/springcloud/"},{"name":"Ribbon","slug":"ribbon","permalink":"http://www.aquestian.cn/tags/ribbon/"}],"author":"aqian666"},{"title":"SpringCloud 学习——注册中心Eureka","slug":"blogs/SpringCould-Eureka","date":"2021-03-23T16:00:00.000Z","updated":"2021-03-23T16:00:00.000Z","comments":true,"path":"blogs/SpringCould-Eureka/","link":"","permalink":"http://www.aquestian.cn/blogs/SpringCould-Eureka/","excerpt":"","text":"SpringCloud Eureka 介绍SpringCloud Eureka 是对Netflix公司的Eureka的二次封装,这个需要注意。 Eureka由两个组件组成:Eureka服务器和Eureka客户端。 如上图所示,Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务 Eureka client客户端连接到 Eureka Server,并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。举个简单的实例,比如现实生活中您给您的爱车上了牌照以后,每年都需要进行车检。此时,您和您的爱车就属于众多微服务中的一个,而车检机构就是服务中心。 本章重点说明服务中心。 Eureka Server服务中心创建新建maven聚合工程新建一个聚合工程,因为后期会学到Ribbon,Feign等其他内容,再新建一个子工程springboot,建成以后如下图。 eureka 子工程搭建pom文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cloud.demo</groupId> <artifactId>eureka</artifactId> <version>0.0.1-SNAPSHOT</version> <name>eureka</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.M3</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> </project> 注意boot和cloud 版本需要对应 yml配置文件 server: port: 8800 eureka:instance:hostname: eureka01client:registerWithEureka: false #服务注册,是否将自己注册到Eureka服务中fetchRegistry: false #服务发现,是否从Eureka中获取注册信息serviceUrl:defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ 启动类 @SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } } 启动 访问eureka01:8800 上图红色提示信息: THE SELF PRESERVATION MODE IS TURNED OFF.THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OFNETWORK/OTHER PROBLEMS. 自我保护模式被关闭。在网络或其他问题的情况下可能不会保护实例失效。Eureka Server有一种自我保护模式,当微服务不再向Eureka Server上报状态,Eureka Server会从服务列表将此服务删除,如果出现网络异常情况(微服务正常),此时Eureka server进入自保护模式,不再将微服务从服务列表删除。 在开发阶段建议关闭自保护模式。 eureka 集群好处:微服务需要连接两台Eureka Server注册,当其中一台Eureka死掉也不会影响服务的注册与发现。 eureka2子工程搭建步骤同eureka子工程搭建,你可以直接复制一份。建好之后项目结构如下图 pom文件 同eureka的pom文件 Eureka Sever相互注册eureka-copy的yml配置文件 server: port: 8810 eureka:instance:hostname: eureka02client:registerWithEureka: truefetchRegistry: trueserviceUrl: #Eureka客户端与Eureka服务端的交互地址,高可用状态配置对方的地址,单机状态配置自己(如果不配置则默认本机8761端口)defaultZone: http://eureka01:8800/eureka/server:项目enable‐self‐preservation: false #是否开启自我保护模式eviction‐interval‐timer‐in‐ms: 60000 #服务注册表清理间隔(单位毫秒,默认是60*1000) eureka的yml配置文件修改 server: port: 8800 eureka: instance: hostname: eureka01 client: registerWithEureka: true fetchRegistry: true serviceUrl: #Eureka客户端与Eureka服务端的交互地址,高可用状态配置对方的地址,单机状态配置自己(如果不配置则默认本机8761端口) defaultZone: http://eureka02:8810/eureka/ server: enable‐self‐preservation: false #是否开启自我保护模式 eviction‐interval‐timer‐in‐ms: 60000 #服务注册表清理间隔(单位毫秒,默认是60*1000) 主机名设置Mac或者Linux配置方式 如果你使用的是osx系统。可以找到/etc/hosts文件并添加如下内容: 127.0.0.1 eureka01 127.0.0.1 eureka02 一般情况下配置完成后就会生效,如果配置不生效,重启即可 Windows配置方式 可以修改C:\\Windows\\System32\\drivers\\etc\\hosts文件,添加内容与Mac方式一致。 启动 eureka01:8800 eureka02:8810 分别访问 服务注册创建子项目provider将provider项目注册到服务中心eureka server 集群环境中,项目结构如下 pom文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cloud.demo</groupId> <artifactId>provider</artifactId> <version>0.0.1-SNAPSHOT</version> <name>provider</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.M3</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.0.RELEASE</version> <scope>compile</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> yml配置文件 eureka: client: serviceUrl: defaultZone: http://eureka01:8800/eureka/,http://eureka02:8810/eureka/ #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔 instance: prefer-ip-address: true #将自己的ip地址注册到Eureka服务中 server: tomcat: uri-encoding: UTF-8 port: 9901 servlet: context-path: /provider spring: application: name: provider 启动类 @SpringBootApplication @EnableEurekaServer @EnableDiscoveryClient //代表自己是一个Eureka的客户端 public class ProviderApplication { public static void main(String[] args) { SpringApplication.run(ProviderApplication.class, args); } } 启动项目 成功注册!","categories":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/categories/springcloud/"}],"tags":[{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/tags/springcloud/"},{"name":"Eureka","slug":"eureka","permalink":"http://www.aquestian.cn/tags/eureka/"}],"author":"aqian666"},{"title":"使用FFmpeg推送rtmp流","slug":"blogs/Nginx-FFmpeg","date":"2021-01-13T16:00:00.000Z","updated":"2021-01-13T16:00:00.000Z","comments":true,"path":"blogs/Nginx-FFmpeg/","link":"","permalink":"http://www.aquestian.cn/blogs/Nginx-FFmpeg/","excerpt":"","text":"服务器设置关闭防火墙或者开放端口1935,80端口 关闭防火墙 systemctl stop firewalld.service 禁⽌firewall开机启动 systemctl disable firewalld.service #查看默认防⽕墙状态(关闭后显示notrunning,开启后显示running) firewall-cmd --state 安装FFmpeg 1、安装yasm编译器 wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz tar -xvf yasm-1.3.0.tar.gz cd yasm-1.3.0/ ./configure make make install 2、安装FFmpeg wget http://www.ffmpeg.org/releases/ffmpeg-3.4.tar.gz tar -xvf ffmpeg-3.4.tar.gz cd ffmpeg-3.4/ ./configure --enable-shared --prefix=/usr/local/ffmpeg make make install 3.安装yum sudo yum install epel-release -y sudo rpm –import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm 4、安装FFmpeg和FFmpeg开发包 sudo yum install ffmpeg ffmpeg-devel -y 5、安装libx264解码包 sudo yum install x264 x264-devel -y 6、验证ffmpeg是否安装成功 ffmpeg -version 安装nginx1、安装基本的编译⼯具 yum install gc gcc gcc-c++ pcre-devel zlib-devel openssl-devel 2、下载rtmp模块 cd /usr/local/src # 保存⽬录 git clone https://github.com/arut/nginx-rtmp-module.git 3、下载nginx wget http://nginx.org/download/nginx-1.18.0.tar.gz tar zxvf nginx-1.18.0.tar.gz 4、编译nginx cd nginx-1.18.0 ./configure --prefix=/usr/local/nginx --add-module=/usr/local/src/nginx-rtmp-module make && make install 5、设置启动脚本 vi /etc/init.d/nginx 填写⼀下内容 #!/bin/bash # chkconfig: - 30 21 # description: http service. # Source Function Library . /etc/init.d/functions # Nginx Settings NGINX_SBIN="/usr/local/nginx/sbin/nginx" NGINX_CONF="/usr/local/nginx/conf/nginx.conf" NGINX_PID="/usr/local/nginx/logs/nginx.pid" RETVAL=0 prog="Nginx" start() { echo -n $"Starting $prog: " mkdir -p /dev/shm/nginx_temp daemon $NGINX_SBIN -c $NGINX_CONF RETVAL=$? echo return $RETVAL } stop() { echo -n $"Stopping $prog: " killproc -p $NGINX_PID $NGINX_SBIN -TERM rm -rf /dev/shm/nginx_temp RETVAL=$? echo return $RETVAL } reload(){ echo -n $"Reloading $prog: " killproc -p $NGINX_PID $NGINX_SBIN -HUP RETVAL=$? echo return $RETVAL } restart(){ stop start } configtest(){ $NGINX_SBIN -c $NGINX_CONF -t return 0 } case "$1" in start) start ;; stop) stop ;; reload) reload ;; restart) restart ;; configtest) configtest ;; *) echo $"Usage: $0 {start|stop|reload| restart|configtest}" RETVAL=1 esacexit $RETVAL 编辑完成后保存 5、 更改权限 chmod 755 /etc/init.d/nginx 6、 添加到启动项配置 chkconfig --add nginx 7、 开机启动 chkconfig nginx on 8、 基本命令 service nginx start service nginx stop service nginx restart 6、配置nginx⽀持rtmp和hls协议 vim /usr/local/nginx/conf/nginx.conf 在HTTP标签同级添加RTMP配置内容 7、 RTMP配置 rtmp { server { listen 1935; chunk_size 4096; application live { live on; record off; } } } 8、修改完成配置后,需要重启nginx服务器 service nginx reload ffmpeg推流NginxFFmpeg能够讲数据流“推流”到已经搭建好的Nginx流媒体服务器上。 Nginx已经增加了RTMP协议的⽀持,因此借助FFmpeg推流成功后,在Nginx服务器上可以得到两种视频流:RTMP流。 需要注意,不管是哪种流,在推流过程中是RTMP流形式体现的。 RTMP流,推流⾄live ffmpeg -re -i {input-source} -vcodec libx264 -vprofile baseline -acodec aac -ar 44100 -strict -2 -ac 1 -f flv -s 1280x720 -q 10 rtmp://localhost:1935/live/test 或者 ffmpeg -i {input-source} -f flv -r 25 -s 1280*720 -an rtmp://localhost:1935/live/test 例如: ffmpeg -i rtsp://摄像头登录账号:密码@摄像头ip:554 -f flv -r 25 -an rtmp://localhost:1935/hls/mystream 拉流地址 RTMP流: rtmp://localhost:1935/rtmplive/test","categories":[{"name":"视频流","slug":"视频流","permalink":"http://www.aquestian.cn/categories/%E8%A7%86%E9%A2%91%E6%B5%81/"}],"tags":[{"name":"Nginx","slug":"nginx","permalink":"http://www.aquestian.cn/tags/nginx/"},{"name":"FFmpeg","slug":"ffmpeg","permalink":"http://www.aquestian.cn/tags/ffmpeg/"}],"author":"aqian666"},{"title":"我的程序人生——第三年","slug":"life/Life-Third-Year","date":"2020-12-11T16:00:00.000Z","updated":"2020-12-11T16:00:00.000Z","comments":true,"path":"life/Life-Third-Year/","link":"","permalink":"http://www.aquestian.cn/life/Life-Third-Year/","excerpt":"","text":"程序人生第三年2020年其实大家都挺难的,本来都是开开心心回家过大年的,谁曾想新冠肺炎来势汹汹,像我这样的外地回乡的打工人,基本上是大门不出,期间也接了不少小活,赚点了点零花钱。 就这样2月就被搁置了。我记得我是2月底返回天津市的,被隔离俩个星期之后其实也并没有第一时间复工,一直等到3月18号,我才收到前公司的通知——在家办公。 在家办公期间其实也没啥事情干,接到领导什么消息处理什么,反正在家办公谁看的到呢?我也懒得弄,应付交差,草草了事。期间我查个人所得税,发现我第一家公司利用我个人信息偷税,顺手我就点了个举报。果不其然,几天后我第一家公司老板毛总亲切的给我致电:“郭,你咋把我举报了。”我也没有说撕破脸,装傻充楞:“毛总,我不知道啊,我只是看到app上有个申请啥的按钮,我就点了。”最后,我自己又去了一趟旧单位签署了一份兼职协议,老毛也是客客气气道歉,回到旧单位不由得唏嘘,才一年,旧单位已经没人了,其实我应该跟这个老板说一声谢谢的,虽然他不咋地,但他确实是给了我第一碗饭吃。 四月份的时候,我尝试着去面试别的公司,想着可以骑驴找马,这里有必要提及一下这个公司——千行时代。我本来已经面试通过了,于是我就和公司提了离职,还被我老板恶意嘲讽能力不够,第二天这千行时代就放我鸽子,说白了就是破公司找到更便宜的人了,那么自然是把我拒之门外了。这件事也同时告诫我,以后入职先得确定入职通知书拿到手,才算是工作真正的拿捏到自己手里了,我和我舍友互相开玩笑:“本来是骑驴找马,结果马没找到,回头一看驴也消失了。”后续我稀稀落落的又面试了几家,遇到几件有意思的事,比如面试过了都准备入职了,被通知学历不够;比如面试问我换工作就是为了涨工资吗?等等!永远都想不到下一个面试官是个什么脑回路。 我不得不吐槽一下我仅仅入职三天就草草离职的公司——天地伟业。这公司推行的是狼性文化,什么是狼性文化?也就是马云先生提出的996,推崇加班文化,利用有限的时间去干无限的工作。天地伟业是个很会做洗脑工作的公司,公司整个园区标语口号随处可见,喇叭也是定时播放公司新闻,各个大厅,电梯间都有滚动屏宣传优秀员工,电脑桌面都是固定的势保节点,不死不休。这种环境对我来说无疑是颇感压力的。我的组长眼睛瞪得想铜铃,他就是挥鞭逼迫员工的众多领头羊之一,下任务,排工期,催进度,做验收,开会总结。当挨骂成为习惯,加班成为常态,工作成为生活的主导,这里的人我觉得他们已经失去了意义。我第一天下午问过我的组长:“今晚上没什么事我就先回去了。”“哦!没什么事就先回去吧,我们一般都会加会儿班,晚上11点才回去呢!”结果第二天晚上我就被恶意留下加班了,果然运行我回去的时候已经晚上11点了,出来已经没有共享单车了,我步行了5公里,到我住的小窝已经晚上12点了。想了一路想通了,我就普普通通,平平淡淡就ok,第三天下午我就从天地伟业撤退了! 五月份我收到了我四月份的工资短信通知,发现少了三分之一。当我跟我老板核实情况,而老板给我的反馈却是你在家办公没写日报,按旷工处理。当我收集证据准备仲裁这一无良公司,而黑心老板在电话里却是含血喷人,说我已经离职,收集到的证据属于窃取公司重要文件。后续我又了解到国家规定二月份应该发放全额工资,而我黑心老板只按照天津市最低工资标准发放时,我拿好我的证据去到劳动仲裁局,仲裁局问我确定要仲裁吗?目前案件挤压太多,到你估计得到一年之后了,我又去到劳动监察局,状纸我都写好了,结果工作人员劝我:“弟弟回去吧,就几千块钱,你回头还得来五六次提交证据,每来一次就得请假,这不是白白损失钱吗?况且你这个不一定能赢这个官司,就是赢了你以后找工作,人公司看你告过前公司,谁还敢要你?”看着这个油腔滑调的工作人员,虽然带着中国共产党党徽,但确是满脑子图省事,假仁假义的为人民服务,我带着我的状子回去了,毫无意义的抗争罢了。我佩服那些勇于斗争的人们,但我的心态就是一个比小市民还小市民的小市民心态——没有必胜的把握,杀敌一千自损一千二的行为,我属实不敢做,也承担不起。最后和家里人聊到这些痛楚的心酸事,出门在外不容易,吃亏是福,吃亏是福! 转眼间到了六月份,天气一下就热起来了,而此时疫情也很好的控制住了,不知道是不是时来运转,我同时收到了好几家公司的录取通知书,有幸在掌上天津呆了一周,最后出于对金钱的渴望选择了立生科技。工作内容简简单单,做的都是一些杂儿活,公司项目是做区块的,而我就是给它维护好而已,其实一开始说是做商城的,来了之后我才发现是做区块的。 七月,我舍友因为去年接了一个私活,被陕西省富平县公安局拘留。这个私活貌似就是区块,我不由得打个寒颤。我另外一个舍友跟我说他之前公司也是做区块的。在之后的工作中我也是保持戒心,我也面对面问我的老板公司所做的业务是否有资格证书?是否合法等等,我也留好了录音。九月份有幸通宵加了一次班,这是我入行以来第一次通宵加班。十月后,我一直都在干这一些运营的工作,虽然简单,但属实无聊又烦躁。本来想着年后再找一份新的工作吧,但在十一月中旬,大周六早上给我打电话,莫名其妙把我骂了一顿,我都没来得及的反应。周一我找他对线,他说是给上边领导做做样子,说我每天上午不就处理完那些破玩意了吗,说白了就是说我划水,混日子,整的我挺无语的。好巧不巧,周五我就面到了新公司。 总结2020年,其实挺操蛋的,我并没有学会太多的新的技术,但是破事确实是一件接一件。接下来就是新的征程了,也算是新的一年马上要开始,给这一年开开路。","categories":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/categories/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"tags":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/tags/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"author":"aqian666"},{"title":"Java通过FTP连接NAS服务器——上传文件","slug":"blogs/Java-FTP-NAS","date":"2020-11-30T16:00:00.000Z","updated":"2020-11-30T16:00:00.000Z","comments":true,"path":"blogs/Java-FTP-NAS/","link":"","permalink":"http://www.aquestian.cn/blogs/Java-FTP-NAS/","excerpt":"","text":"搭建一个NAS服务器新手教程:https://post.smzdm.com/p/a3gw7q27/ 这篇文章我只进行到第二大步,第三步配置DDNS,安装应用程序,我没有实现,连不上网络。 外网访问在加了一些相关群之后,我了解到了需要如何才能外网访问NAS,也就是第三步。 如上图,公网IP需要给连接网络所在运营商(联通,电信,移动)去申请,这个申请其实还是比较麻烦的,那么就只能是改公网IP,目前我没有找到改公网IP的办法,所以上图中的方式也就不行了。 使用内网穿透连接NAS花生壳我就不多介绍了,能玩NAS,肯定也知道花生壳。 花生壳是可以分配一个域名给用户使用的,外网端口一般也是随机生成的。 内网主机:也就是NAS服务器的内网IP,端口为21。 设置NAS文件传输器 因为花生壳映射的端口是变化的,通常在10000-30000之间。 通过FTP连接到NAS服务器,上传文件。import org.apache.commons.net.ftp.FTPClient; import org.apache.log4j.Logger; import java.io.*; /** * 通过FTP上传文件 * * @Author lvhaibao * @Date 2018/2/11 21:43 */ public class FTPTools { //用于打印日志 private static final Logger log = Logger.getLogger(FTPTools.class); //设置私有不能实例化 private FTPTools() { } /** * 上传 * * @param hostname ip或域名地址 * @param port 端口 * @param username 用户名 * @param password 密码 * @param workingPath 服务器的工作目 * @param inputStream 要上传文件的输入流 * @param saveName 设置上传之后的文件名 * @return */ public static boolean upload(String hostname, int port, String username, String password, String workingPath, InputStream inputStream, String saveName) { boolean flag = false; FTPClient ftpClient = new FTPClient(); //1 测试连接 if (connect(ftpClient, hostname, port, username, password)) { try { //2 检查工作目录是否存在 if (ftpClient.changeWorkingDirectory(workingPath)) { // 3 检查是否上传成功 if (storeFile(ftpClient, saveName, inputStream)) { flag = true; disconnect(ftpClient); } } } catch (IOException e) { log.error(\"工作目录不存在\"); e.printStackTrace(); disconnect(ftpClient); } } return flag; } /** * 断开连接 * * @param ftpClient * @throws Exception */ public static void disconnect(FTPClient ftpClient) { if (ftpClient.isConnected()) { try { ftpClient.disconnect(); log.error(\"已关闭连接\"); } catch (IOException e) { log.error(\"没有关闭连接\"); e.printStackTrace(); } } } /** * 测试是否能连接 * * @param ftpClient * @param hostname ip或域名地址 * @param port 端口 * @param username 用户名 * @param password 密码 * @return 返回真则能连接 */ public static boolean connect(FTPClient ftpClient, String hostname, int port, String username, String password) { boolean flag = false; try { //ftp初始化的一些参数 ftpClient.connect(hostname, port); ftpClient.enterLocalPassiveMode(); ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); ftpClient.setControlEncoding(\"UTF-8\"); if (ftpClient.login(username, password)) { log.info(\"连接ftp成功\"); flag = true; } else { log.error(\"连接ftp失败,可能用户名或密码错误\"); try { disconnect(ftpClient); } catch (Exception e) { e.printStackTrace(); } } } catch (IOException e) { log.error(\"连接失败,可能ip或端口错误\"); e.printStackTrace(); } return flag; } /** * 上传文件 * * @param ftpClient * @param saveName 全路径。如/home/public/a.txt * @param fileInputStream 要上传的文件流 * @return */ public static boolean storeFile(FTPClient ftpClient, String saveName, InputStream fileInputStream) { boolean flag = false; try { if (ftpClient.storeFile(saveName, fileInputStream)) { flag = true; log.error(\"上传成功\"); disconnect(ftpClient); } } catch (IOException e) { log.error(\"上传失败\"); disconnect(ftpClient); e.printStackTrace(); } return flag; } public static void main(String[] args) throws FileNotFoundException { String hostname = \"ip\"; int port = 端口; String username = \"nas账户\"; String password = \"nas密码\"; String workingPath = \"/NAS/images/\"; String str = \"C:\\\\Users\\\\43834\\\\Desktop\\\\1.png\"; InputStream fileInputStream = new FileInputStream(new File(str)); String saveName = \"1.png\"; System.out.println(FTPTools.upload( hostname, port, username, password, workingPath, fileInputStream, saveName)); } } 如果只是内网下使用NAS,那么可以直接输入NAS IP+端口 ,这样速度很快。 外网就需要映射的url+端口了!","categories":[{"name":"对象存储","slug":"对象存储","permalink":"http://www.aquestian.cn/categories/%E5%AF%B9%E8%B1%A1%E5%AD%98%E5%82%A8/"}],"tags":[{"name":"FTP","slug":"ftp","permalink":"http://www.aquestian.cn/tags/ftp/"},{"name":"NAS","slug":"nas","permalink":"http://www.aquestian.cn/tags/nas/"}],"author":"aqian666"},{"title":"Webservice运用","slug":"blogs/Java-Webservice","date":"2020-11-14T16:00:00.000Z","updated":"2020-11-14T16:00:00.000Z","comments":true,"path":"blogs/Java-Webservice/","link":"","permalink":"http://www.aquestian.cn/blogs/Java-Webservice/","excerpt":"","text":"什么是WebserviceWebService是一个SOA(面向服务的编程)的架构,它是不依赖于语言,不依赖于平台,可以实现不同的语言间的相互调用,通过Internet进行基于Http协议的网络应用间的交互。 其实WebService并不是什么神秘的东西,它就是一个可以远程调用的类,或者说是组件,把你本地的功能开放出去共别人调用。 ## HttpClient和WebService的区别 二者都是调用对方服务接口,区别在于: HttpClient用来调用服务,它是模拟一个浏览器,发送Http的请求,服务器会返回请求的一个响应结果,Httpclient然后把响应的结果取出来。HttpClinet相当于一个客户端,使用Http协议调用系统中的方法或者接口。 webService是使用soap协议而不是Http协议。 什么是soap协议 SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换。或者更简单地说:SOAP 是用于访问网络服务的协议。 SOAP 消息实列: <?xml version=\"1.0\"?> <soap:Envelope xmlns:soap=\"http://www.w3.org/2001/12/soap-envelope\" soap:encodingStyle=\"http://www.w3.org/2001/12/soap-encoding\"> <soap:Header> ... ...</soap:Header> <soap:Body> ... ... <soap:Fault> ... ... </soap:Fault></soap:Body> </soap:Envelope> xml元素详解:https://www.runoob.com/soap/soap-intro.html SpringBoot使用CXF集成WebService添加依赖 <!--cxf--> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxws</artifactId> <version>3.1.6</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http</artifactId> <version>3.1.6</version> </dependency> <!--axis--> <dependency> <groupId>org.apache.axis</groupId> <artifactId>axis</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>org.apache.axis</groupId> <artifactId>axis-jaxrpc</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>commons-discovery</groupId> <artifactId>commons-discovery</artifactId> <version>0.2</version> </dependency> 创建服务端接口@WebService(name = \"DemoWebService\", // 暴露服务名称 targetNamespace = \"http://service.webservicedemo.example.com\"// 命名空间,一般是接口的包名倒序 ) public interface DemoWebService { String sayHello(String message); } 服务端接口实现@WebService( serviceName = \"DemoService\", // 与接口中指定的name一致 targetNamespace = \"http://service.webservicedemo.example.com\", // 与接口中的命名空间一致,一般是接口的包名倒 endpointInterface = \"com.example.webservicedemo.service.DemoWebService\" // 接口地址 ) public class DemoWebServiceImpl implements DemoWebService { @Override public String sayHello(String message) { return message+\",现在时间:\"+\"(\"+new Date()+\")\"; } } CXF配置@Configuration public class CxfConfig { @Bean public ServletRegistrationBean createServletRegistrationBean() { return new ServletRegistrationBean(new CXFServlet(),\"/demo/*\"); } @Bean(name = Bus.DEFAULT_BUS_ID) public SpringBus springBus() { return new SpringBus(); } @Bean public DemoWebService demoService() { return new DemoWebServiceImpl(); } @Bean public Endpoint endpoint() { EndpointImpl endpoint = new EndpointImpl(springBus(), demoService()); endpoint.publish(\"/api\"); return endpoint; } } 启动SpringBoot服务,输入http://localhost:8090/demo/api?wsdl即可。 使用单元测试模拟客户端使用cxf模拟请求@SpringBootTest class WebServiceDemoApplicationTests { @Test void contextLoads1() { //创建动态客户端 JaxWsDynamicClientFactory factory = JaxWsDynamicClientFactory.newInstance(); Client client = factory.createClient(\"http://localhost:8090/demo/api?wsdl\"); // 需要密码的情况需要加上用户名和密码 //client.getOutInterceptors().add(new ClientLoginInterceptor(USER_NAME,PASS_WORD)); HTTPConduit conduit = (HTTPConduit) client.getConduit(); HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy(); httpClientPolicy.setConnectionTimeout(2000); //连接超时 httpClientPolicy.setAllowChunking(false); //取消块编码 httpClientPolicy.setReceiveTimeout(120000); //响应超时 conduit.setClient(httpClientPolicy); //client.getOutInterceptors().addAll(interceptors);//设置拦截器 try{ Object[] objects = new Object[0]; // invoke(\"方法名\",参数1,参数2,参数3....); objects = client.invoke(\"sayHello\", \"xxx\"); System.out.println(\"返回数据:\" + objects[0]); }catch (Exception e){ e.printStackTrace(); } } } 运行结果 使用axis模拟请求@SpringBootTest class WebServiceDemoApplicationTests { @Test void contextLoads2() throws ServiceException, RemoteException, MalformedURLException { Service service = new Service(); Call call = (Call) service.createCall(); call.setTargetEndpointAddress(new URL(\"http://localhost:8090/demo/api?wsdl\")); call.setOperationName(new QName(\"http://service.webservicedemo.example.com\",\"sayHello\")); // call.setUseSOAPAction(true); // call.setSOAPActionURI(\"http://service.webservicedemo.example.com\"+\"sayHello\"); call.addParameter(new QName(\"http://service.webservicedemo.example.com\", \"message\"), XMLType.XSD_STRING, ParameterMode.IN); call.setReturnType(XMLType.XSD_STRING); call.setTimeout(10000); call.setEncodingStyle(\"utf-8\"); //设置命名空间和需要调用的方法名 Object invoke = call.invoke(new Object[]{\"xxx\"}); System.out.println(invoke.toString()); } } 运行时,会报一个元素错误。 需要暴露服务端的接口以及参数。 @WebService(name = \"DemoWebService\", // 暴露服务名称 targetNamespace = \"http://service.webservicedemo.example.com\"// 命名空间,一般是接口的包名倒序 ) public interface DemoWebService { //暴露接口 参数 @WebMethod String sayHello(@WebParam(name = \"message\",targetNamespace = \"http://service.webservicedemo.example.com\") String message); } 运行结果","categories":[{"name":"Java实例","slug":"java实例","permalink":"http://www.aquestian.cn/categories/java%E5%AE%9E%E4%BE%8B/"}],"tags":[{"name":"Webservice","slug":"webservice","permalink":"http://www.aquestian.cn/tags/webservice/"}],"author":"aqian666"},{"title":"Iview动态编辑标签","slug":"blogs/Iview-Tags","date":"2020-09-16T16:00:00.000Z","updated":"2020-09-16T16:00:00.000Z","comments":true,"path":"blogs/Iview-Tags/","link":"","permalink":"http://www.aquestian.cn/blogs/Iview-Tags/","excerpt":"","text":">写在前面在iview的文档中呢,提供了标签的动态使用 ——>点这里看iview标签,但它是不可编辑的,所以我写个文章记录一下,方便刚学习vue的小伙伴,直接使用,毕竟我开始也是不会到会。 首先,我给大家看一下我最初做的样子,我是写死的,然后进行点选。 这个效果不是我想要的 实现iview 原生封装<iv-tag v-for=\"item in countTags\" :key=\"item\" :name=\"item\" closable @on-close=\"handleClose\">{{ item }} </iv-tag > //tag初始化参数 countTags: ['Java', 'Vue'], 实现方法 //标签部分 handleClose(tag) { this.countTags.splice(this.countTags.indexOf(tag), 1); }, 注:iview中你使用tag,不是iv-tag,,我这个是自定义引入,只使用我需要的iview插件。 如上代码段,遍历标签,不多解释,加了一个可关闭事件。参照iview中的官方文档,跟我这个大同小异。 动态标签实现在iv-tag标签内写一个input。代码如下。 <iv-input class=\"input-new-tag\" v-if=\"inputVisible\" v-model=\"inputValue\" ref=\"saveTagInput\" size=\"small\" @keyup.enter.native=\"handleInputConfirm\" @blur=\"handleInputConfirm\" > //样式,写到你的style里 .input-new-tag { width: 80px; margin-left: 0px; } 再加俩个初始参数, inputVisible: false, inputValue: '', 一个回车事件,一个元素失去焦点时所触发的事件,触发同一个事件。 handleInputConfirm() { let inputValue = this.inputValue; if (inputValue) { this.countTags.push(inputValue); } this.inputVisible = false; this.inputValue = ''; }, 再在input下加一个button,使用ref引用上面的input,点击出现。 <iv-button v-else size=\"small\" type=\"dashed\" icon=\"ios-add\" @click=\"showInput\">+ 添加标签</iv-button> showInput() { this.inputVisible = true; this.$nextTick(_ => { this.$refs.saveTagInput.$refs.input.focus(); }); }, 注意输入完成保存是回车保存。 效果","categories":[{"name":"Vue","slug":"vue","permalink":"http://www.aquestian.cn/categories/vue/"}],"tags":[{"name":"Vue","slug":"vue","permalink":"http://www.aquestian.cn/tags/vue/"},{"name":"Iview","slug":"iview","permalink":"http://www.aquestian.cn/tags/iview/"}],"author":"aqian666"},{"title":"Mybatis一级缓存二级缓存","slug":"blogs/Mybatis-Session","date":"2020-08-26T16:00:00.000Z","updated":"2020-08-26T16:00:00.000Z","comments":true,"path":"blogs/Mybatis-Session/","link":"","permalink":"http://www.aquestian.cn/blogs/Mybatis-Session/","excerpt":"","text":"一级缓存在mybatis中是默认开启的,一级缓存是单个session级别的,只在一次会话中有效,一个SqlSession对象中创建一个本地缓存,对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回;否则,从数据库读取数据,将查询结果存入缓存并返回。 一级缓存失效 会话结束,缓存失效。 SqlSession调用了close(),会释放掉一级缓存PerpetualCache对象,一级缓存失效。 SqlSession调用了clearCache(),会清除缓存PerpetualCache对象中的数据,缓存失效。 SqlSession执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。 使用一级缓存首先得确保俩次执行的sql语句是一致的。 此处多余代码不再赘述,只贴出关键代码。 @Resource private SqlSessionFactory factory; public User selectById() { SqlSession sqlSession = factory.openSession(); System.err.println("第一次执行"); UserMapper userMapper1 = sqlSession.getMapper(UserMapper.class); System.err.println(userMapper1.selectByUserId(1).toString()); System.err.println("第二次执行"); UserMapper userMapper2 = sqlSession.getMapper(UserMapper.class); System.err.println(userMapper2.selectByUserId(1).toString());} 执行结果如下 可以发现,sql只执行了一次,第二次并没有执行还是可以得到同样的user对象。那么此时这个session会话已经结束了,或许你会跟我有疑问谁会这样傻逼的这么写代码呢?我已经得到了userId为1的用户了,我在同一个方法里不可能再去写一遍查询id为1的用户。目前我资历尚欠,实际开发中也没有遇到这种一级缓存例子,所以这个实际应用还需要多方面看一下。 二级缓存SqlSessionFactory层面上的二级缓存默认是不开启的,二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的。 也就是要求实现Serializable接口,配置方法很简单,只需要在映射XML文件配置就可以开启缓存了,如果我们配置了二级缓存就意味着: 映射语句文件中的所有select语句将会被缓存。 映射语句文件中的所欲insert、update和delete语句会刷新缓存。 缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。 根据时间表,比如No Flush Interval,(CNFI没有刷新间隔),缓存不会以任何时间顺序来刷新。 缓存会存储列表集合或对象(无论查询方法返回什么)的1024个引用 缓存会被视为是read/write(可读/可写)的缓存,意味着对象检索不是共享的,而且可以安全的被调用者修改,不干扰其他调用者或线程所做的潜在修改。 二级缓存开启mybatis.configuration.cache-enabled=true <mapper namespace=\"com.example.demo.mapper.UserMapper\"> <!--开启mybatis二级缓存--> <cache/> <resultMap id=\"BaseResultMap\" type=\"com.example.demo.bean.User\"> <id column=\"id\" property=\"id\" /> <result column=\"username\" property=\"username\" /> <result column=\"password\" property=\"password\" /> </resultMap> <select id=\"selectByUserId\" parameterType=\"int\" resultMap=\"BaseResultMap\"> select * from user where id = #{id} </select> </mapper> 使用二级缓存@Resource private UserMapper userMapper; public User selectById() { System.err.println(\"第一次执行\"); System.err.println(userMapper.selectByUserId(1).toString()); System.err.println(\"第二次执行\"); System.err.println(userMapper.selectByUserId(1).toString()); return null; } 这一次调用注入的mapper即可。 执行效果如下 可以看到创建了一个新的SqlSession,执行了一次sql,在不开启缓存的情况下,肯定是要执行俩次sql的。 再次执行 此时已经不在和数据库发生交互,这样在实际项目就会大大减轻数据库的压力。 以上就是mybatis缓存。","categories":[{"name":"Mybatis","slug":"mybatis","permalink":"http://www.aquestian.cn/categories/mybatis/"}],"tags":[{"name":"Mybatis","slug":"mybatis","permalink":"http://www.aquestian.cn/tags/mybatis/"}],"author":"aqian666"},{"title":"Java线程安全与不安全","slug":"blogs/Java-Thread","date":"2020-08-14T16:00:00.000Z","updated":"2020-08-14T16:00:00.000Z","comments":true,"path":"blogs/Java-Thread/","link":"","permalink":"http://www.aquestian.cn/blogs/Java-Thread/","excerpt":"","text":"线程安全与不安全的理解最常说的例子,用户取钱:假设A和B同时去不同ATM上取同一张账户的1000块钱,如果是线程不安全,那么A和B同时取钱时,就可能出现俩人都取到1000块钱,那么这俩人就发财了,而如果线程安全呢,就只有一个人能取出来1000块钱,另外一个人再取就是余额不足。 代码实现实现上述取钱的例子 创建一个账户类 // 银行账户类 public class Account { // private final Lock lock=new ReentrantLock(); // 余额 private double money =1000; public double getMoney() { return money; } public Account() { } // 取钱 /* 在实例方法上使用synchronized,锁的一定是this对象。 这种方式不灵活,另外表示整个方法都需要同步,可能会无故扩大同步的范围。 导致程序的效率降低。所以这种方式不常用。 synchronized使用在实例方法上有什么优点? 就是代码比较少,写一个synchronized关键字就行。 如果共享的对象就是this,并且需要同步的代码是整个方法体,建议在实例方法上 添加synchronized关键字修饰,因为需要同步的确实是整个方法体。 */ // 也可以在实例方法上,加synchronized,这样就扩大了安全的范围,同样效率就变低了 // public synchronized void withdraw(int m) { public void withdraw(int m) { //lock.lock(); // 以下代码是需要线程排队的 //synchronized (this) { // 括号里的参数传一个对象,只要对象必须是线程所共享的就行,也可以不是this // 模拟网络延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.money = this.money - m; //lock.unlock(); //} } } 上述的例子注释中有实现锁的三种方式,都可以实现线程安全,现在先不开启,后面对比一下开启与不开启的区别,就可以更能理解的看到线程安全与不安全了。 创建一个线程运行类,可以理解为不同的取钱点。 // 线程运行类 public class AccountThread implements Runnable { // 线程共享一个账户 private Account account; // 取钱的数目 private int money; public AccountThread(Account account, int money) { this.account = account; this.money = money; } @Override public void run() { // 开始取钱 account.withdraw(money); System.out.println(Thread.currentThread().getName() + \"取钱\" + money + \"元成功,剩余\" + account.getMoney() + \"元。\"); } } 测试 public class Test01 { public static void main(String[] args) { // 创建银行账户,里边初始有1000 Account account = new Account(); // 俩个地点取钱 AccountThread at1 = new AccountThread(account, 200); AccountThread at2 = new AccountThread(account, 100); Thread t1 = new Thread(at1); Thread t2 = new Thread(at2); // 设置name t1.setName(\"t1\"); t2.setName(\"t2\"); // 启动线程,开始取钱 t1.start(); t2.start(); } } 线程非安全下运行结果: 多运行几次,会出现时而取钱正确,时而取钱错误。 线程安全下运行结果: 开启账户类中的任意一种(有三种方式:分别是 同步代码块 、同步方法和锁机制(Lock))线程安全的方式,即可保证输出无误。","categories":[{"name":"Java实例","slug":"java实例","permalink":"http://www.aquestian.cn/categories/java%E5%AE%9E%E4%BE%8B/"}],"tags":[{"name":"线程","slug":"线程","permalink":"http://www.aquestian.cn/tags/%E7%BA%BF%E7%A8%8B/"}],"author":"aqian666"},{"title":"SpringBoot整合Elasticsearch","slug":"blogs/SpringBoot-Elasticsearch","date":"2020-06-30T16:00:00.000Z","updated":"2020-06-30T16:00:00.000Z","comments":true,"path":"blogs/SpringBoot-Elasticsearch/","link":"","permalink":"http://www.aquestian.cn/blogs/SpringBoot-Elasticsearch/","excerpt":"","text":"写在前面Docker搭建elaseticsearch 先看这篇文章完成Docker搭建elaseticsearch 之前看过一篇大佬文章,但是文章现在需要vip才可以查看,所以我重新总结一下。windows的小伙伴也可以安装docker,或者直接安装elaseticsearch也是可以的,yml配置需要改一下。 springboot整合elasticsearchpom引入 <!--elasticsearch--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-elasticsearch</artifactId> <version>3.1.6.RELEASE</version> <scope>compile</scope> </dependency> yml配置 spring: data: elasticsearch: cluster-name: docker-cluster #集群名称 cluster-nodes: ip:9300 #配置es节点信息,逗号分隔,如果没有指定,则启动ClientNode properties: path: logs: ./elasticsearch/log #elasticsearch日志存储目录 data: ./elasticsearch/data #elasticsearch数据存储目录 配置只需这么些,接下来就写一些demo来玩一下elaseticsearch 构建Item类 @Document(indexName = \"item\",type = \"docs\", shards = 1, replicas = 0) public class Item { @Id private Long id; //文章使用分词器 @Field(type = FieldType.Text, analyzer = \"ik_max_word\") private String title; //标题 @Field(type = FieldType.Keyword) private String category;// 分类 @Field(type = FieldType.Keyword) private String brand; // 品牌 @Field(type = FieldType.Double) private Double price; // 价格 } 创建ItemRepository并继承ElasticsearchRepository,有兴趣的可以看一下底层源码 public interface ItemRepository extends ElasticsearchRepository<Item,Long>{ /** * @Description:根据价格区间查询 自定义查询 * @Param price1 * @Param price2 */ List<Item> findByPriceBetween(double price1, double price2); List<Item> findByTitle(String title1); List<Item> findByTitleIn(Collection<String> ss); } 创建索引 删除索引@RunWith(SpringRunner.class) @SpringBootTest(classes = BootApplication.class) public class EsDemoApplicationTest{ @Autowired private ElasticsearchTemplate elasticsearchTemplate; /** * @Description:创建索引,会根据Item类的@Document注解信息来创建 */ @Test public void testCreateIndex() { elasticsearchTemplate.createIndex(Item.class); } /** * @Description:删除索引 */ @Test public void testDeleteIndex() { elasticsearchTemplate.deleteIndex(Item.class); } } 先执行创建索引 索引数据操作此处不多描述,注解已经写明。此处再次贴出原作者经典文章https://blog.csdn.net/chen_2890/article/details/83895646#t7 @RunWith(SpringRunner.class) @SpringBootTest(classes = BootApplication.class) public class ceshiTest { @Autowired private ItemRepository itemRepository; /** * @Description:定义新增方法 */ @Test public void insert() { Item item = new Item(1L, \"小米手机7\", \" 手机\", \"小米\", 3499.00); itemRepository.save(item); } /** * @Description:定义批量新增方法 */ @Test public void insertList() { List<Item> list = new ArrayList<>(); list.add(new Item(1L, \"小米9\", \"手机\", \"小米\", 3299.00)); list.add(new Item(2L, \"华为pro30\", \"手机\", \"华为\", 3999.00)); list.add(new Item(3L, \"一加7\", \"手机\", \"一加\", 2999.00)); list.add(new Item(4L, \"魅族16\", \"手机\", \"魅族\", 1999.00)); list.add(new Item(5L, \"苹果xs\", \"手机\", \"苹果\", 5099.00)); list.add(new Item(6L, \"360pro\", \"手机\", \"360\", 1099.00)); list.add(new Item(7L, \"荣耀V10\", \"手机\", \"华为\", 899.00 )); // 接收对象集合,实现批量新增 itemRepository.saveAll(list); } /** * @Description:按照价格区间查询 自定义方法 * 自定义方法 Spring Data 的另一个强大功能,是根据方法名称自动实现功能。 比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。 当然,方法名称要符合一定的约定 下边为约定 And findByNameAndPrice Or findByNameOrPrice Is findByName Not findByNameNot Between findByPriceBetween LessThanEqual findByPriceLessThan GreaterThanEqual findByPriceGreaterThan Before findByPriceBefore After findByPriceAfter Like findByNameLike StartingWith findByNameStartingWith EndingWith findByNameEndingWith Contains/Containing findByNameContaining In findByNameIn(Collection<String>names) NotIn findByNameNotIn(Collection<String>names) Near findByStoreNear True findByAvailableTrue False findByAvailableFalse OrderBy findByAvailableTrueOrderByNameDesc * @Author: https://blog.csdn.net/chen_2890 */ @Test public void queryByPriceBetween(){ List<Item> list = this.itemRepository.findByPriceBetween(2000.00, 3500.00); for (Item item : list) { System.out.println(\"item = \" + item.getTitle()); } } @Test public void queryByTitle(){ List<Item> list = this.itemRepository.findByTitle(\"华为\"); for (Item item : list) { System.out.println(\"item = \" + item.getTitle()); } } @Test public void queryByTitleTo(){ Collection<String> ss = new ArrayList<>(); ss.add(\"华为\"); ss.add(\"小米\"); List<Item> list = this.itemRepository.findByTitleIn(ss); for (Item item : list) { System.out.println(\"item = \" + item.getTitle()); } } /** * @Description:matchQuery底层采用的是词条匹配查询 * @Author: https://blog.csdn.net/chen_2890 */ @Test public void testMatchQuery(){ // 构建查询条件 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 添加基本分词查询 queryBuilder.withQuery(QueryBuilders.matchQuery(\"title\", \"华为\")); // 搜索,获取结果 Page<Item> items = this.itemRepository.search(queryBuilder.build()); // 总条数 long total = items.getTotalElements(); System.out.println(\"获取的总条数 = \" + total); for (Item item : items) { System.out.println(\"手机名称是:\"+item.getTitle()); } } /** * @Description: * termQuery:功能更强大,除了匹配字符串以外,还可以匹配 * int/long/double/float/.... * @Author: https://blog.csdn.net/chen_2890 */ @Test public void testTermQuery(){ NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery(QueryBuilders.termQuery(\"price\",1099)); // 查找 Page<Item> page = this.itemRepository.search(builder.build()); for(Item item:page){ System.out.println(\"手机是:\"+item.getTitle()); } } /** * @Description:布尔查询 多条件查询 * @Author: https://blog.csdn.net/chen_2890 */ @Test public void testBooleanQuery(){ NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery( QueryBuilders.boolQuery().must(QueryBuilders.matchQuery(\"title\",\"华为\")) .must(QueryBuilders.matchQuery(\"brand\",\"华为\")) ); // 查找 Page<Item> page = this.itemRepository.search(builder.build()); for(Item item:page){ System.out.println(\"手机名称是\"+item.getTitle()); } } /** * @Description:布尔查询 多条件查询 * @Author: https://blog.csdn.net/chen_2890 */ @Test public void testBlQuery(){ NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery( QueryBuilders.boolQuery().must(QueryBuilders.matchQuery(\"title\",\"荣耀\")) .must(QueryBuilders.matchQuery(\"title\",\"华为\")) ); // 查找 Page<Item> page = this.itemRepository.search(builder.build()); for(Item item:page){ System.out.println(\"手机名称是\"+item.getTitle()); } } /** * @Description:模糊查询 * @Author: https://blog.csdn.net/chen_2890 */ @Test public void testFuzzyQuery(){ NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder(); builder.withQuery(QueryBuilders.fuzzyQuery(\"title\",\"一\")); Page<Item> page = this.itemRepository.search(builder.build()); for(Item item:page){ System.out.println(\"手机名称是:\"+item.getTitle()); } } /** * @Description:分页查询 * @Author: https://blog.csdn.net/chen_2890 */ @Test public void searchByPage(){ // 构建查询条件 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 添加基本分词查询 queryBuilder.withQuery(QueryBuilders.termQuery(\"category\", \"手机\")); // 分页: int page = 0; int size = 2; queryBuilder.withPageable(PageRequest.of(page,size)); // 搜索,获取结果 Page<Item> items = this.itemRepository.search(queryBuilder.build()); // 总条数 long total = items.getTotalElements(); System.out.println(\"总条数 = \" + total); // 总页数 System.out.println(\"总页数 = \" + items.getTotalPages()); // 当前页 System.out.println(\"当前页:\" + items.getNumber()); // 每页大小 System.out.println(\"每页大小:\" + items.getSize()); for (Item item : items) { System.out.println(item.getTitle()); } } /** * @Description:排序查询 * @Author: https://blog.csdn.net/chen_2890 */ @Test public void searchAndSort(){ // 构建查询条件 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 添加基本分词查询 queryBuilder.withQuery(QueryBuilders.termQuery(\"category\", \"手机\")); // 排序 queryBuilder.withSort(SortBuilders.fieldSort(\"price\").order(SortOrder.DESC)); // 搜索,获取结果 Page<Item> items = this.itemRepository.search(queryBuilder.build()); // 总条数 long total = items.getTotalElements(); System.out.println(\"总条数 = \" + total); for (Item item : items) { System.out.println(\"手机的价格是:\"+item.getTitle()+\":\"+item.getPrice()); } } /** * @Description:按照品牌brand进行分组 * @Author: https://blog.csdn.net/chen_2890 */ @Test public void testAgg(){ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 不查询任何结果 queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{\"\"}, null)); // 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand queryBuilder.addAggregation( AggregationBuilders.terms(\"brands\").field(\"brand\")); // 2、查询,需要把结果强转为AggregatedPage类型 AggregatedPage<Item> aggPage = (AggregatedPage<Item>) this.itemRepository.search(queryBuilder.build()); // 3、解析 // 3.1、从结果中取出名为brands的那个聚合, // 因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型 StringTerms agg = (StringTerms) aggPage.getAggregation(\"brands\"); // 3.2、获取桶 List<StringTerms.Bucket> buckets = agg.getBuckets(); // 3.3、遍历 for (StringTerms.Bucket bucket : buckets) { // 3.4、获取桶中的key,即品牌名称 System.out.println(bucket.getKeyAsString()); // 3.5、获取桶中的文档数量 System.out.println(bucket.getDocCount()); } } /** * @Description:嵌套聚合,求平均值 * @Author: https://blog.csdn.net/chen_2890 */ @Test public void testSubAgg(){ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 不查询任何结果 queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{\"\"}, null)); // 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand queryBuilder.addAggregation( AggregationBuilders.terms(\"brands\").field(\"brand\") .subAggregation(AggregationBuilders.avg(\"priceAvg\").field(\"price\")) // 在品牌聚合桶内进行嵌套聚合,求平均值 ); // 2、查询,需要把结果强转为AggregatedPage类型 AggregatedPage<Item> aggPage = (AggregatedPage<Item>) this.itemRepository.search(queryBuilder.build()); // 3、解析 // 3.1、从结果中取出名为brands的那个聚合, // 因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型 StringTerms agg = (StringTerms) aggPage.getAggregation(\"brands\"); // 3.2、获取桶 List<StringTerms.Bucket> buckets = agg.getBuckets(); // 3.3、遍历 for (StringTerms.Bucket bucket : buckets) { // 3.4、获取桶中的key,即品牌名称 3.5、获取桶中的文档数量 System.out.println(bucket.getKeyAsString() + \",共\" + bucket.getDocCount() + \"台\"); // 3.6.获取子聚合结果: InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get(\"priceAvg\"); System.out.println(\"平均售价:\" + avg.getValue()); } } } 大致的demo就是以上的情况。 问题但是项目在启动过程中回报一个错误, Timeout connecting to [localhost/127.0.0.1:9200] 明明已经连接到远端es,但还会出现这个问题。 解决, spring: data: elasticsearch: cluster-name: docker-cluster cluster-nodes: ip:9300 #配置es节点信息,逗号分隔,如果没有指定,则启动ClientNode properties: path: logs: ./elasticsearch/log #elasticsearch日志存储目录 data: ./elasticsearch/data #elasticsearch数据存储目录 elasticsearch: rest: uris: [\"ip:9200\"] 即可。","categories":[{"name":"Elasticsearch","slug":"elasticsearch","permalink":"http://www.aquestian.cn/categories/elasticsearch/"}],"tags":[{"name":"Elasticsearch","slug":"elasticsearch","permalink":"http://www.aquestian.cn/tags/elasticsearch/"},{"name":"SpringBoot","slug":"springboot","permalink":"http://www.aquestian.cn/tags/springboot/"}],"author":"aqian666"},{"title":"Docker搭建Elasticsearch","slug":"blogs/Docker-Elasticsearch","date":"2020-06-09T16:00:00.000Z","updated":"2020-06-09T16:00:00.000Z","comments":true,"path":"blogs/Docker-Elasticsearch/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-Elasticsearch/","excerpt":"","text":"Docker搭建elasticsearch#拉取镜像 docker pull elasticsearch:6.5.4 #启动镜像 docker run --name elasticsearch -d -e ES_JAVA_OPTS="-Xms512m -Xmx512m" -p 9200:9200 -p 9300:9300 elasticsearch:6.5.4 --name表示镜像启动后的容器名称 -d: 后台运行容器,并返回容器ID; -e: 指定容器内的环境变量 -p: 指定端口映射,格式为:主机(宿主)端口:容器端口 这样docker就完成安装elasticsearch! elasticsearch错误但是我服务器elasticsearch在昨天突然挂掉了,启动时报错 ERROR: [1] bootstrap checks failed [1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144] 解决办法#切换到root用户修改sysctl.conf文件 vi /etc/sysctl.conf #添加配置 vm.max_map_count=655360 #保存退出 :wq #执行命令 sysctl -p Docker搭建elasticsearch-head#拉取镜像 docker pull mobz/elasticsearch-head:5 #创建容器 docker create --name elasticsearch-head -p 9100:9100 mobz/elasticsearch-head:5 #启动容器 docker start elasticsearch-head or docker start 容器id (docker ps -a 查看容器id ) 浏览器打开: http://IP:9100 尝试连接easticsearch会发现无法连接上,由于是前后端分离开发,所以会存在跨域问题,需要在服务端做CORS的配置。 解决办法 修改docker中elasticsearch的elasticsearch.yml文件 docker exec -it elasticsearch /bin/bash (进不去使用容器id进入) vi config/elasticsearch.yml 在最下面添加2行 http.cors.enabled: true http.cors.allow-origin: "*" 退出并重启服务 exit docker restart 容器id 安装ik分词器这里采用离线安装 下载分词器压缩包 下载地址: https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.5.4/elasticsearch-analysis-ik-6.5.4.zip 将IK分词器上传到/tmp目录中(xftp) 将分词器安装进容器中 #将压缩包移动到容器中 docker cp /tmp/elasticsearch-analysis-ik-6.5.4.zip elasticsearch:/usr/share/elasticsearch/plugins #进入容器docker exec -it elasticsearch /bin/bash #创建目录mkdir /usr/share/elasticsearch/plugins/ik #将文件压缩包移动到ik中mv /usr/share/elasticsearch/plugins/elasticsearch-analysis-ik-6.5.4.zip /usr/share/elasticsearch/plugins/ik #进入目录cd /usr/share/elasticsearch/plugins/ik #解压unzip elasticsearch-analysis-ik-6.5.4.zip #删除压缩包rm -rf elasticsearch-analysis-ik-6.5.4.zip 退出并重启镜像 给大家个建议,这玩意装与不装都挺好,不装查到的东西也很精确够使,装上会查出一些没有用的!","categories":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/categories/docker/"}],"tags":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/tags/docker/"},{"name":"Elasticsearch","slug":"elasticsearch","permalink":"http://www.aquestian.cn/tags/elasticsearch/"}],"author":"aqian666"},{"title":"Java反射","slug":"blogs/Java-Reflection","date":"2020-04-14T16:00:00.000Z","updated":"2020-04-14T16:00:00.000Z","comments":true,"path":"blogs/Java-Reflection/","link":"","permalink":"http://www.aquestian.cn/blogs/Java-Reflection/","excerpt":"","text":"感谢这篇博客:https://blog.csdn.net/ju_362204801/article/details/90578678,让我对反射有了新的理解。 写在前面什么是反射?反射就是就是把Java类中的各个部分(成员变量,方法,构造方法,等),映射成一个个的Java对象,可以进行任意调用。打个贴合文章标题的比方,new一个对象,这个对象就是一个美女,她告诉你她今天传了一件性感内衣,然后啥都没告诉你,这时候你灵光一现使用反射即可了解她穿这件内衣始末!下面会通过代码解释! 了解美女对象穿内衣始末创建实体类/** * 创建一个美女对象类 */ public class Beauty { /** * 对象名字 */ public String name; /** * 对象穿内衣原因 */ private String reason; /** * 内衣大小 */ private double size; public Beauty() { System.err.println("无参构造:穿内衣"); } public Beauty(String name, String reason, double size) { this.name = name; this.reason = reason; this.size = size; } public String getName() { return name; } public void setName(String name) { this.name=name; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } public double getSize() { return size; } public void setSize(double size) { this.size = size; } } 1.无参构造:public class Test { public static void main(String[] args) throws Exception { //传统创建一个对象 Beauty customer = new Beauty(); //利用反射创建一个对象 Class clazz = Class.forName(customer.getClass().getName()); //调用无参构造 clazz.getConstructor(null).newInstance(null); } } 从写法上来,好像下边的要麻烦一些。运行结果: 2.有参构造://调用Customer的有参构造 Beauty customer1 = new Beauty("佟丽娅","穿内衣给我老公看",36d); System.out.println(customer1.getName()); //利用反射调用有参构造 customer = (Beauty)clazz.getConstructor(String.class,String.class,double.class).newInstance("高圆圆","穿内衣给我老公看",36d); System.out.println(\"谁在传内衣给他对象看:\"+customer.getName()); 都是穿内衣给他老公看。还是反射麻烦些。 3.获取属性值:为了更好的区分,此时需要在美女类加一个私有属性。 public String love1 = "陈思诚"; private String love2 = "赵又廷"; //传统获取属性值,获取不到2 System.out.println("让我看看她们在想谁"); System.err.println(customer1.love1); //暴力获取私有属性 Field love2 = clazz.getDeclaredField("love2"); love2.setAccessible(true); System.err.println(""+love2.get(customer)); 利用反射暴力获取值,发现他俩想的都不是你!!! 4.给属性赋值让俩美女都想你! //赋值 customer1.love1="我自己"; System.err.println(customer1.love1); Field f = clazz.getDeclaredField("love2");f.setAccessible(true);f.set(customer,"我自己");System.err.println(""+f.get(customer)); 开心吗?都是你了! ### 5.调用方法 //调用get,set方法 customer.setName("佟丽娅"); System.err.println(customer.getName()); //反射调用set方法Method clazzMethod = clazz.getMethod("setName", String.class);clazzMethod.invoke(customer,"高圆圆");//反射调用get方法Method m = clazz.getMethod("getName", null);String returnValue = (String)m.invoke(customer, null);System.err.println(returnValue); 此处情况众多,请参考文章https://img-blog.csdnimg.cn/20200806174448138.png,按需分析情况。 为了更好的贴合主题,举例一种! 6.贴合主题的例子在美女对象中加入 private void getStr(String[] name,int password){ for (int i=0 ;i<name.length;i++){ if (password==123456) { System.err.println(name[i] + ":其实我真正爱的是隔壁老王"); }else{ System.err.println("密码输入错误,你看不到我心里的想法"); } } } 私有方法,传统方式没法子调用。 Method m = clazz.getDeclaredMethod("getStr", String[].class,int.class); m.setAccessible(true); m.invoke(customer, (Object)new String[]{"佟丽娅","高圆圆"},123456); 看到这里你可能已经流泪了!那么接下来就是更流泪的事。 //获得对象的所有属性 Field fields[]=clazz.getDeclaredFields(); for (int i=0; i<fields.length;i++){ Field field=fields[i]; String fieldName=field.getName(); System.out.println("具体属性参数:"+fieldName); } //获得对象的所有方法 Method[] declaredMethods = clazz.getDeclaredMethods(); for (int i=0; i<declaredMethods.length;i++){ Method methods = declaredMethods[i]; String methodName=methods.getName(); System.out.println("所有方法名称:"+methodName); } 这就好比你的女朋友心里想的不仅不是你,而且她穿的什么颜色的内衣已经他们嘿嘿嘿的方式,都应经被你知道了,你现在是不是觉得头上有点奇怪的东西! 映射封装工具类分享网上找的,这个算是很全了! /** * 反射工具类. * 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. * @author aqian666 * @version */ public class ReflectUtils { private static final String SETTER_PREFIX = "set"; private static final String GETTER_PREFIX = "get"; private static final String CGLIB_CLASS_SEPARATOR = "$$"; private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class); /** * 调用Getter方法. * 支持多级,如:对象名.对象名.方法 */ public static Object invokeGetter(Object obj, String propertyName) { Object object = obj; for (String name : StringUtils.split(propertyName, ".")){ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); } return object; } /** * 调用Setter方法, 仅匹配方法名。 * 支持多级,如:对象名.对象名.方法 */ public static void invokeSetter(Object obj, String propertyName, Object value) { Object object = obj; String[] names = StringUtils.split(propertyName, "."); for (int i=0; i<names.length; i++){ if(i<names.length-1){ String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]); object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); }else{ String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]); invokeMethodByName(object, setterMethodName, new Object[] { value }); } } } /** * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数. */ public static Object getFieldValue(final Object obj, final String fieldName) { Field field = getAccessibleField(obj, fieldName); if (field == null) { throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + obj + "]"); } Object result = null; try { result = field.get(obj); } catch (IllegalAccessException e) { logger.error("不可能抛出的异常{}", e.getMessage()); } return result; } /** * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数. */ public static void setFieldValue(final Object obj, final String fieldName, final Object value) { Field field = getAccessibleField(obj, fieldName); if (field == null) { throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + obj + "]"); } try { field.set(obj, value); } catch (IllegalAccessException e) { logger.error("不可能抛出的异常:{}", e.getMessage()); } } /** * 直接调用对象方法, 无视private/protected修饰符. * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用. * 同时匹配方法名+参数类型, */ public static Object invokeMethod(final Object obj, final String methodName, final Class<?>[] parameterTypes, final Object[] args) { Method method = getAccessibleMethod(obj, methodName, parameterTypes); if (method == null) { throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + obj + "]"); } try { return method.invoke(obj, args); } catch (Exception e) { throw convertReflectionExceptionToUnchecked(e); } } /** * 直接调用对象方法, 无视private/protected修饰符, * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用. * 只匹配函数名,如果有多个同名函数调用第一个。 */ public static Object invokeMethodByName(final Object obj, final String methodName, final Object[] args) { Method method = getAccessibleMethodByName(obj, methodName); if (method == null) { throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + obj + "]"); } try { return method.invoke(obj, args); } catch (Exception e) { throw convertReflectionExceptionToUnchecked(e); } } /** * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问. * * 如向上转型到Object仍无法找到, 返回null. */ public static Field getAccessibleField(final Object obj, final String fieldName) { Validate.notNull(obj, "object can't be null"); Validate.notBlank(fieldName, "fieldName can't be blank"); for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) { try { Field field = superClass.getDeclaredField(fieldName); makeAccessible(field); return field; } catch (NoSuchFieldException e) {//NOSONAR // Field不在当前类定义,继续向上转型 continue;// new add } } return null; } /** * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. * 如向上转型到Object仍无法找到, 返回null. * 匹配函数名+参数类型。 * * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) */ public static Method getAccessibleMethod(final Object obj, final String methodName, final Class<?>... parameterTypes) { Validate.notNull(obj, "object can't be null"); Validate.notBlank(methodName, "methodName can't be blank"); for (Class<?> searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) { try { Method method = searchType.getDeclaredMethod(methodName, parameterTypes); makeAccessible(method); return method; } catch (NoSuchMethodException e) { // Method不在当前类定义,继续向上转型 continue;// new add } } return null; } /** * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. * 如向上转型到Object仍无法找到, 返回null. * 只匹配函数名。 * * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) */ public static Method getAccessibleMethodByName(final Object obj, final String methodName) { Validate.notNull(obj, "object can't be null"); Validate.notBlank(methodName, "methodName can't be blank"); for (Class<?> searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) { Method[] methods = searchType.getDeclaredMethods(); for (Method method : methods) { if (method.getName().equals(methodName)) { makeAccessible(method); return method; } } } return null; } /** * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 */ public static void makeAccessible(Method method) { if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) && !method.isAccessible()) { method.setAccessible(true); } } /** * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 */ public static void makeAccessible(Field field) { if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || Modifier .isFinal(field.getModifiers())) && !field.isAccessible()) { field.setAccessible(true); } } /** * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处 * 如无法找到, 返回Object.class. * eg. * public UserDao extends HibernateDao<User> * * @param clazz The class to introspect * @return the first generic declaration, or Object.class if cannot be determined */ @SuppressWarnings("unchecked") public static <T> Class<T> getClassGenricType(final Class clazz) { return getClassGenricType(clazz, 0); } /** * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. * 如无法找到, 返回Object.class. * * 如public UserDao extends HibernateDao<User,Long> * * @param clazz clazz The class to introspect * @param index the Index of the generic ddeclaration,start from 0. * @return the index generic declaration, or Object.class if cannot be determined */ public static Class getClassGenricType(final Class clazz, final int index) { Type genType = clazz.getGenericSuperclass(); if (!(genType instanceof ParameterizedType)) { logger.warn(clazz.getSimpleName() + "'s superclass not ParameterizedType"); return Object.class; } Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); if (index >= params.length || index < 0) { logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + params.length); return Object.class; } if (!(params[index] instanceof Class)) { logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); return Object.class; } return (Class) params[index]; } public static Class<?> getUserClass(Object instance) { Validate.notNull(instance, "Instance must not be null"); Class clazz = instance.getClass(); if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { Class<?> superClass = clazz.getSuperclass(); if (superClass != null && !Object.class.equals(superClass)) { return superClass; } } return clazz; } /** * 将反射时的checked exception转换为unchecked exception. */ public static RuntimeException convertReflectionExceptionToUnchecked(Exception e) { if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException || e instanceof NoSuchMethodException) { return new IllegalArgumentException(e); } else if (e instanceof InvocationTargetException) { return new RuntimeException(((InvocationTargetException) e).getTargetException()); } else if (e instanceof RuntimeException) { return (RuntimeException) e; } return new RuntimeException("Unexpected Checked Exception.", e); } /** * 判断属性是否为日期类型 * * @param clazz * 数据类型 * @param fieldName * 属性名 * @return 如果为日期类型返回true,否则返回false */ public static <T> boolean isDateType(Class<T> clazz, String fieldName) { boolean flag = false; try { Field field = clazz.getDeclaredField(fieldName); Object typeObj = field.getType().newInstance(); flag = typeObj instanceof Date; } catch (Exception e) { // 把异常吞掉直接返回false } return flag; } /** * 根据类型将指定参数转换成对应的类型 * * @param value * 指定参数 * @param type * 指定类型 * @return 返回类型转换后的对象 */ public static <T> Object parseValueWithType(String value, Class<?> type) { Object result = null; try { // 根据属性的类型将内容转换成对应的类型 if (Boolean.TYPE == type) { result = Boolean.parseBoolean(value); } else if (Byte.TYPE == type) { result = Byte.parseByte(value); } else if (Short.TYPE == type) { result = Short.parseShort(value); } else if (Integer.TYPE == type) { result = Integer.parseInt(value); } else if (Long.TYPE == type) { result = Long.parseLong(value); } else if (Float.TYPE == type) { result = Float.parseFloat(value); } else if (Double.TYPE == type) { result = Double.parseDouble(value); } else { result = (Object) value; } } catch (Exception e) { // 把异常吞掉直接返回null } return result; } }","categories":[{"name":"Java实例","slug":"java实例","permalink":"http://www.aquestian.cn/categories/java%E5%AE%9E%E4%BE%8B/"}],"tags":[{"name":"反射","slug":"反射","permalink":"http://www.aquestian.cn/tags/%E5%8F%8D%E5%B0%84/"}],"author":"aqian666"},{"title":"RabbitMQ五种消息发送模式","slug":"blogs/RabbitMQ-Learning","date":"2020-03-27T16:00:00.000Z","updated":"2020-03-27T16:00:00.000Z","comments":true,"path":"blogs/RabbitMQ-Learning/","link":"","permalink":"http://www.aquestian.cn/blogs/RabbitMQ-Learning/","excerpt":"","text":"写在前面MQ的应用场景:MQ(消息队列)常见的应用场景解析,请查看这篇文章! MQ环境搭建:Docker搭建RabbitMQ,请查看这篇文章! 环境搭建1.搭建一个springboot项目。 2.pom引入,mq所需jar包。 <!--rabbitMQ--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> 3.yaml配置 spring: rabbitmq: host: 127.0.0.1 # rabbitmq的连接地址 port: 5672 # rabbitmq的连接端口号 username: username # rabbitmq的用户名 password: password # rabbitmq的密码 普通模式 简单模式是最简单的消息模式,它包含一个生产者、一个消费者和一个队列。生产者向队列里发送消息,消费者从队列中获取消息并消费。 声明一个队列 @Configuration public class RabbitConfig { //申明第一个队列 @Bean public Queue helloQueue() { return new Queue(&quot;hello&quot;); } } 创建一个生产者(发送者) @Component public class SenderConfig { @Resource private AmqpTemplate rabbitTemplate; //创建消息发送者 public void send() { String context = \"普通模式发送的消息\"; System.out.println(\"普通模式发送者: \" + context); this.rabbitTemplate.convertAndSend(\"hello\", context); } } 创建一个消费者(接受者) @Component public class ReceiverConfig { //创建消息接收者 @RabbitListener(queues = "hello") @RabbitHandler public void process1(String hello) { System.err.println("普通模式消费者: " + hello); } } 测试 @SpringBootTest public class RabbitDemoApplicationTests { @Resource private SenderConfig senderConfig; @Test public void contextLoads() { senderConfig.send(); } } 运行结果 工作模式工作模式是指向多个互相竞争的消费者发送消息的模式,它包含一个生产者、两个消费者和一个队列。两个消费者同时绑定到一个队列上去,当消费者获取消息处理耗时任务时,空闲的消费者从队列中获取并消费消息。 公平发放 声明一个新的队列,防止混淆。公平发放,能让效率高的消费的接受更多消息,举个例子,如果消费者一启动一个线程sleep(10000),那么这时候肯定就是消费者二效率高! //申明一个工作模式队列 @Bean public Queue workQueue() { return new Queue("work"); } 创建一个生产者(发送者) //创建消息发送者 public void sendToWork() { String context = "工作模式发送的消息"; System.out.println("工作模式发送者: " + context); this.rabbitTemplate.convertAndSend("work", context); } 创建俩个消费者(接受者) //创建消息接收者 @RabbitListener(queues = "work") @RabbitHandler public void process2(String work) throws InterruptedException { //Thread.sleep(20000); System.err.println("工作模式 消费者1: " + work); } @RabbitListener(queues = "work") @RabbitHandler public void process3(String work) { System.out.println("工作模式 消费者2: " + work); } 运行 @Test public void contextLoads2() { for (int i = 0;i<10; i++){ senderConfig.sendToWork(); } } 上边说到了公平发放,能者多劳,现在说一下轮训发放,效率没有公平发放高。 轮训发放 发送方式改为有序即可: this.rabbitTemplate.convertSendAndReceive("work", context); 发布订阅模式发布/订阅模式是指同时向多个消费者发送消息的模式(类似广播的形式),它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列绑定到交换机上去,生产者通过发送消息到交换机,所有消费者接收并消费消息。 声明交换机和队列 //申明俩个发布订阅模式的交换机 @Bean public FanoutExchange fanout() { return new FanoutExchange("exchange.fanout"); } //申明俩个发布订阅模式队列 @Bean public Queue fanoutQueue1() { return new Queue("fanout1");//队列一 } @Bean public Queue fanoutQueue2() { return new Queue("fanout2");//队列二 } //将队列一绑定到交换机 @Bean public Binding fanoutBinding1(FanoutExchange fanout, Queue fanoutQueue1) { return BindingBuilder.bind(fanoutQueue1).to(fanout); } //将队列二绑定到交换机 @Bean public Binding fanoutBinding2(FanoutExchange fanout, Queue fanoutQueue2) { return BindingBuilder.bind(fanoutQueue2).to(fanout); } 创建生产者(发送者) //创建消息发送者 public void sendToFanout() { String context = "发布订阅模式发送的消息"; System.out.println("发布订阅模式发送者: " + context); this.rabbitTemplate.convertAndSend("exchange.fanout", "",context); } 创建消费者(接收者) @RabbitListener(queues = "fanout1") @RabbitHandler public void process4(String fanout) { System.out.println("发布订阅模式 消费者1: " + fanout); } @RabbitListener(queues = "fanout2") @RabbitHandler public void process5(String fanout) { System.out.println("发布订阅模式 消费者2: " + fanout); } 运行 @Test public void contextLoads3() { for (int i = 0;i<10; i++){ senderConfig.sendToFanout(); } } 路由模式路由模式是可以根据路由键选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键转发到不同队列,队列绑定的消费者接收并消费消息。 声明交换机和队列 //声明路由模式交换机 @Bean public DirectExchange direct() { return new DirectExchange("exchange.direct"); } //申明俩个路由模式队列 @Bean public Queue directQueue1() { return new Queue("direct1"); //队列一 } @Bean public Queue directQueue2() { return new Queue("direct2"); //队列二 } //将队列队列一绑定到交换机 @Bean public Binding directBinding1a(DirectExchange direct, Queue directQueue1) { return BindingBuilder.bind(directQueue1).to(direct).with("orange"); } @Bean public Binding directBinding1b(DirectExchange direct, Queue directQueue1) { // return BindingBuilder.bind(directQueue1).to(direct).with("black"); } //将队列队列二绑定到交换机 @Bean public Binding directBinding2a(DirectExchange direct, Queue directQueue2) { return BindingBuilder.bind(directQueue2).to(direct).with("green"); } @Bean public Binding directBinding2b(DirectExchange direct, Queue directQueue2) { return BindingBuilder.bind(directQueue2).to(direct).with("black"); } 创建生产者(发送者) //创建消息发送者 public void sendToDirect() { String context = "路由模式发送的消息"; System.out.println("路由模式发送者: " + context); //走black路由 this.rabbitTemplate.convertAndSend("exchange.direct", "black",context); //走orange路由 // this.rabbitTemplate.convertAndSend("exchange.direct", "orange",context); //走green路由 // this.rabbitTemplate.convertAndSend("exchange.direct", "green",context); } 创建消费者(接收者) @RabbitListener(queues = "direct1") @RabbitHandler public void process6(String direct) { System.err.println("路由模式 消费者1: " + direct); } @RabbitListener(queues = "direct2") @RabbitHandler public void process7(String direct) { System.out.println("路由模式 消费者2: " + direct); } 运行 //路由模式 @Test public void contextLoads4() { for (int i = 0;i<10; i++){ senderConfig.sendToDirect(); } } 不同的路由自己试一下。 通配符模式通配符模式是可以根据路由键匹配规则选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键匹配规则绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键匹配规则转发到不同队列,队列绑定的消费者接收并消费消息。 *:只能匹配一个单词; #:可以匹配零个或多个单词。 声明交换机和队列 //声明通配符模式交换机 @Bean public TopicExchange topic() { return new TopicExchange("exchange.topic"); } //声明俩个通配符模式队列 @Bean public Queue topicQueue1() { return new Queue("topic1");//队列一 } @Bean public Queue topicQueue2() { return new Queue("topic2");//队列二 } //将队列队列一绑定到交换机 @Bean public Binding topicBinding1a(TopicExchange topic, Queue topicQueue1) { return BindingBuilder.bind(topicQueue1).to(topic).with("*.orange.*"); } @Bean public Binding topicBinding1b(TopicExchange topic, Queue topicQueue1) { return BindingBuilder.bind(topicQueue1).to(topic).with("*.*.rabbit"); } //将队列队列二绑定到交换机 @Bean public Binding topicBinding2a(TopicExchange topic, Queue topicQueue2) { return BindingBuilder.bind(topicQueue2).to(topic).with("lazy.#"); } 创建生产者(发送者) //创建消息发送者 public void sendToTopic(String index) { String context = "通配符模式发送的消息"; System.out.println("通配符模式发送者: " + context); this.rabbitTemplate.convertAndSend("exchange.topic", index,context+":"+index); } 创建消费者(接收者) @RabbitListener(queues = "topic1") @RabbitHandler public void process8(String topic) { System.err.println("通配符模式 消费者1: " + topic); } @RabbitListener(queues = "topic2") @RabbitHandler public void process9(String topic) { System.out.println("通配符模式 消费者2: " + topic); } 运行 //通配符模式 @Test public void contextLoads5() { senderConfig.sendToTopic("lazy.111"); senderConfig.sendToTopic("111.orange.111"); senderConfig.sendToTopic("1111.111.orange"); senderConfig.sendToTopic("1111.111.rabbit"); senderConfig.sendToTopic("1111.rabbit"); }","categories":[{"name":"消息队列","slug":"消息队列","permalink":"http://www.aquestian.cn/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"}],"tags":[{"name":"RabbitMQ","slug":"rabbitmq","permalink":"http://www.aquestian.cn/tags/rabbitmq/"}],"author":"aqian666"},{"title":"Docker搭建RabbitMQ","slug":"blogs/Docker-RabbitMQ","date":"2020-03-26T16:00:00.000Z","updated":"2020-03-26T16:00:00.000Z","comments":true,"path":"blogs/Docker-RabbitMQ/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-RabbitMQ/","excerpt":"","text":"查看已有镜像//查看rabbitmq已经存在镜像,management表示支持web可视化 docker search rabbitmq:management 结果如下: [root@mail ~]# docker search rabbitmq:management NAME DESCRIPTION STARS OFFICIAL AUTOMATED macintoshplus/rabbitmq-management Based on rabbitmq:management whit python and… 6 [OK] xiaochunping/rabbitmq xiaochunping/rabbitmq:management 2018-06-30 4 transmitsms/rabbitmq-sharded Fork of rabbitmq:management with sharded_exc… 0 拉取镜像//拉取镜像 docker pull rabbitmq:management 结果如下: [root@mail ~] docker pull rabbitmq:management management: Pulling from library/rabbitmq 5667fdb72017: Pull complete d83811f270d5: Pull complete ee671aafb583: Pull complete 7fc152dfb3a6: Pull complete 511da93b5ba5: Pull complete 2e439885870f: Pull complete 5c07a284c0d9: Pull complete 0e4528af7d06: Pull complete f8a8a551f015: Pull complete 5a7a6d68d51f: Pull complete 2210e947fea4: Pull complete f0ea315cdd14: Pull complete Digest: sha256:ff92870e678bbf18868a4da3da7a00048da04504cd34d8848d70bd2c5f64c9e9 Status: Downloaded newer image for rabbitmq:management docker.io/library/rabbitmq:management 查看已有镜像,如下: [root@mail ~]# docker images REPOSITORY TAG IMAGE ID CREATED SIZE rabbitmq management d55229deb03e 3 days ago 187MB 启动镜像//默认启动,账户密码默认都是guest docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management //账号user 密码111111 docker run -d --hostname rabbitmq --name rabbitmq -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=111111 -p 15672:15672 -p 5672:5672 rabbitmq:management http://[宿主机IP]:15672,即可访问mq页面! 部署完成。","categories":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/categories/docker/"}],"tags":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/tags/docker/"},{"name":"RabbitMQ","slug":"rabbitmq","permalink":"http://www.aquestian.cn/tags/rabbitmq/"}],"author":"aqian666"},{"title":"MQ(消息队列)常见的应用场景解析","slug":"blogs/MQ-Scenario","date":"2020-03-25T16:00:00.000Z","updated":"2020-03-25T16:00:00.000Z","comments":true,"path":"blogs/MQ-Scenario/","link":"","permalink":"http://www.aquestian.cn/blogs/MQ-Scenario/","excerpt":"","text":"MQ简介MQ,Message queue,消息队列,就是指保存消息的一个容器。具体的定义这里就不类似于数据库、缓存等,用来保存数据的。当然,与数据库、缓存等产品比较,也有自己一些特点,具体的特点后文会做详细的介绍。 现在常用的MQ组件有activeMQ、rabbitMQ、rocketMQ、zeroMQ,当然近年来火热的kafka,从某些场景来说,也是MQ,当然kafka的功能更加强大,虽然不同的MQ都有自己的特点和优势,但是,不管是哪种MQ,都有MQ本身自带的一些特点,下面,咱们就先聊聊MQ的特点。 MQ特点 先进先出 不能先进先出,都不能说是队列了。消息队列的顺序在入队的时候就基本已经确定了,一般是不需人工干预的。而且,最重要的是,数据是只有一条数据在使用中。 这也是MQ在诸多场景被使用的原因。 发布订阅 发布订阅是一种很高效的处理方式,如果不发生阻塞,基本可以当做是同步操作。这种处理方式能非常有效的提升服务器利用率,这样的应用场景非常广泛。 持久化 持久化确保MQ的使用不只是一个部分场景的辅助工具,而是让MQ能像数据库一样存储核心的数据。 分布式 在现在大流量、大数据的使用场景下,只支持单体应用的服务器软件基本是无法使用的,支持分布式的部署,才能被广泛使用。而且,MQ的定位就是一个高性能的中间件。 应用场景基于上文所述的特点,那么MQ就衍生出了中的使用场景,在大型的系统中,应用非常广泛,这里我们就列举一下常见的应用场景。 应用解耦(异步) 系统之间进行数据交互的时候,在时效性和稳定性之间我们都需要进行选择。基于线程的异步处理,能确保用户体验,但是极端情况下可能会出现异常,影响系统的稳定性,而同步调用很多时候无法保证理想的性能,那么我们就可以用MQ来进行处理。上游系统将数据投递到MQ,下游系统取MQ的数据进行消费,投递和消费可以用同步的方式处理,因为MQ接收数据的性能是非常高的,不会影响上游系统的性能,那么下游系统的及时率能保证吗?当然可以,不然就不会有下面的一个应用场景。 通知 这里就用到了前文一个重要的特点,发布订阅,下游系统一直在监听MQ的数据,如果MQ有数据,下游系统则会按照 先进先出 这样的规则, 逐条进行消费 ,而上游系统只需要将数据存入MQ里,这样就既降低了不同系统之间的耦合度,同时也确保了消息通知的及时性,而且也不影响上游系统的性能。 限流 上文有说了一个非常重要的特性,MQ 数据是只有一条数据在使用中。 在很多存在并发,而又对数据一致性要求高,而且对性能要求也高的场景,如何保证,那么MQ就能起这个作用了。不管多少流量进来,MQ都会让你遵守规则,排除处理,不会因为其他原因,导致并发的问题,而出现很多意想不到脏数据。 数据分发 MQ的发布订阅肯定不是只是简单的一对一,一个上游和一个下游的关系,MQ中间件基本都是支持一对多或者广播的模式,而且都可以根据规则选择分发的对象。这样上游的一份数据,众多下游系统中,可以根据规则选择是否接收这些数据,这样扩展性就很强了。 PS:上文中的上游和下游,在MQ更多的是叫做生产者(producer)和消费者(consumer)。 分布式事务 分布式事务是我们开发中一直尽量避免的一个技术点,但是,现在越来越多的系统是基于微服务架构开发,那么分布式事务成为必须要面对的难题,解决分布式事务有一个比较容易理解的方案,就是二次提交。基于MQ的特点,MQ作为二次提交的中间节点,负责存储请求数据,在失败的情况可以进行多次尝试,或者基于MQ中的队列数据进行回滚操作,是一个既能保证性能,又能保证业务一致性的方案,当然,这个方案的主要问题就是定制化较多,有一定的开发工作量。 应用示例为了更加直观的展示MQ的应用场景,这里我们就用一个常见的电商系统中的几个业务,来具体说明下MQ在实际开发中应用场景。 我们的实际场景大概是一个基于微服务架构的电商系统,分为用户微服务、商品微服务、订单微服务、促销微服务等。基于微服务模式开发的系统,MQ的使用场景更多,下面我们逐一说明: 1、注册后我们可能需要做很多初始化的操作,如:调用邮件服务器发送邮件、调用促销服务赠送优惠劵、下发用户数据到客户关系系统等。那么这时候我们将这些操作去监听MQ,当用户注册成功过后,通过MQ通知其他业务进行操作。确保注册用户的性能。 2、后台发布商品的时候,商品数据需要从数据库中转换成搜索引擎数据(基于elasticsearch),那么我们应该将商品写入数据库后,再写入到MQ,然后通过监听MQ来生成elasticsearch对应的数据。 3、用户下单后,24小时未支付,需要取消订单。以前我们可能是定时任务循环查询,然后取消订单。实际上,我更推荐类似延迟MQ的方式,避免了很多无效的数据库查询,将一个MQ设置为24小时后才让消费者消费掉,这样很大程度上能减轻服务器压力。 4、支付完成后,需要及时的通知子系统(进销存系统发货,用户服务积分,发送短信)进行下一步操作,但是,支付回调我们都是需要保证高性能的,所以,我应该直接修改数据库状态,存入MQ,让MQ通知子系统做其他非实时的业务操作。这样能保证核心业务的高效及时。 注意事项其实,还有非常多的业务场景,是可以考虑用MQ方式的,但是很多时候,也会存在滥用的情况,我们需要清楚认识我们的业务场景: 发验证码短信、邮件,这种过分依赖外部,而且时效性可以接收几十秒延迟的,其实更好的方式是多线程异步处理,而不是过多依赖MQ。 秒杀抢购确保库存不为负数,更多的依赖高性能缓存(如redis),以及强制加锁,千万不要依赖消费者最终的返回结果。(实际工作中已经看到好几个这样的案例了)上游-下游 这种直接的处理方式效率肯定是比 上游-MQ-下游 方式要高,MQ效率高,是因为,我只是上游-MQ 这个阶段就当做已经成功了。 总结任何一个技术的出现,都有他的业务场景,只有清楚技术的特点,才能更加贴切的挖掘出应用场景,深入思考,深入实践才能将一个技术用在最合适的地方。 转载 https://www.cnblogs.com/joylee/p/8916460.html","categories":[{"name":"消息队列","slug":"消息队列","permalink":"http://www.aquestian.cn/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"}],"tags":[{"name":"消息队列","slug":"消息队列","permalink":"http://www.aquestian.cn/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"}],"author":"aqian666"},{"title":"SpringBoot 整合Security + JWT","slug":"blogs/Springboot-Security-JWT","date":"2020-03-15T16:00:00.000Z","updated":"2020-03-15T16:00:00.000Z","comments":true,"path":"blogs/Springboot-Security-JWT/","link":"","permalink":"http://www.aquestian.cn/blogs/Springboot-Security-JWT/","excerpt":"","text":"什么是JWTJson web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。 传统Cookie+Session与JWT对比① 在传统的用户登录认证中,因为http是无状态的,所以都是采用session方式。用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。cookie+session这种模式通常是保存在内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。而JWT不是这样的,只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。 ② JWT方式校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录,验证token更为简单。 项目搭建spring boot + Security + JWT + JPA。说明我已经全部写在注解里。 目录结构 引入jar包<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.gbq.jpa.jwt.demo</groupId> <artifactId>jpa-jwt-demo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> </dependency> <!-- 引入jpa 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- JWT依赖 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.32</version> </dependency> </dependencies> <!-- 使用maven打包 --> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project> yml配置server: tomcat: uri-encoding: UTF-8 port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: database: mysql show-sql: true hibernate: ddl-auto: update jwt: secret: secret expiration: 7200000 token: Authorization bean@Entity @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(nullable = false) private String username; @Column(nullable = false) private String password; } daopublic interface UserDao extends JpaRepository<User, Integer> { //自定义repository。手写sql @Query(value = "update user set name=?1 where id=?2",nativeQuery = true) //占位符传值形式 @Modifying int updateById(String name,int id); @Query("from User u where u.username=:username") //SPEL表达式 User findUser(@Param("username") String username);// 参数username 映射到数据库字段username } service * Created by 阿前 * 2020年6月30日15:56:36 */ public interface UserService { User getUser(String loginName); } serviceImplpublic class UserServiceImpl implements UserService { @Resource private UserDao userDao; @Override public User getUser(String username) { return userDao.findUser(username); } } security配置@EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class SecurityUserDetails extends User implements UserDetails { private Collection<? extends GrantedAuthority> authorities; public SecurityUserDetails(User user) { if (user != null) { this.setUsername(user.getUsername()); this.setPassword(user.getPassword()); } } @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList<>(); String username = this.getUsername(); if (username != null) { SimpleGrantedAuthority authority = new SimpleGrantedAuthority(username); authorities.add(authority);//分配权限 } return authorities; } /** * 账户是否过期 * @return */ @Override public boolean isAccountNonExpired() { return true; } /** * 是否禁用 * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 密码是否过期 * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否启用 * @return */ @Override public boolean isEnabled() { return true; } } comment(JWT+Security验证)//jwt验证 @Component public class JwtAuthorizationTokenFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final JwtTokenComment jwtTokenComment; private final String tokenHeader; public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsService") UserDetailsService userDetailsService, JwtTokenComment jwtTokenComment, @Value("${jwt.token}") String tokenHeader) { this.userDetailsService = userDetailsService; this.jwtTokenComment = jwtTokenComment; this.tokenHeader = tokenHeader; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestHeader = request.getHeader(this.tokenHeader); String username = null; String authToken = null; if (requestHeader != null && requestHeader.startsWith("Bearer ")) { authToken = requestHeader.substring(7); try { username = jwtTokenComment.getUsernameFromToken(authToken); } catch (ExpiredJwtException e) { } } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenComment.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(request, response); } } @Component public class JwtUserDetailsService implements UserDetailsService { @Resource private UserService userService; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { System.out.println("JwtUserDetailsService:" + s); User user = userService.getUser(s); if (user == null) throw new UsernameNotFoundException("Username " + s + " not found"); return new SecurityUserDetails(user); } } @Component public class LoadUserComment { @Resource private UserDetailsService userDetailsService; public UserDetails loadUserByUsername(String username, String password) throws BusinessException { try { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (userDetails != null) { if (!userDetails.getPassword().contains(password)) { throw new BackingStoreException("密码不正确"); } else { return userDetails; } }else { throw new BackingStoreException("用户不存在"); } } catch (BackingStoreException e) { throw new BusinessException(e.getMessage()); } } } @Component public class JwtTokenComment { private static final long serialVersionUID = -3301605591108950415L; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; @Value("${jwt.token}") private String tokenHeader; private Clock clock = DefaultClock.INSTANCE; public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } private String doGenerateToken(Map<String, Object> claims, String subject) { final Date createdDate = clock.now(); final Date expirationDate = calculateExpirationDate(createdDate); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(createdDate) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } private Date calculateExpirationDate(Date createdDate) { return new Date(createdDate.getTime() + expiration); } public Boolean validateToken(String token, UserDetails userDetails) { SecurityUserDetails user = (SecurityUserDetails) userDetails; final String username = getUsernameFromToken(token); return (username.equals(user.getUsername()) && !isTokenExpired(token) ); } public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(clock.now()); } public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } } @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { System.out.println("JwtAuthenticationEntryPoint:"+authException.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"没有凭证"); } } controller@RestController @Slf4j public class UserController { @Resource private LoadUserComment loadUserComment; @Resource @Qualifier private JwtTokenComment jwtTokenComment; @PostMapping("login") public HashMap<String, Object> login (@RequestBody Map<String,String> map){ HashMap<String, Object> result = new HashMap<>(); String username = map.get("username"); String password = map.get("password"); UserDetails userDetails = loadUserComment.loadUserByUsername(username,password); String token = jwtTokenComment.generateToken(userDetails); result.put("token",token); result.put("user",userDetails); return result; } @GetMapping("getUser") public String getUser(){ UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return "getUser:"+userDetails.getUsername()+","+userDetails.getPassword(); } } 测试","categories":[{"name":"单点登录","slug":"单点登录","permalink":"http://www.aquestian.cn/categories/%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95/"}],"tags":[{"name":"SpringBoot","slug":"springboot","permalink":"http://www.aquestian.cn/tags/springboot/"},{"name":"JWT","slug":"jwt","permalink":"http://www.aquestian.cn/tags/jwt/"},{"name":"Security","slug":"security","permalink":"http://www.aquestian.cn/tags/security/"}],"author":"aqian666"},{"title":"springboot租房管理系统源码分享","slug":"code/Code-Rent","date":"2020-03-02T16:00:00.000Z","updated":"2020-03-02T16:00:00.000Z","comments":true,"path":"code/Code-Rent/","link":"","permalink":"http://www.aquestian.cn/code/Code-Rent/","excerpt":"","text":"项目描述springboot租房管理系统源码分享,前端使用vue.js,后端使用springboot+hibernate。 运行环境jdk8+tomcat8+mysql5.7+IntelliJ IDEA+maven 项目技术spring boot+spring mvc+hibernate+jquery+bootstrap 项目截图 运行截图localhost:8888","categories":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/categories/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"tags":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/tags/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"author":"aqian666"},{"title":"Redis的五种数据类型","slug":"blogs/Redis-Types","date":"2020-02-09T16:00:00.000Z","updated":"2019-08-06T16:00:00.000Z","comments":true,"path":"blogs/Redis-Types/","link":"","permalink":"http://www.aquestian.cn/blogs/Redis-Types/","excerpt":"","text":"序列化防止乱码: // key序列化 redisTemplate.setKeySerializer(STRING_SERIALIZER); // value序列化 redisTemplate.setValueSerializer(JACKSON__SERIALIZER); // Hash key序列化 redisTemplate.setHashKeySerializer(STRING_SERIALIZER); // Hash value序列化 redisTemplate.setHashValueSerializer(JACKSON__SERIALIZER); redisTemplate.afterPropertiesSet(); String //插入字符串 redisTemplate.opsForValue().set("String","String"); 图示 使用场景String是最常用的数据类型,普通的key/value都可以归为此类,value其实不仅是String,也可以是数字。 比如想知道什么时候封锁一个IP地址(访问超过几次)。INCRBY命令让这些变得很容易,通过原子递增保持计数。 List//插入集合 List<String> strings1 = new ArrayList<>(); strings1.add("LIST1"); strings1.add("LIST2"); strings1.add("LIST2"); for (String str:strings1){ redisTemplate.opsForList().leftPush("list1",str);//左插入 redisTemplate.opsForList().rightPush("list2",str);//右插入 } 图示(注意对比左右插入的区别) 使用场景最新消息排行。 消息队列。利用Lists的push的操作,将任务存储在list中,然后工作线程再用pop操作将任务取出进行执行。 Set//插入set集合 Set<String> set1 = new HashSet<>(); set1.add("set1"); set1.add("set2"); set1.add("set3"); set1.add("set3"); for (String str:set1){ redisTemplate.opsForSet().add("set",str); } 图示(注意对比代码和图示) 使用场景set类似list,特殊之处是set可以自动排重。 set还提供了某个成员是否在一个set内的接口,这个也是list没有的。 比如在微博应用中,每个人的好友存在一个集合(set)中,这样求两个人的共同好友的操作,可能就只需要用求交集命令即可。 Redis还为集合提供了求交集、并集、差集等操作。 Hash//插入map redisTemplate.opsForHash().put("hash","hashKey1","hashValue1"); redisTemplate.opsForHash().put("hash","hashKey2","hashValue2"); 图示 使用场景1.购物车:hset [key] [field] [value] 命令, 可以实现以用户Id,商品Id为field,商品数量为value,恰好构成了购物车的3个要素。 2.存储对象:hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。 zset//插入zset redisTemplate.opsForZSet().add("zset","hello", 1); redisTemplate.opsForZSet().add("zset","hi", 2); redisTemplate.opsForZSet().add("zset","nihao", 3); 使用场景例如热门歌曲榜单列表,value值是歌曲ID,score是播放次数,这样就可以对歌曲列表按播放次数进行排序。 当然还有类似微博粉丝列表、评论列表等等,可以将value定义为用户ID、评论ID,score定义为关注时间、评论点赞次数等等。 图示 redis学习思维导图学习地址","categories":[{"name":"Redis","slug":"redis","permalink":"http://www.aquestian.cn/categories/redis/"}],"tags":[{"name":"Redis","slug":"redis","permalink":"http://www.aquestian.cn/tags/redis/"}],"author":"aqian666"},{"title":"Docker搭建MySQL5.7","slug":"blogs/Docker-MySQL5.7","date":"2020-01-09T16:00:00.000Z","updated":"2020-01-09T16:00:00.000Z","comments":true,"path":"blogs/Docker-MySQL5.7/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-MySQL5.7/","excerpt":"","text":"Docker搭建mysql查看mysql已有镜像docker search mysql //查看mysql已有镜像,然后它会给大家展示一大堆,这里我就 不给大家截图了 拉取mysql镜像这里安装5.7版本 docker pull mysql:5.7 //安装mysql5.7 启动容器记得关闭防火墙,或者开放3306端口。 docker run -it --name mysql5.7 -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:5.7 --lower_case_table_names=1 //Docker搭建mysql忽略大小写 这样Docker搭建mysql就完成了。 Navicat连接数据库情况一 情况二 Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘sss.month_id’ which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by //这种错误,我百度得到的解释是MySQL 5.7.5及以上功能依赖检测功能。如果启用了ONLY_FULL_GROUP_BY SQL模式(默认情况下),MySQL将拒绝选择列表,HAVING条件或ORDER BY列表的查询引用在GROUP BY子句中既未命名的非集合列,也不在功能上依赖于它们。(5.7.5之前,MySQL没有检测到功能依赖关系,默认情况下不启用ONLY_FULL_GROUP_BY。有关5.7.5之前的行为的说明,请参见“MySQL 5.6参考手册”。) 第二种错误大部分要求你修改my.cnf配置文件,添加如下内容。 [mysqld] sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION 我试了但是并没有生效。 经过尝试,俩种不同的报错可以一起解决。 解决docker ps //查看正在运行的容器 如上图所示, 我们进入到容器中 docker exec -it 4730106799e3 bash //注意使用容器id,而不是容器名称,网上很多都是容器名称,我这里使用容器名称是不成功的。 mysql -uroot -p123456 //登录mysql mysql> ALTER user 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; Query OK, 0 rows affected (0.01 sec) mysql> mysql> FLUSH PRIVILEGES; Query OK, 0 rows affected (0.01 sec) //运行如上内容,调整权限 mysql> quit //退出mysqlroot@4730106799e3:/# exit //退出容器docker restart 4730106799e3 //重新启动容器 就OK了!","categories":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/categories/docker/"}],"tags":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/tags/docker/"},{"name":"MySQL","slug":"mysql","permalink":"http://www.aquestian.cn/tags/mysql/"}],"author":"aqian666"},{"title":"Java二叉树","slug":"blogs/Java-BinaryTree","date":"2020-01-09T16:00:00.000Z","updated":"2020-01-09T16:00:00.000Z","comments":true,"path":"blogs/Java-BinaryTree/","link":"","permalink":"http://www.aquestian.cn/blogs/Java-BinaryTree/","excerpt":"","text":"查找二叉树的条件 左子树上所有结点的值均小于或等于它的根结点的值。 右子树上所有结点的值均大于或等于它的根结点的值。 左、右子树也分别为二叉排序树。 如图所示, 构建二叉树构建节点(叶子) @Data public class Node { Integer id; Node left; Node right; Node(Integer id){ this.id = id; this.left = null; this.right = null; } Node() { this.id = null; this.left = null; this.right = null; } } 构建整体(树) 包括根节点 @Data public class BinaryTree extends Node{ private Node root; } 首先我们必须找到新节点的位置,是为了保持树排序。从根节点开始,必须遵循下面的规则: 如果新节点小于当前的值,将会进入左子树。 如果新节点大于当前的节点,将会进入右子树。 当当前的节点是null时,我们已经到达叶子节点,我们可以添加新节点到这个位置。 添加节点 public Node addNode(Node currentNode, int value) { //第一次创建,从根节点开始 if (currentNode == null){ return new Node(value); } if (value< currentNode.getId()){ //如果新节点小于当前的值,将会进入左子树。 currentNode.left =addNode(currentNode.left, value); }else { currentNode.right =addNode(currentNode.right, value); } return currentNode; } public void addNode(int value) { root = addNode(root,value); } 查找节点以及其子节点 public BinaryTree selectNode(int value,Node current){ BinaryTree binaryTree = new BinaryTree(); if (current == null){ return null; } if (value == current.id){ binaryTree.root = current; return binaryTree; }else { if (value<root.id){ current=root.left; }else { current=root.right; } return selectNode(value,current); } } 删除节点 public Node deleteNode(Node current, int value) { if (current == null) { return null; } if (value == current.id) { // 开启下边方法删除单个节点 // if (current.left == null && current.right == null) { // return null; // } // if (current.left == null) { // return current.right; // } // if (current.right == null) { // return current.left; // } // int id = findNode(current.right); // current.id = id; // current.right = deleteNode(current.right, id); // return current; // 删除当前节点下的子节点 // current.left = null; // current.right = null; // return current; // 删除本节点及其子节点 // return null; } if(value < root.id) { current.left = deleteNode(current.left, value); } current.right = deleteNode(current.right, value); return current; } public BinaryTree deleteNode(BinaryTree binaryTree, int value){ Node node = deleteNode(binaryTree.root, value); if (node!=null){ binaryTree.root = node; return binaryTree; } return null; } private int findNode(Node root) { return root.left == null ? root.id : findNode(root.right); } 遍历树这个就跟那位老哥写的一样了,简单易懂。 我们将以不同的方式遍历树,以depth-first,breadth-first方式遍历树。 以Depth-First遍历树 Depth-first查询是一种在查询兄弟节点之前,尽可能的查询每个子节点。 in-order,pre-order,post-order方式都是以depth-first方式遍历树的。 in-order遍历是首先遍历左子树,然后root节点,最后是右子树。 public void traverseInOrder(Node root) { if (root != null) { traverseInOrder(root.left); System.out.println(root.data); traverseInOrder(root.right); } } pre-order遍历首先是root节点,然后是左子树,最后是右子树。 public void traversePreOrder(Node root) { if (root != null) { System.out.println(root.data); traversePreOrder(root.left); traversePreOrder(root.right); } } post-order遍历首先是遍历左子树,然后是右子树,最后是root节点。 public void traversePostOrder(Node root) { if (root != null) { traversePostOrder(root.left); traversePostOrder(root.right); System.out.println(root.data); } } 以Breadth-First遍历 它在遍历下一级的节点之前,会遍历当前级的所有节点。 这种类型的遍历也叫做level-order,遍历树从root节点开始,从左到右。 为了实现,使用队列来存储每个级别的节点。我们将会从列表中获取每个节点。然后添加他的子节点到队列中。 public void traverseLevelOrder(Node root) { if (root == null) { return; } Queue<Node> nodes = new LinkedList<Node>(); nodes.add(root); while(!nodes.isEmpty()) { Node node = nodes.remove(); System.out.println(node.data); if (node.left != null) { nodes.add(node.left); } if (node.right != null) { nodes.add(node.right); } } } 测试 @Test public void Nodes() { //创建树 BinaryTree binaryTree = new BinaryTree(); binaryTree.addNode(5 ); binaryTree.addNode(3 ); binaryTree.addNode(7); binaryTree.addNode(2); binaryTree.addNode(4); binaryTree.addNode(6); binaryTree.addNode(8); //查询节点 BinaryTree binaryTreeSun = binaryTree.selectNode(7,binaryTree.getRoot()); System.err.println(binaryTreeSun.toString()); //遍历节点 binaryTree.traverseLevelOrder(binaryTree.getRoot()); //删除节点 BinaryTree binaryTrees = binaryTree.deleteNode(binaryTree,7); System.err.println(binaryTrees.toString()); } 创建树完成后树结构为 查询节点树结构为 删除节点树结构为","categories":[{"name":"Java实例","slug":"java实例","permalink":"http://www.aquestian.cn/categories/java%E5%AE%9E%E4%BE%8B/"}],"tags":[{"name":"数据结构","slug":"数据结构","permalink":"http://www.aquestian.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"}],"author":"aqian666"},{"title":"Spring中常用的注解","slug":"blogs/Sping-Comments","date":"2019-12-11T16:00:00.000Z","updated":"2019-12-11T16:00:00.000Z","comments":true,"path":"blogs/Sping-Comments/","link":"","permalink":"http://www.aquestian.cn/blogs/Sping-Comments/","excerpt":"","text":"查找所有注解首先,我们来创建一个项目,使用SPRING INITIALIZR生成一个引入Spring各种组件的项目模板,然后引入如下工具包: <dependency> <groupId>org.reflections</groupId> <artifactId>reflections</artifactId> <version>0.9.11</version> </dependency> 通过这个反射工具包,我们可以创建一个Spring Boot应用程序,以一行代码打印出所有Spring框架的注解: import org.reflections.Reflections; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.lang.annotation.Annotation; @Componentpublic class ScanAnnotationRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { new Reflections("org.springframework") .getSubTypesOf(Annotation.class) .stream() .map(clazz->clazz.getName()) .sorted() .forEach(System.out::println); }} 输出结果,下面我们逐一进行梳理其中的一些重要注解。 org.springframework.beans.factory.annotation.Autowired org.springframework.beans.factory.annotation.Configurable org.springframework.beans.factory.annotation.Lookup org.springframework.beans.factory.annotation.Qualifier org.springframework.beans.factory.annotation.Required org.springframework.beans.factory.annotation.Value org.springframework.boot.SpringBootConfiguration org.springframework.boot.autoconfigure.AutoConfigurationPackage org.springframework.boot.autoconfigure.AutoConfigureAfter org.springframework.boot.autoconfigure.AutoConfigureBefore org.springframework.boot.autoconfigure.AutoConfigureOrder org.springframework.boot.autoconfigure.EnableAutoConfiguration org.springframework.boot.autoconfigure.ImportAutoConfiguration org.springframework.boot.autoconfigure.SpringBootApplication org.springframework.boot.autoconfigure.condition.ConditionalOnBean org.springframework.boot.autoconfigure.condition.ConditionalOnClass org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform org.springframework.boot.autoconfigure.condition.ConditionalOnExpression org.springframework.boot.autoconfigure.condition.ConditionalOnJava org.springframework.boot.autoconfigure.condition.ConditionalOnJndi org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication org.springframework.boot.autoconfigure.condition.ConditionalOnProperty org.springframework.boot.autoconfigure.condition.ConditionalOnResource org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication org.springframework.boot.autoconfigure.data.ConditionalOnRepositoryType org.springframework.boot.autoconfigure.domain.EntityScan org.springframework.boot.autoconfigure.flyway.FlywayDataSource org.springframework.boot.autoconfigure.liquibase.LiquibaseDataSource org.springframework.boot.autoconfigure.quartz.QuartzDataSource org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean org.springframework.boot.context.properties.ConfigurationProperties org.springframework.boot.context.properties.ConfigurationPropertiesBinding org.springframework.boot.context.properties.DeprecatedConfigurationProperty org.springframework.boot.context.properties.EnableConfigurationProperties org.springframework.boot.context.properties.NestedConfigurationProperty org.springframework.boot.convert.DataSizeUnit org.springframework.boot.convert.Delimiter org.springframework.boot.convert.DurationFormat org.springframework.boot.convert.DurationUnit org.springframework.boot.jackson.JsonComponent org.springframework.boot.web.server.LocalServerPort org.springframework.boot.web.servlet.ServletComponentScan org.springframework.cache.annotation.CacheConfig org.springframework.cache.annotation.CacheEvict org.springframework.cache.annotation.CachePut org.springframework.cache.annotation.Cacheable org.springframework.cache.annotation.Caching org.springframework.cache.annotation.EnableCaching org.springframework.context.annotation.Bean org.springframework.context.annotation.ComponentScan org.springframework.context.annotation.ComponentScan$Filter org.springframework.context.annotation.ComponentScans org.springframework.context.annotation.Conditional org.springframework.context.annotation.Configuration org.springframework.context.annotation.DependsOn org.springframework.context.annotation.Description org.springframework.context.annotation.EnableAspectJAutoProxy org.springframework.context.annotation.EnableLoadTimeWeaving org.springframework.context.annotation.EnableMBeanExport org.springframework.context.annotation.Import org.springframework.context.annotation.ImportResource org.springframework.context.annotation.Lazy org.springframework.context.annotation.Primary org.springframework.context.annotation.Profile org.springframework.context.annotation.PropertySource org.springframework.context.annotation.PropertySources org.springframework.context.annotation.Role org.springframework.context.annotation.Scope org.springframework.context.event.EventListener org.springframework.core.annotation.AliasFor org.springframework.core.annotation.Order org.springframework.format.annotation.DateTimeFormat org.springframework.format.annotation.NumberFormat org.springframework.jmx.export.annotation.ManagedAttribute org.springframework.jmx.export.annotation.ManagedMetric org.springframework.jmx.export.annotation.ManagedNotification org.springframework.jmx.export.annotation.ManagedNotifications org.springframework.jmx.export.annotation.ManagedOperation org.springframework.jmx.export.annotation.ManagedOperationParameter org.springframework.jmx.export.annotation.ManagedOperationParameters org.springframework.jmx.export.annotation.ManagedResource org.springframework.lang.NonNull org.springframework.lang.NonNullApi org.springframework.lang.NonNullFields org.springframework.lang.Nullable org.springframework.lang.UsesJava7 org.springframework.lang.UsesJava8 org.springframework.lang.UsesSunHttpServer org.springframework.lang.UsesSunMisc org.springframework.objenesis.instantiator.annotations.Instantiator org.springframework.scheduling.annotation.Async org.springframework.scheduling.annotation.EnableAsync org.springframework.scheduling.annotation.EnableScheduling org.springframework.scheduling.annotation.Scheduled org.springframework.scheduling.annotation.Schedules org.springframework.stereotype.Component org.springframework.stereotype.Controller org.springframework.stereotype.Indexed org.springframework.stereotype.Repository org.springframework.stereotype.Service org.springframework.validation.annotation.Validated 有关注解Java的Annotation注解(类似于C#的Attribute特性),说白了就是给代码打上标签的能力。我们可以配置这个标签的保留阶段,仅源代码,源代码+字节码,源代码+字节码+运行时。通过引入注解,我们可以简单快速赋予代码生命力,大大提高代码可读性和扩展性。注解本身不具有任何能力,只是一个标签,但是我们可以定义各种标签然后实现各种标签处理器来对类、方法、属性甚至参数等进行功能扩展、功能开启、属性定义、行为定义、规则定义、关联处理、元数据定义等等。在实现各种框架的时候,我们经常会自定义标签方便框架使用者仅仅通过在合适的地方引入合适的注解来启用(或自定义)框架的一些能力并应用到我们的程序中。 不仅仅是框架的作者会大量使用注解,在之前的系列文章中我们也多次自定义注解,我们有通过定义@Metrics注解配合Spring AOP来为程序启动打点、日志、异常等功能,我们有通过定义@Sign注解配合Spring MVC的ResponseBodyAdvice进行数据签名功能,我们还经常会定义各种自定义注解配合Spring MVC的HandlerMethodArgumentResolver进行权限的校验等等功能。采用这种模式,我们的核心业务逻辑可以保持清晰干净,通过注解配合AOP赋予代码额外的能力。 你可能会说,注解还是有侵入性,我们需要耦合框架定义的那些注解,这个问题其实是无解的,100%无侵入性也代表了可读性的降低,代码的功能和能力应当聚合在一起,这也就是为什么Spring现在也不建议采用XML来做配置。Java核心类库并没有什么注解,好在Spring已经有了大量注解,而Spring也变为了Java开发的标准,所以其实我们很多时候如果希望自己的框架(RPC啥的)完全没有侵入性的话可以借用Spring的那些注解@Autowired、@Controller、@Service等注解,配合各种包的规范其实我们可以对目标元素的功能识别个八九不离十,完全有可能实现0侵入的功能增强。 元注解(注解的注解): A. @Documented:将会在被此注解注解的元素的javadoc文档中列出注解,一般都打上这个注解没坏处 B. @Target:注解能被应用的目标元素,比如类、方法、属性、参数等等,需要仔细思考 C. @Retention:仅在源码保留,还是保留到编译后的字节码,还是到运行时也去加载,超过90%的应用会在运行时去解析注解进行额外的处理,所以大部分情况我们都会设置配置为RetentionPolicy.RUNTIME D. @Inherited:如果子类没有定义注解的话,能自动从父类获取定义了继承属性的注解,比如Spring的@Service是没有继承特性的,但是@Transactional是有继承特性的,在OO继承体系中使用Spring注解的时候请特别注意这点,理所当然认为注解是能被子类继承的话可能会引起不必要的Bug,需要仔细斟酌是否开启继承 E. @Repeatable:Java 8 引入的特性,通过关联注解容器定义可重复注解,小小语法糖提高了代码可读性,对于元素有多个重复注解其实是很常见的事情,比如某方法可以是A角色可以访问也可以是B角色可以访问,某方法需要定时任务执行,要在A条件执行也需要在B条件执行 F. @Native:是否在.h头文件中生成被标记的字段,除非原生程序需要和Java程序交互,否则很少会用到这个元注解 现在我们来从几个方面逐一温习一下Spring的那些常用的值得关注的注解。 Spring核心注解 A. 首先来看一下各种stereotype:按分类定义了由Spring管理的各种组件,@Controller定义表现层组件,@Service定义业务逻辑层组件,@Repository定义数据访问层资源库组件,@Component定义其它组件(比如访问外部服务的组件),之前也说过了随着这些注解功能无区别,但是对组件进行合适的分类意义重大,不仅仅增加可读性而且方便我们通过AOP对不同类型的组件进行更多自动增强 B.再来看看IOC相关的一些注解:@Autowired自动装配不用多说了;@Required用于在setter方法标记属性值需要由Spring进行装配,对于目前版本的Spring这个注解已经废弃,现在Spring更推荐使用构造方法注入;@Qualifier用于通过给Bean定义修饰语来注入相应的Bean,和@Autowired一起使用相当于@Resource的效果,当然还有一种常见用法是嵌入其它注解用于对Bean进行区分,然后配合@Autowired一起使用,参见后面提到的Spring Cloud的@LoadBalanced注解;@Value用于注入属性配置或SpEL表达式(前者是我们常见用法,后者可以从其它对象获取值,功能更强大一点);@Lookup可以实现方法注入,如果我们的类是单例的,但是又希望Spring注入的依赖的对象是Prototype生命周期(每次new一个出来)的,这个时候可以通过此注解进行方法注入 C. 然后来看一下有关事务的几个注解:@EnableTransactionManagement用于开启事务管理,使用Spring Boot如果引入Spring Data的话不需要手动开启(不过建议大家在使用事务的时候还是通过日志来验证事务管理是否生效);@Transactional大家都知道用于开启事务以及设置传播性、隔离性、回滚条件等;@TransactionalEventListener用于配置事务的回调方法,可以在事务提交前、提交后、完成后以及回滚后几个阶段接受回调事件。 D. @Order注解可以设置Spring管理对象的加载顺序,在之前介绍AOP的文章中我们看到有的时候我们必须通过设置合理的@Order来合理安排切面的切入顺序避免一些问题,还有在一些业务场景中,我们往往会去定义一组类似于Filter的@Component,然后会从容器获得一组Bean,这个时候业务组件的运行顺序往往会比较重要,也可以通过这个方式进行排序 E. @AliasFor注解可以设置一组注解属性相互作为别名,对于有歧义的时候会使代码更清晰,此外还有一个用途是创建复合注解,Spring MVC的@GetMapping注解就是基于@RequestMapping这个注解创建的复合注解,我们可以很方便得通过这种方式来实现注解的继承 Spring上下文注解 A. 首先来看一下配置相关的一些注解:@Configuration用于标注配置类,启用Java配置方式的Bean配置;@Bean用于配置一个Bean;@ComponentScan(@ComponentScans用于配置一组@ComponentScan,Java 8可以直接使用重复注解特性配置多个@ComponentScan)用于扫描包方式配置Bean;@PropertySource以及 @PropertySources用于导入配置文件;@Conditional用于设置关联的条件类,在合适的时候启用Bean的配置(Spring Boot自动配置根基);@Import用于导入其它配置类; @ImportResource用于导入非Java配置方式的XML配置;@Profile用于指定在合适的Profile下启用配置;@Lazy用于告知容器延迟到使用的时候实例化Bean(默认情况下容器启动的时候实例化Bean来检查所有的问题);@Description用于给Bean设置描述;@Scope用于设置Bean的生命周期;@Primary用于在定义了多个Bean的时候指定首选的Bean B. 其它一些注解包括:@EventListener用于设置回调方法监听Spring制定的以及自定义的各种事件;@EnableAspectJAutoProxy用于开启支持AspectJ的 @Aspect切面配置支持,使用Spring Boot引入了AOP启动器的话不需要显式开启 Spring Web注解Spring MVC的各种注解对应了Spring MVC各方面的功能,下面我们来了解一下: A. 首先是三个定义了Bean特殊生命周期的复合注解:@RequestScope、@SessionScope和 @ApplicationScope。在Web应用中,我们可能需要Bean跟随请求、会话和应用程序的声明周期来进行创建,这个时候可以直接使用这三个快捷的复合注解 B. 接下去可以看到各种 @XXXMapping的注解,分别用于配置HandlerMethod匹配到不同的Http Method,当然不使用这些快捷的注解也是可以的,直接使用@RequestMapping然后手动设置method C. @ResponseStatus可以用到方法上也可以用到异常上,前者会直接使请求得到指定的响应代码或原因(可以配合@ExceptionHandler使用),后者可以实现遇到指定异常的时候给出指定的响应代码或原因,@ResponseBody我们实现Restful接口的时候(@RestController)最常用了,把返回内容(序列化后)输出到请求体 D. Spring MVC给了我们各种注解方便我们从HTTP请求各种地方获取参数,@RequestBody从请求体(处理复杂数据,比如JSON),@RequestHeader从请求头,@CookieValue从cookie中,@SessionAttribute从会话中,@RequestAttribute从请求的Attribute中(比如过滤器和拦截器手动设置的一些临时数据),@RequestParam从请求参数(处理简单数据,键值对),@PathVariable从路径片段,@MatrixAttribute矩阵变量允许我们采用特殊的规则在URL路径后加参数(分号区分不同参数,逗号为参数增加多个值) E. @ControllerAdvice是一个重要注解,允许我们在集中的地方配置控制器(有@RequestMapping的方法)相关的增强(@RestControllerAdvice也是差不多的,只是相当于为@ExceptionHandler加上了@ResponseBody)。那么可以应用哪些增强呢?首先是可以用 @ExceptionHandler进行统一的全局异常处理;第二是 @InitBinder用来设置WebDataBinder,WebDataBinder用来自动绑定前台请求参数到Model中;第三是 @ModelAttribute让全局的@RequestMapping都能获得在此处设置的键值对。当然,这里说的@InitBinder和@ExceptionHandler也可以不定义在@ControllerAdvice内部(作为全局开启),定义在Controller内部应用到某个Controller也是可以的 F. 其它还有一些注解比如:@CrossOrigin可以用到Controller或Method上(需要配合@RequestMapping)设置细粒度的跨域行为 在之前的文章中我们也提到,对于Spring MVC,定义自己的注解应用到参数、方法、控制器上,配合HandlerMethodArgumentResolver、XXAdvise、以及Interceptor实现具体的功能来使用太太常见了,几乎所有的非业务横切关注点,我们都不应该在方法实现中重复任何一行代码。 Spring Boot注解 A. 来看一下上下文相关的注解:@ConfigurationProperties很常用(配合 @EnableConfigurationProperties注解来设置需要启用的配置类),用来自定义配置类和配置文件进行关联;@DeprecatedConfigurationProperty用于标记废弃的配置以及设置替代配置和告知废弃原因;@ConfigurationPropertiesBinding用于指定自定义的转换器用于配置解析的时的类型转换; @NestedConfigurationProperty用于关联外部的类型作为嵌套配置类 B. 再看看自动配置相关的注解,自动配置是Spring Boot最重要的特性,在之前的系列文章中我有提到一个观点,IOC是好事情,但是把组件内部的一些默认配置以及组件和组件的组装交给外部用户来配置其实是不合理的,组件应当可以自动进行自我配置实现开箱急用,只有需要自定义组件的时候才要求外部来进行个性化配置:@EnableAutoConfiguration注解可以启用自动配置,Spring Boot应用程序一般我们会直接使用复合注解@SpringBootApplication;@AutoConfigureOrder(值越小优先级越高)、@AutoConfigureAfter、@AutoConfigureBefore用于设置自动配置类加载顺序,以及精确控制加载依赖关系,有的时候我们的自动配置需要相互依赖或者会相互干扰,需要手动调节 C. 最后来看一下十几种配置条件,用好这些注解是实现完善的自动配置的关键:@ConditionalOnBean用于仅当容器中已经包含指定的Bean类型或名称时才匹配条件;@ConditionalOnClass仅当classpath上存在指定类时条件匹配;@ConditionalOnCloudPlatform仅当指定的云平台处于活动状态时条件匹配;@ConditionalOnExpression依赖于SpEL表达式的值的条件元素的配置注解;@ConditionalOnJava基于应用运行的JVM版本的条件匹配;@ConditionalOnJndi基于JNDI可用和可以查找指定位置的条件匹配;@ConditionalOnMissingBean仅当容器中不包含指定的Bean类型或名称时条件匹配;@ConditionalOnMissingClass仅当classpath上不存在指定类时条件匹配;@ConditionalOnNotWebApplication 仅当不是WebApplicationContext(非Web项目)时条件匹配,对应 @ConditionalOnWebApplication;@ConditionalOnProperty是检查指定的属性是否具有指定的值;@ConditionalOnResource表示仅当 classpath 上存在指定资源时条件匹配;@ConditionalOnSingleCandidate仅当容器中包含指定的Bean类并且可以判断只有单个候选者时条件匹配。其实所有这些实现原理都是扩展SpringBootCondition抽象类(实现之前提到的Condition接口),我们完全可以实现自己的条件注解(配合 @Conditional注解关联到自己实现的SpringBootCondition) Spring Cloud注解在介绍本系列文章的第一篇中我们就提到了,Spring Cloud整齐划一通过各种EnableXXX注解开启某个功能,这里就不对这些注解进行说明了,使用Spring Boot组件的功能非常简单,基本就是引POM+EnableXXX+设置配置文件三部曲。 A. 首先是 Netflix包下的一些注解,各种EnableXXX就不说了,参考前一篇文章,之前没介绍过 @RibbonClient,这个注解用来为负载均衡客户端做一些自定义的配置,可以进一步配置或自定义从哪里获取服务端列表、负载均衡策略、Ping也就是服务鉴活策略等等 B. client包下的 @SpringCloudApplication之前文章中我们也没有使用到,这是一个复合注解就是 @SpringBootApplication+ @EnableDiscoveryClient+ @EnableCircuitBreaker,Spring Cloud那堆东西很多,还是自己亲手定义一个一个功能的注解来的踏实; @LoadBalanced注解用于和RestTemplate配合使用构成一个负载均衡的Http客户端,实现原理上其实这个注解是一个@Qualifier注解,Spring会为所有@LoadBalanced的RestTemplate加入一个LoadBalancerInterceptor(实现ClientHttpRequestInterceptor)实现负载均衡 C. sleuth包下面的注解和链路跟踪相关,比较常用的是通过 @SpanName手动设置span的名称,其它注解对于业务开发并不常用 总结 元注解,也就是注解的注解 Spring容器相关的一些注解,包括@Qualifier、@AliasFor、@Order等看似不重要但其实很重要的注解 Spring Java配置相关的一些注解,包括条件注解 Spring Boot自动配置相关的一些注解 很多注解可以同时应用到类型、方法、参数上,有的时候应用到不同的地方作用会略微不一样,这个需要重点关注。我们知道注解其实只是一个标识,注解如何起作用背后的实现原理还是比较多样的,你可以进一步结合本文介绍的Spring的各种注解探寻一下背后实现的原理 转自筱进GG的博客 End","categories":[{"name":"Spring","slug":"spring","permalink":"http://www.aquestian.cn/categories/spring/"}],"tags":[{"name":"Spring","slug":"spring","permalink":"http://www.aquestian.cn/tags/spring/"}],"author":"aqian666"},{"title":"七牛云图片上传并支持Https","slug":"blogs/Https-Qiniu","date":"2019-12-01T16:00:00.000Z","updated":"2019-12-01T16:00:00.000Z","comments":true,"path":"blogs/Https-Qiniu/","link":"","permalink":"http://www.aquestian.cn/blogs/Https-Qiniu/","excerpt":"","text":"写在前面七牛云上传图片的文章不在少数,但他们只是使用了七牛云给的测试域名,作为一个站长把自己的域名升级为https之后,强迫症患者是不允许浏览器出现不安全这三个字的,当然了使用七牛云绑定自己域名时也会出现一些小坑。为了更好的维护自己的网站,对众人来说不一定是完美的,但尽力做到最好的! 准备工作 因为我的个人服务器是阿里云的,所以我在我的阿里云上申请了一个二级域名(阿里云二级域名如何申请:点这里) 再次二级域名申请完成之后,需要购买一个ssl证书,阿里云有免费证书再申请一个即可! 并且下载ssl证书!(不需要Nginx等配置。) 七牛云配置新建存储空间 空间名称可以自定义,访问控制选择公开。 此时你就新建完成了。 配置域名上传https证书,一级域名证书,二级域名证书首先要上传到七牛云上。 空间管理里点击域名即可配置域名,首先绑定一级域名,再新绑定二级域名,绑定完成如下。 当然此处有坑,如果不进行cname,会出现如下问题。 如果直接在一级域名cname,也就是在阿里云新建一个cname,记录值和-A一直,那么就会冲突。如果直接修改-A那么就会出现如下问题。 所以才需要一个二级域名! 代码实现pom引入jar包<!--七牛服务器--> <dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>7.2.11</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>3.3.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.6.2</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.qiniu</groupId> <artifactId>happy-dns-java</artifactId> <version>0.1.4</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>3.3.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.6.2</version> <scope>compile</scope> </dependency> <!-- 请求头参数分析包 --> <dependency> <groupId>eu.bitwalker</groupId> <artifactId>UserAgentUtils</artifactId> <version>1.20</version> </dependency> <!--七牛云上传图片服务--> <dependency> <groupId>com.qiniu</groupId> <artifactId>sdk</artifactId> <version>6.1.0</version> </dependency> 配置SDK这个sdk在你七牛云的秘钥管理下就有 * 类作用描述:命名类 * 创建时间:2018/12/1 17:54 * 构造方法参数: * 修改时间:2018/12/1 17:54 * 创建者:Aqian666 **/ public class QiNiuSdk { // 七牛AK public static final String accessKey = "ak"; // 七牛SK public static final String secretKey = "sk"; // 七牛存储空间名 public static final String bucket = "空间名"; // 七牛默认域名 public static final String domain = "二级域名"; } 代码实现 * 类作用描述:上传图片到服务器 * 创建时间:2018/12/1 17:54 * 构造方法参数: * 修改时间:2018/12/1 17:54 * 创建者:ZENG * 类全限定名称:com.hyxiaojingyu.common.QiniuUpload **/ public class QiniuUpload { //设置好账号的ACCESS_KEY和SECRET_KEY private static String ACCESS_KEY = QiNiuSdk.accessKey; //这两个登录七牛 账号里面可以找到 private static String SECRET_KEY = QiNiuSdk.secretKey; //要上传的空间 private static String bucketname = QiNiuSdk.bucket; //对应要上传到七牛上 你的那个路径(自己建文件夹 注意设置公开) //密钥配置 private static Auth auth = Auth.create(ACCESS_KEY, SECRET_KEY); private static Configuration cfg = new Configuration(Zone.huanan()); //创建上传对象 private static UploadManager uploadManager = new UploadManager(cfg); //简单上传,使用默认策略,只需要设置上传的空间名就可以了 public static String getUpToken(){ return auth.uploadToken(bucketname); } public static String UploadPic(String FilePath,String FileName){ Configuration cfg = new Configuration(Zone.huanan()); UploadManager uploadManager = new UploadManager(cfg); String accessKey = QiNiuSdk.accessKey; //AccessKey的值 String secretKey = QiNiuSdk.secretKey; //SecretKey的值 String bucket = QiNiuSdk.bucket; //存储空间名 Auth auth = Auth.create(accessKey, secretKey); String upToken = auth.uploadToken(bucket); try { Response response = uploadManager.put(FilePath, FileName, upToken); //解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); System.out.println(putRet.key); System.out.println(putRet.hash); return QiNiuSdk.domain+FileName; }catch (QiniuException ex){ Response r = ex.response; System.err.println(r.toString()); try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { //ignore } } return null; } public static String updateFile(MultipartFile file, String filename) throws Exception { //默认不指定key的情况下,以文件内容的hash值作为文件名 try { InputStream inputStream=file.getInputStream(); ByteArrayOutputStream swapStream = new ByteArrayOutputStream(); byte[] buff = new byte[600]; //buff用于存放循环读取的临时数据 int rc = 0; while ((rc = inputStream.read(buff, 0, 100)) > 0) { swapStream.write(buff, 0, rc); } byte[] uploadBytes = swapStream.toByteArray(); try { com.qiniu.http.Response response = uploadManager.put(uploadBytes,filename,getUpToken()); //解析上传成功的结果 DefaultPutRet putRet; putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); return QiNiuSdk.domain+putRet.key; } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { } } } catch (UnsupportedEncodingException ex) { } return null; } } @ResponseBody @PostMapping("/upload") public HashMap<String, Object> uploadImageByCover(MultipartFile file){ HashMap<String,Object> result = new HashMap<>(); try { // 获取文件名称 String fileName = file.getOriginalFilename(); //结果 2017/03/01 String format = DateUtil.format(new Date(), "yyyy-MM-dd-HH-mm-ss"); String newFilename = format+"-"+fileName; String s = QiniuUpload.updateFile(file, newFilename); result.put(&quot;msg&quot;,s); } catch (Exception e) { e.printStackTrace(); } return result; } 测试 End","categories":[{"name":"对象存储","slug":"对象存储","permalink":"http://www.aquestian.cn/categories/%E5%AF%B9%E8%B1%A1%E5%AD%98%E5%82%A8/"}],"tags":[{"name":"Https","slug":"https","permalink":"http://www.aquestian.cn/tags/https/"},{"name":"七牛云","slug":"七牛云","permalink":"http://www.aquestian.cn/tags/%E4%B8%83%E7%89%9B%E4%BA%91/"}],"author":"aqian666"},{"title":"我的程序人生——第二年","slug":"life/Life-Second-Year","date":"2019-12-01T16:00:00.000Z","updated":"2019-12-01T16:00:00.000Z","comments":true,"path":"life/Life-Second-Year/","link":"","permalink":"http://www.aquestian.cn/life/Life-Second-Year/","excerpt":"","text":"程序人生第二年今年年初,离职了,怀着一腔热血去北京想要闯荡一番,但是不知道什么原因,学历?还是经验?还是大环境不友好?面试遇到了大麻烦,收到的面试机会参差不齐,不排除有好的我没有把握住机会,但大部分公司都是很坑的,都需要什么岗前培训,去上班你给他干活完事你还得给他贴钱。在此期间,必须感谢大橙子的建议,否则当时面试四处碰壁的我,很可能会病急乱投医。 困难或者心酸都不说太多,在北京很不顺利,我也没有打肿脸继续装胖子,退而求其次回天津,讽刺的是,会天津俩天就收到三个offer,卓识让我感受到了北京,上海这种城市独一档的特有风情从北京回天津,磨磨蹭蹭,都四月了,瞎眼可见的差距,再不努力恐怕会落后别人越来越多。 入职。除了熟悉新环境新同事以外,我也开始了我的新的工作内容。此时甲方有个很奇怪的需求,就是需要在一个图片里用一个不规则四边形圈出图片上的物体,求出这个物体的所在真实环境中的面积。众所周知,照片是有焦距的,这个功能越想其实越觉得不靠谱,我也打电话到阿里问过阿里客服他们的相关api,也申请了一个,好像是做不到,比如你站在1米处拍一个物体和站在一百米以外拍,所呈现图片中的物体肯定是不一样的,而此时要求出这个物体的面积确是一样的,这就是不合理之处。最后我通过一个海伦定律,结合一个工大教授的算法,尽可能的图片不失衡的情况将此功能实现,最后就是产品和甲方的口水战了。这也是我入这行以来第一次见到这样匪夷所思的需求,毕竟手机壳根据主题自由变换,产品和开发大打出手的事情还是历历在目的。闲暇之余,大橙子的博客给我分享了一波,完事发现赵公子也有自己的博客,瞬间我觉得我也得做一个,得把这个提上日程,也算是今年的一个小小愿望,做个小站长。 gis我相信很多人都开发过,但我们公司项目是和别家公司合作开发的,需要我们去调用合作伙伴的python接口,请求格式是JSON,这也是我第一次做请求别人的数据,为了代码写的尽量好看些,自己封装一个发送请求的util工具类。本来开始只是请求,后边需要定时请求,那我加入boot自带的定时器@Scheduled。后来需要定时任务自定义,那好定时器改为qz。利用下班时间,我找到了一个vue的博客模板,至于为啥钟爱于vue呢?这就可能与我之后的想法有关系了,我一直觉得我需要多维方向发展,学习的方向其实和学习什么内容并不冲突,当然就这个观点也是为后来的事情埋下了伏笔。 六月,一直反感linux的我开始接触它,虽然它一直乌漆嘛黑,但作为一个java开发会一些常规操作即可。linux上手后,开始学习nginx,原来别人嘴里说的不难,自己实践起来才发现原来真的不难,原来是自己一直对新内容有抵触心理,就跟开始学习java一样,接触多了,学会只是时间问题,缺乏的是耐心。 有天牛哥跟我说了一下docker,我当时就是一头雾水,这是个啥,牛哥说类似yum,当时我心里就犯嘀咕:我上个月才用linux呀!但还是硬着头皮尝试了,牛哥想要实现的目标既镜像迁移。奇特的是,我这样一个docker门外汉,在学习的时候并未遇到什么阻碍,仅仅俩天,镜像迁移就可以了。当某件事情你处理的越开心越顺利,那么这就是兴趣点,七月我一心都扑在了docker上,类似nginx,mysql,jdk这些我在docker上尝试了不少,但是热情上了头就会出岔子,我把公司一个新的服务器用Docker搭建了mysql,因为数据加载慢,所以更改了配置文件,而此时我虽然热情高涨但容器和镜像的关系还处于混淆状态,本应该是重启容器,而我却鬼使神差删除了容器并重新启动mysql镜像,这就导致之前的数据库数据全部丢失,之前更改的配置也全部失效。不幸中的万幸,服务器是新的,数据库在本地有备份,还真是热情有多高,冷汗就能冒多少。 接下来几个月公司来了新项目,此前所说的多维发展在此时遇到了挑战,那就是我发现了我的一个致命问题,就是逻辑思维,做编程的张嘴就就是面向对象,逻辑思维…等等,但我遇到一个问题,一头扎进去是常态,为了速度立刻先码代码,结果就会导致出现很多逻辑性错误,不是代码错误,就是写完的代码在我想象的情况下是可以跑通的,而出现另外的一种我没有想到的情况下就出现错误了。就是说我缺乏逻辑上的思考。学习再多内容,docker也好,nginx也罢,回归本质关键还是得写代码处理业务逻辑。这个问题我问了大橙子和赵公子还有牛哥,结论就是——多写。而且还需要把代码尽力写的美如画,idea右侧就有一个评测代码好坏的小东西,写的代码不报任何颜色,最上方有个绿色对号,说明代码耦合度啥的都已经降到最低了。尽管代码写的有点磕磕绊绊,但还是学习到了新内容,websocket和socket,闲暇之余也整了个rabbitmqdemo,clouddemo。我还是得多维方向发展,但也得解决自己本身存在的问题。我的一个朋友劝我,虽然每个人都想活成自己所想的样子,但有时候总得听听别人的意见,不要钻牛角尖。 在vue方面,我了解的内容也越来越全,在一个朋友的帮助下,成功的在我的博客里加入了一些多元化的元素,印象最深的就是父子组件传值,作者在这方面做得很巧妙,到现在还是觉得很奇特。同时,在自己的博客里加入了es索引,并实现代码高亮。 双十一,咬牙买了三年的服务器,域名,备案等等一些列流程走完,年前实现了自己的小小愿望,并不是说自己有个博客就怎么样,而是有一个真正自己创造并且还属于自己的东西,实属难得,通俗来讲,别人有不如自己也有。 新的一年就要开始了,接下来面临的是更大的挑战,也希望自己在一次挑战勉强越来越坚强,也能够学到更多的东西。 结语,虽然依旧很菜,但不努力,就赚不到钱!赚不到就取不到媳妇,娶不到媳妇就….想想就可怕,就写到这吧,我要去搬砖了!","categories":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/categories/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"tags":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/tags/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"author":"aqian666"},{"title":"使用Nginx将Http升级为Https","slug":"blogs/Http-Https","date":"2019-11-30T16:00:00.000Z","updated":"2019-11-30T16:00:00.000Z","comments":true,"path":"blogs/Http-Https/","link":"","permalink":"http://www.aquestian.cn/blogs/Http-Https/","excerpt":"","text":"升级所需环境1.有一台自己的服务器。 2.有自己的域名。 3.ssl认证证书。 4.nginx,并且支持ssl,比较新的版本都会有的! 证书申请阿里云官网搜索ssl证书。购买步骤如下图。 过个半小时左右就会签发完成。 下载证书 解压之后是一个pem和key的文件。 把它上传到服务器起下 nginx配置 server { listen 443 ssl; server_name 你的域名; ssl_certificate cert/ce.pem; ssl_certificate_key cert/ce.key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { root /usr/local/nginx/html; try_files $uri $uri/ /index.html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } #http强制转到https server { listen 80; server_name 你的域名; rewrite ^(.*)$ https://$host$1 permanent; } End","categories":[{"name":"Nginx","slug":"nginx","permalink":"http://www.aquestian.cn/categories/nginx/"}],"tags":[{"name":"Nginx","slug":"nginx","permalink":"http://www.aquestian.cn/tags/nginx/"},{"name":"Https","slug":"https","permalink":"http://www.aquestian.cn/tags/https/"}],"author":"aqian666"},{"title":"ssm+vue前后端分离学生管理系统","slug":"code/Code-Student","date":"2019-10-31T16:00:00.000Z","updated":"2019-10-31T16:00:00.000Z","comments":true,"path":"code/Code-Student/","link":"","permalink":"http://www.aquestian.cn/code/Code-Student/","excerpt":"","text":"项目描述此项目为博主自主研发的学生管理系统,包含了登录,注册,公告,教师,学生,消息,班级,签到打卡等多个模块的管理,也利用iview-admin提供的权限内容,对不同的角色进行了权限分配,也实现了图片上传,文件导入导入等这些零散的功能。 项目不大,但前台vue的确是一个非常不错的框架,一步一个坑,学习到了很多。加我好友领取源码. 运行环境jdk8+jetty+mysql+eclipse+maven+nodejs 项目技术spring+spring mvc+mybatis+iview-admin+vue+shiro 项目截图超级管理员admin,密码 admin 注意事项1.有且只有一个超级管理员admin,密码 admin,对数据库操作不要对其删除,也不建议多添加。 2.请先启动后台项目再启动vue项目,后台端口为8080(不要修改),vue项目启动端口便会自动为8081,我不知道前台端口如何配置,有懂的可以自行配置。 3.vue项目需要安装node.js,百度官网即可安装。nodejs环境必须安装 4.前台启动步骤,请参考此文档https://www.cnblogs.com/webdom/p/8780890.html, 在根目录中按照 shift+右键,然后选择“在此处打开命令窗口”输入 npm run dev 启动 注意npm install 安装相关的模块包,这一步应该已经安装好了,请跳过此步骤 npm run dev启动后会下载插件,如果慢的话,可以更换cnpm","categories":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/categories/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"tags":[{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/tags/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"}],"author":"aqian666"},{"title":"Docker搭建FastDFS文件系统","slug":"blogs/Docker-FastDFS","date":"2019-10-26T16:00:00.000Z","updated":"2019-10-26T16:00:00.000Z","comments":true,"path":"blogs/Docker-FastDFS/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-FastDFS/","excerpt":"","text":"Docker搭建fastdfscentos7怎么安装docker请看 https://blog.csdn.net/qq_36357242/article/details/100144208 查看fastdfs的镜像 docker search fastdfs 拉取镜像 docker pull delron/fastdfs 启动tracker服务 docker run -d --network=host --name tracker -v /home/tracker:/var/fdfs delron/fastdfs tracker 启动storage服务 docker run -d --network=host --name storage -e TRACKER_SERVER=你的ip:22122 -v /home/storage:/var/fdfs -e GROUP_NAME=group1 delron/fastdfs storage 注意指向你服务器的ip地址 docker ps 查看已启动的容器 俩容器已经成功启动了! 进入到storage容器中 docker exec -it 3e54741d7b9b bash 这里说明一下,进入容器中后你会发现进入了一个nginx文件下,是的没错!这个fastdfs容器自带了nginx,重点是因为你现在进入了容器中,而你并不能通过xftp访问到这个文件夹,后面我会讲到文件路径的问题 fastdfs默认的端口有三个8888,23000,22122,分别解释一下(我个人理解,对与不对我不保证,因为我也是第一次玩),8888是默认的nginx代理端口,23000是storage服务端口,22122是tracker服务端口。 https://www.cnblogs.com/smartycity/p/5752865.html,这篇文章解释了! 所以为了图片上传成功,需要开发这三个端口! firewall-cmd --zone=public --permanent --add-port=23000/tcp firewall-cmd --zone=public --permanent --add-port=22122/tcp firewall-cmd --zone=public --permanent --add-port=8888/tcp 默认端口修改 那么我在访问的时候不想用默认端口8888,因为这个端口可能比较常用,被占用了就不好了!那么这时候就可以把它修改一下了。 vi /etc/fdfs/storage.conf 翻到最后一行,如下图,我改为了8001 nginx修改 vi /usr/local/nginx/conf/nginx.conf 如下图 修改完成,重启容器,端口才能生效,还需开启8001端口 docker restart 3e54741d7b9b firewall-cmd --zone=public --permanent --add-port=8001/tcp 我们现在将一张图片上传到fastdfs服务上,看看效果, 再次进入容器 docker exec -it 3e54741d7b9b bash 进入fdfs这个文件下! cd /var/fdfs 运行命令(注1) /usr/bin/fdfs_upload_file /etc/fdfs/client.conf 1.jpg 这时候你会问,你他妈的1.jpg哪来的,当时我整这个时候也是懵逼的,网上到了这一步就是一概而过,很难搞!找了半天,我发现了其中秘密! 在之前的启动服务的时候,细心的伙伴发现了没有 也就是容器挂载的位置! 通过xftp查看home文件夹 会有俩个文件夹,将你的图片1.jpg放到storage文件夹下就可以了! 上传文件 /usr/bin/fdfs_upload_file /etc/fdfs/client.conf 1.jpg 会出现如下图,会返回一个地址 这样就以为这上传成功了! 在浏览器上look一下, 至此,这个文章就算完成了,在我学习的过程中,他们会把转储格式也会贴上来,这里我就不贴了,想了解的大家多百度百度!整体来讲,Docker搭建要比centos简单,简略了关联linux这一块! End","categories":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/categories/docker/"}],"tags":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/tags/docker/"},{"name":"FastDFS","slug":"fastdfs","permalink":"http://www.aquestian.cn/tags/fastdfs/"}],"author":"aqian666"},{"title":"SpringBoot整合FastDFS","slug":"blogs/SpringBoot-FastDFS","date":"2019-10-19T16:00:00.000Z","updated":"2019-10-19T16:00:00.000Z","comments":true,"path":"blogs/SpringBoot-FastDFS/","link":"","permalink":"http://www.aquestian.cn/blogs/SpringBoot-FastDFS/","excerpt":"","text":"接上一篇,docker搭建FastDFS文件系统。 这一篇实现SpringBoot2.0整合Fastdfs。 引入FastDfs依赖<dependency> <groupId<com.github.tobato</groupId> <artifactId<fastdfs-client</artifactId> <version<1.26.2</version> </dependency> application.yml配置文件fdfs: connect-timeout: 600 so-timeout: 1500 trackerList: 127.0.0.1:22122 thumb-image: width: 150 height: 150 pool: max-total: 200 在启动类添加如下配置 @Configuration @SpringBootApplication @MapperScan(\"com.gbq.boot.web.mapper\") @EnableMBeanExport(registration= RegistrationPolicy.IGNORE_EXISTING) @Import(FdfsClientConfig.class) @EnableAsync public class BootApplication { public static void main(String[] args) { SpringApplication.run(BootApplication.class, args); } } controller 接口编写 package com.gbq.boot.web.controller; import cn.hutool.core.lang.UUID; import com.github.tobato.fastdfs.domain.StorePath; import com.github.tobato.fastdfs.proto.storage.DownloadByteArray; import com.github.tobato.fastdfs.service.FastFileStorageClient; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.velocity.shaded.commons.io.FilenameUtils; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.util.HashMap; @RestController @RequestMapping(\"/file\") public class FileController { @Resource private FastFileStorageClient fastFileStorageClient; @Resource private RedisTemplate redisTemplate; /** * 文件上传 * @return result */ @ResponseBody @PostMapping(\"/upload\") public HashMap Object uploadImageByCover(MultipartFile file){ HashMapObject result = new HashMap<(); try { // 获取文件名称 String originalFileName = file.getOriginalFilename(); //fastDfs返回的路径名称 String fastDfsGroup = redisTemplate.opsForValue().get(originalFileName); if(StringUtils.isNotBlank(fastDfsGroup)){ result.put(\"msg\",fastDfsGroup); }else { //上传 StorePath path = fastFileStorageClient. uploadFile(file.getInputStream(),file.getSize(), FilenameUtils.getExtension(file.getOriginalFilename()),null); //获取路径加名称 String picName = path.getFullPath(); //把名称,路径存入redis redisTemplate.opsForValue().set(originalFileName,picName); result.put(\"msg\",picName); } } catch (IOException e) { e.printStackTrace(); } return result; } /** * 文件删除 * @param path * @return */ @DeleteMapping(\"/delete\") public HashMap Object delete(@RequestParam String path) { HashMapObject result = new HashMap<(); // 第一种删除:参数:完整地址 fastFileStorageClient.deleteFile(path); result.put(\"msg\",\"恭喜恭喜,删除成功!\"); // 第二种删除:参数:组名加文件路径 // fastFileStorageClient.deleteFile(group,path); return result; } /** * 文件下载 * @param url 路径 * @return */ @GetMapping(\"/download\") public void downLoad(@RequestParam String url, HttpServletResponse response) throws IOException { String group = url.substring(0, url.indexOf(\"/\")); String path = url.substring(url.indexOf(\"/\") + 1); //文件后缀 String substring = url.substring(url.lastIndexOf(\".\") + 1); byte[] bytes = fastFileStorageClient.downloadFile(group, path, new DownloadByteArray()); response.setCharacterEncoding(\"UTF-8\"); response.setHeader(\"Content-disposition\", \"attachment;filename=\" + URLEncoder.encode(UUID.randomUUID().toString()+\".\"+substring, \"UTF-8\")); // 写出 ServletOutputStream outputStream = response.getOutputStream(); IOUtils.write(bytes, outputStream); } } 测试一下!!!!上传 浏览器访问一下 下载 注意一下上传路径,fastFileStorageClient会加上你的服务器地址+ip,只需要刚刚上传成功路径即可! 删除!把之前的照片删掉! End","categories":[{"name":"对象存储","slug":"对象存储","permalink":"http://www.aquestian.cn/categories/%E5%AF%B9%E8%B1%A1%E5%AD%98%E5%82%A8/"}],"tags":[{"name":"FastDFS","slug":"fastdfs","permalink":"http://www.aquestian.cn/tags/fastdfs/"},{"name":"SpringBoot","slug":"springboot","permalink":"http://www.aquestian.cn/tags/springboot/"}],"author":"aqian666"},{"title":"使用Docker部署jar包并实现镜像迁移","slug":"blogs/Docker-Insatll-jar","date":"2019-05-16T16:00:00.000Z","updated":"2019-08-06T16:00:00.000Z","comments":true,"path":"blogs/Docker-Insatll-jar/","link":"","permalink":"http://www.aquestian.cn/blogs/Docker-Insatll-jar/","excerpt":"","text":"说明 介绍一下,就我个人而言,docker,就是一个虚拟机。大家平时在使用vm等虚拟机,创建linux系统,然后再在这个系统里边安装各种运行项目所需的环境,最后打包项目,启动项目。docker跟这个大同小异,不知道我的感觉对不对,你可以在虚拟机上创建多个linux系统,创建过程中你需要对其分盘,创建密码等等一些列操作,而docker就是可以创建多个容器。你在你的服务器上需要安装mysql,jdk等环境,同样docker容器也需要!但最关键的区别在于——一台新的服务器,你需要重新安装新的环境,项目也需要重新打包,而docker不用,你可以把之前的容器创建为新的镜像,在新的服务器,安装docker,再运行这个镜像就OK了! 设想,你公司的项目做好了,接下来就是给甲方大大去部署这个项目,而你甲方大大只会给你提供一台服务器,而你去了他们公司,只需要拿着你的镜像,给他服务器安装一个docker,运行就ok了,是不是非常省事呢?哦~具体我也不太清楚,方案b就是,你把你的容器生成镜像后,推送到远端,然后从远端拉取镜像,而这个也绝对是可以实现的。 实例 好了进入正题——以一个boot项目为列子。 1.将打包好的jar包,上传到服务器下, 2.安装docker,在contos7下,并启动 sudo yum -y install docker-ce sudo systemctl start docker 3.在刚刚gbq这个文件下,也就是和jar包的相同位置,创建Dockerfile文件夹,编辑 FROM azul/zulu-openjdk:8 VOLUME /gbq ADD docker_test.jar app.jar RUN bash -c ‘touch /app.jar’ ENTRYPOINT [“java”,"-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] dockerFile详解 具体可以看这个 镜像迁移4.生成镜像,注意:空格 . 不可省略 sudo docker build -t springbootdemo .//生成一个叫springbootdemo的镜像 docker images //查看你已有镜像 5.使用容器启动这个镜像 sudo docker run -d -p 8080:8082 springbootdemo // 8080:代理8082 8082:springboot项目端口 docker ps //查看正在运行的容器 docker ps -a //查看所有容器 好了,使用docker启动项目成功。看一下效果,跟linux 后台启动jar包是一个效果 为了更好地查看,我也给大家看一下我服务器的ip(内网,各位访问不了) 项目启动成功了,接下来,我给大家把这个服务器下的运行docker容器,保存为一个新的镜像,再另外一个服务器,启动并且能够访问! 7.先把这个容器停下来 docker stop fe0ca055a52d //后边是你的容器id 通过docker ps查看,如下图 8.将容器转换为一个新的镜像 docker commit fe0ca055a52d bootdemo //fe0ca055a52d 容器id bootdemo 新的镜像名称 docker images //查看是否生成了新的镜像,如下图. 9.把这个镜像保存为一个tar格式文件 docker save -o test.tar bootdemo //test.tar 新生成的文件 bootdemo刚才创建镜像名 ls //可以看到生成了 test.tar文件 10.把test.tar复制到另外一个安装有docker的服务器下,安装过程请参考第2步,如下图 11.加载这个镜像 docker load -i test.tar docker images //查看镜像是否加载成功 如下图 12.运行这个镜像 sudo docker run -d -p 8081:8082 bootdemo // 8081:代理8082 8082:springboot项目端口 docker ps //查看正在运行的容器 至此结束,我也是刚刚研究,所以有大牛觉得不对的地方,请指点!另外还是那句话,有问题群里找我,加我q,都可以。 拜拜!","categories":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/categories/docker/"}],"tags":[{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/tags/docker/"}],"author":"aqian666"},{"title":"SpringBoot实现QQ登录","slug":"blogs/QQ-Loing","date":"2018-12-24T16:00:00.000Z","updated":"2018-12-24T16:00:00.000Z","comments":true,"path":"blogs/QQ-Loing/","link":"","permalink":"http://www.aquestian.cn/blogs/QQ-Loing/","excerpt":"","text":"QQ互联注册一个账号网站地址:https://connect.qq.com/,添加一个应用,具体怎么申请以及需要填写的信息,腾讯官网有详细文档。注册并完成相应信息填写后,可以在应用管理中查到应用的APP ID和APP Key。(注,这个申请还是比较麻烦的,申请了好几次,可能是脸黑吧)成功后如下图 添加回调地址 代码编写加入jar包<dependency> <groupId>com.qq</groupId> <artifactId>Sdk4J</artifactId> <version>2</version> </dependency> 登录页面<button type="submit" class="btn btn-default" οnclick="qqLogin()">qq登录</button> function qqLogin() { window.open("/login/qqLogin","TencentLogin"); } Controllerpackage com.gbq.boot.web.controller; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONObject; import com.gbq.boot.web.bean.User; import com.gbq.boot.web.comment.qqLoginComment.AuthComment; import com.gbq.boot.web.service.UserService; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; /** * 登录Controller * @author 阿前 * 2019年1月4日09:48:21 */ @RestController @RequestMapping("/login") public class LoginController { @Resource private UserService userService; @Resource private AuthComment authComment; @RequestMapping("/index") public ModelAndView index(@ModelAttribute("user") User user) { return new ModelAndView("/shop/index","user",user); } @RequestMapping("/login.html") public ModelAndView toLogin() { return new ModelAndView("login"); } @RequestMapping("/qqLogin") public void qqLogin(HttpServletResponse response)throws Exception{ //随机产生字符串 String state = StrUtil.uuid(); String url = authComment.getAuthUrl(state); System.out.println(url); //重定向 response.sendRedirect(url); } @GetMapping("/redirect") public ModelAndView getData(@RequestParam(value = "code") String code, RedirectAttributes model){ //获取token String accessToken = authComment.getAccessToken(code); System.out.println("accessToken"+accessToken); //获取openId String openId = authComment.getOpenId(accessToken); System.out.println("openId"+openId); //获取用户信息 JSONObject userInfo = authComment.getUserInfo(accessToken, openId); String myName = userInfo.getString("nickname"); User user = new User(null, "","111111",myName, System.currentTimeMillis(),"是", userInfo.getString("figureurl_2"), userInfo.getString("gender") ,1,1,"", "", openId); //通过openId查询 User usr = userService.findUsrByOpenId(openId); if (null != usr){ user.setId(usr.getId()); userService.updateById(user); }else { userService.insert(user); } model.addFlashAttribute("user", user); //重定向 return new ModelAndView("redirect:/login/index"); } } AuthComment类编写package com.gbq.boot.web.comment.qqLoginComment; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.apache.commons.io.IOUtils.toByteArray; @Component public class AuthComment { //QQ 登陆页面的URL private final static String AUTHORIZATION_URL = "https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s"; //获取token的URL private final static String ACCESS_TOKEN_URL = "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s"; // 获取用户 openid 的 URL private static final String OPEN_ID_URL = "https://graph.qq.com/oauth2.0/me?access_token=%s"; // 获取用户信息的 URL,oauth_consumer_key 为 apiKey private static final String USER_INFO_URL = "https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s"; // 下面的属性可以通过配置读取 // QQ 在登陆成功后回调的 URL,这个 URL 必须在 QQ 互联里填写过 private static final String CALLBACK_URL = "http://127.0.0.1:8080/login/redirect"; // QQ 互联应用管理中心的 APP ID private static final String APP_ID = "你的id"; // QQ 互联应用管理中心的 APP Key private static final String APP_SECRET = "你的key"; /** * QQ 登陆页面的URL * @param scope * @return */ public String getAuthUrl(String scope) { return String.format(AUTHORIZATION_URL, APP_ID, CALLBACK_URL, scope); } /** * 获取Access Token值 */ public String getAccessToken(String code){ String ur = String.format(ACCESS_TOKEN_URL, APP_ID, APP_SECRET,code, CALLBACK_URL); String compile = "access_token=(\\\\w*)&"; String result = this.getUrl(ur); return this.getMatcher(result,compile); } /** * 获取openId * @param accessToken * @return */ public String getOpenId(String accessToken) { String url = String.format(OPEN_ID_URL, accessToken); String compile = "openid\\":\\"(\\\\w*)\\""; String result = this.getUrl(url); return this.getMatcher(result,compile); } /** * 获取qq用户信息 * @param accessToken * @param openId * @return */ public JSONObject getUserInfo(String accessToken, String openId) { String url = String.format(USER_INFO_URL, accessToken, APP_ID, openId); String result = this.getUrl(url); return JSON.parseObject(result); } private String getMatcher(String result,String compile) { //使用正则表达式解析网址 Pattern p = Pattern.compile(compile); Matcher m = p.matcher(result); m.find(); return m.group(1); } //解析url private String getUrl(String ur) { try { URL url = new URL(ur); HttpURLConnection conn = null; conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5 * 1000); conn.setRequestMethod("GET"); InputStream inStream = conn.getInputStream(); byte[] data = toByteArray(inStream); String result = new String(data, "UTF-8"); System.out.println(result); return result; } catch (IOException e) { e.printStackTrace(); } return null; } } 此处不再编写userService 成功会返回json串 其中主要需要的是,nickname——qq名称,figureurl_qq_x——不同尺寸的qq头像,等等等等! ## 登录成功跳转到页面 成功页面index<span>欢迎你,${user.name}</span> freemarker配置注意我使用的是freemarker模板,给大家贴上freemarker配置,已经mvc配置 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> freemarker: template-loader-path: classpath:/templates/ # 是否启用模板缓存。 suffix: .ftl cache: false # 模板编码。 charset: UTF-8 # 是否检查模板位置是否存在。 check-template-location: true content-type: text/html #是否启用freemarker enabled: true mvc: view: prefix: /templates/ suffix: .html static-path-pattern: /static/** END","categories":[{"name":"QQ登录","slug":"qq登录","permalink":"http://www.aquestian.cn/categories/qq%E7%99%BB%E5%BD%95/"}],"tags":[{"name":"QQ登录","slug":"qq登录","permalink":"http://www.aquestian.cn/tags/qq%E7%99%BB%E5%BD%95/"},{"name":"SpringBoot","slug":"springboot","permalink":"http://www.aquestian.cn/tags/springboot/"}],"author":"aqian666"},{"title":"我的程序人生——第一年","slug":"life/Life-Frist-Year","date":"2018-11-30T16:00:00.000Z","updated":"2018-11-30T16:00:00.000Z","comments":true,"path":"life/Life-Frist-Year/","link":"","permalink":"http://www.aquestian.cn/life/Life-Frist-Year/","excerpt":"","text":"程序人生第一年这一年多以来,经历了很多,虽然也有好的事情,但心酸的事情也是蛮多的。我这个人呢又比较矫情,各位看客,尤其是新人,请把心情收拾一下,接下来我会叙述我代码生涯的第一年,从菜鸟到成熟,从工作到生活,从学校到社会,以及面试经历等等。 2017年7月,因为我本就是一个计算机系的学生,但自己也没有好好学习,可脑子里还是有点由英语单词拼成的零零散散的代码,所以选择某家培训机构去学习,为期4个月。7.15号,正式开课,从h5开始讲起,这里不得不说一下,不是说h5没有用,而这是培训机构的心机所在,h5是可以直观看到效果的,而且也很简单,一个礼拜就学完了,还会再给一个礼拜时间再做一个h5的项目。在这里我想提醒各位新人,h5不是不需要学,而是你在学习的过程中,千万不要以为原来代码很简单这样的一种错觉,也不要有俩天打鱼三天晒网的心理。 2017年8月,开始讲JavaEE,这时候会比较枯燥了,因为老师开始讲一些理论性的知识,不要忽视这些知识,笔试面试的时候都会有的,最好做好笔记。就这样,8月一个月,每天会重复n次的set,get,syso+Alt+/,if,else,for,while,break等等一些东西,枯燥的日子日复一日,有时候也不免觉得烦了,拿上别人写好的代码据为己有,稍作修改变成自己的作业,蒙混过关。每天下午也会犯困,也会任性的倒下睡一觉,起来还会自我安慰,没讲啥,接下来好好听。JaveEE结束后,我们又做了一个项目,纯控制台输入输出的项目。你可能听起来觉得这是个什么破项目啊,但这个项目能让我们对Java的三大特性的理解,以及在未来项目中的编程逻辑,思维等等会有一个质的改变。 2017年9月,半个月的mysql,mysql的安装需要注意下,一不小心操作失误,装错了,有可能面临重装系统的可能。但好像现在的mysql5.7等等,已经比之前的版本要好安装很多了,装错了也好处理了很多。接下来就是创建表,删除表,增删改查语句等等。后半个月,进入了servlet,导入所需要的jar包,连接数据库,把每一层代码都规范化,转发,重定向等等一系列。再给半个月时间做个项目,我还记得我们不知道从哪里找的易买网的页面,做了一套简单的电商系统。这个项目无非就是增删改查,但第一次从建表到页面到后台都是自己一个人在做,这样也让我第一次体会到了一个项目要考虑到的地方是方方面面的,要实现某一个功能也是需要斟酌一番,怎么才能更简单更有效。 2017年10月,培训已经接近了尾声,我们班有很多同学都是一种什么状态呢——说他不会吧,其实还会点。说他会吧,但好像又很差劲。一瓶不满,半瓶晃悠!令我们庆幸的是,换了一个老师。这个老师比之前那个老师严格好几倍,而且技术也过硬,他自己总结了很多知识,比如插件,jar包,安装包,已经各种知识点等等,他都保存在mybase里边,用的时候就拿出来,这是一个非常好的习惯。他开始给我们讲ssh,但由于国庆还放了7天假,他只给我们讲了一个礼拜,留了一个礼拜再做项目,结果可想而知,比想象中的糟糕还要糟糕。这时候我们开始抱怨,抱怨这个地方坑人。情急之下,这个老师当机立断,教我们现在大多公司都在用的ssm,maven等等内容。就我个人而言,ssm比起ssh有一种莫名的好感,我也不知道为啥,可能是我后来用ssm多了吧,以至于ssh都忘得所剩无几。因为时间有限,老师让我们配置步骤牢牢记住,他说有可能你现在回觉得有些东西你不懂,但等到你真正用到的时候,你就会明白了,后来证明事实如此。 2017年10月20号左右,培训机构以及安排我们出去面试了,班级里的学生来上课的也少了,有的学生已经借口找工作不来上课了。就这样课也就停了,我也四处投简历,每日奔波于市里和宿舍。就这么过了一个礼拜,说实话我是有些崩溃的,但有意识的学生,觉得与其一下找不到工作,不如听老师再讲讲其他的内容。于是我同他们一起,又来到了教室,而且老师也很慷慨(也就是我们后来的老师),这时候给我们讲课他是没有工资的,用五天左右的时间学了点ssm理论性知识。 2017年11月,培训机构的就业老师疯狂的四处给我们投简历,我们也犯小孩脾气,跟老师们置气,有的同学呢已经选择了放弃。 综上所述,就是我的代码培训生涯,表现平平,成绩平平,当时的我也义愤填膺的骂过这家机构,现在想想,坑的确是坑,但刚出学校的我几乎是啥也不会,有这么个地方才让我找到了方向,其实有利有弊吧,只不过利大于弊。所以我这里建议新人,假如想入这一行,自学也好,还是培训也好,首先对自己有个认知——是否适合,因为很有可能是决定人生轨迹的一件事情。 接下来,我给大家说一说,一个菜鸟的面试经历。 11月4日,第一家公司,市区繁华地段,环境很棒。hr是一个有资历的女士,她正在面试我前面的俩个小伙子,这俩个小伙子的举止可能并不讨喜,女士没有问几句就对他们说等消息吧。到我了——我双手把简历递给hr,等她落座以后我再毕恭毕敬地坐到椅子的2/3的位置上,身子不随意前后倾,也不故意挺直,手放到自然的地方,脸上阳光一点。由于我第一次面试,紧张,项目描述一团糟,hr又问到我不会的地方,我还是会支支吾吾的想要掩盖过去。结果呢,Java工程师面试失败,但hr看我有h5项目经历,问我愿不愿意留下来做微信小程序页面开发,她还带我进她们公司参观了她们公司。我没有答应但也没有否决,理由呢我还是想做一个后端开发,但转前端的话也不是不可以接收。hr最后说了一句话让我记忆犹新:小伙子形象挺好的,所以我才想留你下来做小程序,既然你还想做开发,你可以再去找找,如果没有面上合适的,再考虑一下我们这里的小程序,我给你半个月时间考虑。这句话着实让我有点受宠若惊,心底也是由衷的带着一种感激吧。 接下来的几天连续面了好几家,我也总结了一下面试经验。11月8日,我来到了我现在所呆的公司,是我们老板面试的。当我们来到公司时,发现我们同学正在里边面试,不久他们出来后并对我们说,面试挺简单的,就说说做过什么项目。进去以后,我还是跟以往一样。坐下后,我讲述了我的项目经验,我们老板跟我介绍完公司情况后问我有什么还想问的。当时我觉得面试时间也太短了,就提了几个问题,比如公司用什么框架,有没有一个成熟的有开发经验的人带我,公司的开发过什么项目,我具体负责哪块,以及公司未来准备向哪个方向发展等等。这些问题其实有很多都是空炮,目的呢——面试的人多了,可能一个上午,公司可能就会面试5个以上,水平其实都差不多,所以想让老板记住我,而不是出门就把我忘了!在这里我想讲一个我在某天下班回家路上听到一个程序员hr说他面试新人的事:现在这小孩真敢写,简历写的什么熟练掌握某某某,结果我一深问,就懵了。我想说的是,简历呢,没有啥经验或者沟通能力又不太强的人呢,最好还是照实写吧。 在我正式叙述我的上班生活之前,我先说明一下在培训之后,工作之前的我的技术水平——会写简单的增删改查,了解ssm,ssh模仿着写…。我也算是响水不开,开水不响。 11月8日,正式上班,前俩周的时间基本上都在熟悉项目,我也模仿着搭建多模块ssm项目,但是没有成功。第三周,交给我一个简单的任务,测试bug,然后再写个文档。第四周,写一个简单的增删改查,可是熟悉注解的使用。 因为到了年底,也没有新项目,2017年就这样过去了。 2018年2月,我开始写一些相对复杂的功能,从建表开始→用mybatis generator生成mapper.xml映射文件,mapper接口,实体类→dao层,service层,controller层→使用工具类,调用静态方法→创建枚举→使用hasmap返回json对象,前台使用angular.js,怎么传参,怎么取值。 2018年3月,开始学着配置shiro,了解boot。 2018年4月,我在闲暇时刻,写出了属于我自己的项目,也就是我最代码个人主页里的那个项目,虽然项目也没有什么闪光点,但我觉得很满足。 2018年5月,写了第一个到spring 定时任务,了解到了极光推送。 2018年6月,这个月对我来说有一个质的提高,我不再只是写一个功能了,而是一个大模块。经理对我也非常信任,对我说了一句话,能让我代码生涯记一直铭记的一句话——大胆写,写错了就改,把自己的想法加进去。这个模块是商品从入库到出库、上架、退库、退货,一整个流程。在这个过程中我也遇到过十分棘手的问题,但现在回头再看那段时间写的代码,哇~太乱了吧。最让人尴尬的是,本以为自己写的没问题了,结果测试的时候出了问题了,还连累同事陪我加了俩个小时的班。这个模块的代码量比之前多了不止一星半点,对我编程思维的提升也有极大的帮助。 2018年7月,公司走了一个项目负责人,正好公司来了一个小项目,叫我搭个简单的ssm框架,于是我给这个框架配置好了拦截器,过滤器等东西,把登录认证做好,就交给我同事处理了。之后我便进入了让人掉头发期,就是统计。之前的项目负责并没有把之前的工作交接到我的手里,有很多我不知道的东西。活人不会给尿憋死,硬着头皮来吧。由于不太了解项目,所以我只能按照现在的需求重新写。其实也都挺简单的,只是统计嘛,最主要的就是查,sql语句其实是最复杂的,我便开始每一个统计写一个视图,这个烦人到什么程度呢? 我的代码职业生涯——第一年这只是一部分,虽然有的是重复的语句,但拿眼睛一个个对,痛不欲生。 2018年8月,我开始对上个月写好的统计进行Excel导出,了解到些二维数组的内容,并开始学习linux。 2018年11月,我开始做微信公众号开发,这个对我来说是个挑战。做这个我请教了我们项目经理,也被批评了好几次,有时候不会都不敢再去问了,男生嘛,总想要点什么面子。 从11月一直到现在,我完成公众号开发,成功获取到关注人的信息,自定义创建菜单栏,生成二维码带上参数。用时挺长的,而代码呢是使用的我们经理之前使用过的微信项目,一开始我看他代码是懵逼的,到最后完全看懂,心里由衷佩服那些老程序员,代码真的是一层套一层,环环相扣,令人折服。如果从网上找类似的代码,不是没有,而是他们基本上都是为了实现而实现,他们也不能在网上挂上那么多代码,所以这种东西,还得是自己打断点,一步步的走一遍,会发现很多巧妙的部分。过几天我会再写一遍博客,详细介绍一下我的公众号开发。 最后,我自己给我自己一年以来打个分,基本及格吧,不算太好,也不算太差,也希望自己以后生活顺顺利利就好,另外也祝各位同仁飞黄腾达!","categories":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/categories/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"tags":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/tags/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"}],"author":"aqian666"}],"categories":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/categories/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"},{"name":"数据处理","slug":"数据处理","permalink":"http://www.aquestian.cn/categories/%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86/"},{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/categories/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"},{"name":"Netty","slug":"netty","permalink":"http://www.aquestian.cn/categories/netty/"},{"name":"MongoDB","slug":"mongodb","permalink":"http://www.aquestian.cn/categories/mongodb/"},{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/categories/docker/"},{"name":"Java实例","slug":"java实例","permalink":"http://www.aquestian.cn/categories/java%E5%AE%9E%E4%BE%8B/"},{"name":"MySQL","slug":"mysql","permalink":"http://www.aquestian.cn/categories/mysql/"},{"name":"面试题","slug":"面试题","permalink":"http://www.aquestian.cn/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/"},{"name":"Linux","slug":"linux","permalink":"http://www.aquestian.cn/categories/linux/"},{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/categories/springcloud/"},{"name":"视频流","slug":"视频流","permalink":"http://www.aquestian.cn/categories/%E8%A7%86%E9%A2%91%E6%B5%81/"},{"name":"对象存储","slug":"对象存储","permalink":"http://www.aquestian.cn/categories/%E5%AF%B9%E8%B1%A1%E5%AD%98%E5%82%A8/"},{"name":"Vue","slug":"vue","permalink":"http://www.aquestian.cn/categories/vue/"},{"name":"Mybatis","slug":"mybatis","permalink":"http://www.aquestian.cn/categories/mybatis/"},{"name":"Elasticsearch","slug":"elasticsearch","permalink":"http://www.aquestian.cn/categories/elasticsearch/"},{"name":"消息队列","slug":"消息队列","permalink":"http://www.aquestian.cn/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"},{"name":"单点登录","slug":"单点登录","permalink":"http://www.aquestian.cn/categories/%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95/"},{"name":"Redis","slug":"redis","permalink":"http://www.aquestian.cn/categories/redis/"},{"name":"Spring","slug":"spring","permalink":"http://www.aquestian.cn/categories/spring/"},{"name":"Nginx","slug":"nginx","permalink":"http://www.aquestian.cn/categories/nginx/"},{"name":"QQ登录","slug":"qq登录","permalink":"http://www.aquestian.cn/categories/qq%E7%99%BB%E5%BD%95/"}],"tags":[{"name":"程序人生","slug":"程序人生","permalink":"http://www.aquestian.cn/tags/%E7%A8%8B%E5%BA%8F%E4%BA%BA%E7%94%9F/"},{"name":"MySQL","slug":"mysql","permalink":"http://www.aquestian.cn/tags/mysql/"},{"name":"Oracle","slug":"oracle","permalink":"http://www.aquestian.cn/tags/oracle/"},{"name":"SQL Server","slug":"sql-server","permalink":"http://www.aquestian.cn/tags/sql-server/"},{"name":"源码分享","slug":"源码分享","permalink":"http://www.aquestian.cn/tags/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/"},{"name":"kettle","slug":"kettle","permalink":"http://www.aquestian.cn/tags/kettle/"},{"name":"Netty","slug":"netty","permalink":"http://www.aquestian.cn/tags/netty/"},{"name":"WebSocket","slug":"websocket","permalink":"http://www.aquestian.cn/tags/websocket/"},{"name":"MongoDB","slug":"mongodb","permalink":"http://www.aquestian.cn/tags/mongodb/"},{"name":"Docker","slug":"docker","permalink":"http://www.aquestian.cn/tags/docker/"},{"name":"队列","slug":"队列","permalink":"http://www.aquestian.cn/tags/%E9%98%9F%E5%88%97/"},{"name":"SpringBoot","slug":"springboot","permalink":"http://www.aquestian.cn/tags/springboot/"},{"name":"Spring","slug":"spring","permalink":"http://www.aquestian.cn/tags/spring/"},{"name":"Linux","slug":"linux","permalink":"http://www.aquestian.cn/tags/linux/"},{"name":"SqlServer","slug":"sqlserver","permalink":"http://www.aquestian.cn/tags/sqlserver/"},{"name":"SpringCloud","slug":"springcloud","permalink":"http://www.aquestian.cn/tags/springcloud/"},{"name":"Zuul","slug":"zuul","permalink":"http://www.aquestian.cn/tags/zuul/"},{"name":"Turbine","slug":"turbine","permalink":"http://www.aquestian.cn/tags/turbine/"},{"name":"Hystrix","slug":"hystrix","permalink":"http://www.aquestian.cn/tags/hystrix/"},{"name":"Feign","slug":"feign","permalink":"http://www.aquestian.cn/tags/feign/"},{"name":"Ribbon","slug":"ribbon","permalink":"http://www.aquestian.cn/tags/ribbon/"},{"name":"Eureka","slug":"eureka","permalink":"http://www.aquestian.cn/tags/eureka/"},{"name":"Nginx","slug":"nginx","permalink":"http://www.aquestian.cn/tags/nginx/"},{"name":"FFmpeg","slug":"ffmpeg","permalink":"http://www.aquestian.cn/tags/ffmpeg/"},{"name":"FTP","slug":"ftp","permalink":"http://www.aquestian.cn/tags/ftp/"},{"name":"NAS","slug":"nas","permalink":"http://www.aquestian.cn/tags/nas/"},{"name":"Webservice","slug":"webservice","permalink":"http://www.aquestian.cn/tags/webservice/"},{"name":"Vue","slug":"vue","permalink":"http://www.aquestian.cn/tags/vue/"},{"name":"Iview","slug":"iview","permalink":"http://www.aquestian.cn/tags/iview/"},{"name":"Mybatis","slug":"mybatis","permalink":"http://www.aquestian.cn/tags/mybatis/"},{"name":"线程","slug":"线程","permalink":"http://www.aquestian.cn/tags/%E7%BA%BF%E7%A8%8B/"},{"name":"Elasticsearch","slug":"elasticsearch","permalink":"http://www.aquestian.cn/tags/elasticsearch/"},{"name":"反射","slug":"反射","permalink":"http://www.aquestian.cn/tags/%E5%8F%8D%E5%B0%84/"},{"name":"RabbitMQ","slug":"rabbitmq","permalink":"http://www.aquestian.cn/tags/rabbitmq/"},{"name":"消息队列","slug":"消息队列","permalink":"http://www.aquestian.cn/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"},{"name":"JWT","slug":"jwt","permalink":"http://www.aquestian.cn/tags/jwt/"},{"name":"Security","slug":"security","permalink":"http://www.aquestian.cn/tags/security/"},{"name":"Redis","slug":"redis","permalink":"http://www.aquestian.cn/tags/redis/"},{"name":"数据结构","slug":"数据结构","permalink":"http://www.aquestian.cn/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"},{"name":"Https","slug":"https","permalink":"http://www.aquestian.cn/tags/https/"},{"name":"七牛云","slug":"七牛云","permalink":"http://www.aquestian.cn/tags/%E4%B8%83%E7%89%9B%E4%BA%91/"},{"name":"FastDFS","slug":"fastdfs","permalink":"http://www.aquestian.cn/tags/fastdfs/"},{"name":"QQ登录","slug":"qq登录","permalink":"http://www.aquestian.cn/tags/qq%E7%99%BB%E5%BD%95/"}]}