数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

1.使用线程组同步工具SynchronousQueue实现交替打印 class FooBar { private int n; private SynchronousQueue fooTobar; private SynchronousQueue barTofoo; public FooBar(int n) { this.n = n; fooTobar = new SynchronousQueue(); barTofoo = new SynchronousQueue(); } public void foo(Runnable printFoo) throws InterruptedException { for (int i = 0; i < n; i++) { printFoo.run(); fooTobar.put(1); barTofoo.take(); } } public void bar(Runnable printBar) throws InterruptedException { for (int i = 0; i < n; i++) { fooTobar.take(); printBar.run(); barTofoo.put(1); } } } 思路:定义两个同步队列,分别代表foo到bar的消息流,和bar到foo的消息流。在每一轮循环,foo端,先打印,然后向bar端阻塞发送消息阻塞直到bar消费了消息,然后阻塞等待来自bar的消息,收到来自bar的消息后进入下一轮循环;在bar端,首先要阻塞接受来自foo的消息,直到收到了foo的消息,才能执行打印操作,完成打印后,向foo端阻塞发送消息,成功发送消息之后,bar端进入下一轮循环; 2.利用线程组同步工具信号量Semaphore实现交替打印 class FooBar { private int n; private Semaphore fooPrint; private Semaphore barPrint; public FooBar(int n) { this.n = n; fooPrint = new Semaphore(1); barPrint = new Semaphore(0); } public void foo(Runnable printFoo) throws InterruptedException { for (int i = 0; i < n; i++) { fooPrint.acquire(); printFoo.run(); barPrint.release(); } } public void bar(Runnable printBar) throws InterruptedException { for (int i = 0; i < n; i++) { barPrint.acquire(); printBar.run(); fooPrint.release(); } } } 思路:一个信号量(Semaphore)管理许多的许可证(permit)。 为了通过信号量,线程通过调用acquire 请求许可。其实没有实际的许可对象, 信号量仅维护一个计数。许可的数目是固定的, 由此限制了通过的线程数量。其他线程可以通过调用 release 释放许可。而且,许可不是必须由获取它的线程释放。事实上,任何线程都可以释放任意数目的许可,这可能会增加许可数目以至于超出初始数目。本题中维护两个信号量实例,fooPrint和barPrint,其中fooPrint初始化为1个许可,barPrint初始化为0个许可。在foo函数中首先需要获取一个fooPrint的许可才能打印,打印结束后,给barPrint增加一个许可,这个时候fooPrint中没有任何许可barPrint中有一个许可,然后foo进入下一轮循环,但是无法获取许可,会阻塞等待上一轮bar函数执行完;在bar函数中首先需要获取一个barPrint的许可才能打印,打印结束后,给fooPrint增加一个许可,这个时候barPrint中没有任何许可fooPrint中有一个许可,然后bar进入下一轮循环,但是无法获取许可,会阻塞等待上本轮foo函数先执行完;
来源:OSCHINA
发布时间:2020-07-14 15:02:00
开发工具 **Python版本:**3.6.4 相关模块: requests模块 以及一些Python自带的模块。 环境搭建 安装Python并添加到环境变量,pip安装需要的相关模块即可。 相关文件 关注公众号**“python工程狮” 回复 ‘酷狗’**获取。 原理简介 QQ音乐下载器: 分析网页数据之后可以找到下面三个接口: 第一个接口需要根据歌曲名构造完整链接来搜索需要下载的歌曲信息; 第二个接口需要根据第一个接口返回的信息来构造完整链接来获取歌曲下载地址的部分信息; 第三个接口即为歌曲下载地址,需要根据前两个接口获取的信息来构造完整链接。 具体的实现过程见相关文件中的源代码。 酷狗音乐下载器: 分析网页数据之后可以获得以下两个接口: 利用方式类似于QQ音乐下载器,但比QQ音乐下载器简单一些。第二个接口需要第一个接口返回的哈希值来构造完整链接,然后请求第二个接口就可以获取歌曲的下载链接。 具体的实现过程见相关文件中的源代码。 使用演示 QQ音乐下载器: 在cmd窗口运行 QQ_Downloader.py 文件即可。 演示如下图所示: 酷狗音乐下载器: 在cmd窗口运行 KG_Downloader.py 文件即可。 演示如下图所示:
来源:OSCHINA
发布时间:2020-07-14 14:02:00
LeetCode–二维数组中的查找 博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢! 说明 剑指offer,4题,主站 240题 二维数组中的查找 题目 在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 示例: 现有矩阵 matrix 如下: [ [1, 4, 7, 11, 15], [2, 5, 8, 12, 19], [3, 6, 9, 16, 22], [10, 13, 14, 17, 24], [18, 21, 23, 26, 30] ] 给定 target = 5,返回 true。 给定 target = 20,返回 false。 Java 使用线性查找,从右上角开始查找,因为一行的最大值在右上角,如果比当前的值小,那么就向左查询,如果比当前的值大,那么就向右查 class Solution { public boolean findNumberIn2DArray(int[][] matrix, int target) { if(matrix.length == 0 || matrix[0].length == 0){ return false; } int x = 0, y = matrix[0].length - 1; while(x=0){ int num = matrix[x][y]; if(num == target){ return true; }else if(num > target){ y--; }else{ x++; } } return false; } } Go 使用线性查找,从右上角开始查找,因为一行的最大值在右上角,如果比当前的值小,那么就向左查询,如果比当前的值大,那么就向右查 func findNumberIn2DArray(matrix [][]int, target int) bool { y := len(matrix[0])-1 x := 0 for x=0{ if matrix[x][y] == target{ return true }else if matrix[x][y] > target{ y-- }else{ x++ } } return false; } C 使用线性查找,从右上角开始查找,因为一行的最大值在右上角,如果比当前的值小,那么就向左查询,如果比当前的值大,那么就向右查 bool findNumberIn2DArray(int** matrix, int matrixSize, int* matrixColSize, int target){ int x = 0; int y = matrixSize - 1; while(x=0){ if(matrix[x][y] == target){ return true; }else if(matrix[x][y] > target){ y--; }else{ x++; } } return false; } PHP 暴力法 class Solution { /** * @param Integer[][] $matrix * @param Integer $target * @return Boolean */ function findNumberIn2DArray($matrix, $target) { if(count($matrix)==0 || count($matrix[0])==0){ return false; } for($i = 0;$i
来源:OSCHINA
发布时间:2020-07-14 13:47:00
本文经机器之心(微信公众号:almosthuman2014)授权转载,禁止二次转载 原文: https://dev.to/lydiahallie/cs-visualized-useful-git-commands-37p1 作者:Lydia Hallie,机器之心编译,参与:Panda、杜伟 git merge、git rebase、git reset、git revert、git fetch、git pull、git reflog……你知道这些 git 命令执行的究竟是什么任务吗?如果你还有些分不清楚,那千万不能错过这篇文章。 在本文中,熟知 JavaScript、TypeScript、GraphQL、Serverless、AWS、Docker 和 Golang 的 21 岁年轻软件顾问 Lydia Hallie 通过动图形式直观地介绍了这些常用 git 命令的工作过程,包你过目不忘。 尽管 Git 是一款非常强大的工具,但如果我说 Git 用起来简直是噩梦,大多数人也会认同我的说法。我发现在使用 Git 时,在头脑里可视化地想象它会非常有用:当我执行一个特定命令时,这些分支会如何交互,又会怎样影响历史记录?为什么当我在 master 上执行硬重启,force push 到原分支以及 rimraf 我们的 .git 文件夹时,我的同事哭了? 我觉得创建一些最常用且最有用的 Git 命令的可视化示例会是一个完美的用例!下面我将介绍的很多命令都有可选参数——你可以使用这些参数来改变对应命令的行为。而我的示例只会涵盖命令的默认行为,而不会添加(或添加太多)可选配置! 本文作者Lydia Hallie。 合并 拥有多个分支是很方便的,这样可以将不同的新修改互相隔离开,而且还能确保你不会意外地向生产代码推送未经许可或破损的代码修改。但一旦这些修改得到了批准许可,我们就需要将其部署到我们的生产分支中! 可将一个分支的修改融入到另一个分支的一种方式是执行 git merge。Git 可执行两种类型的合并:fast-forward 和 no-fast-forward。现在你可能分不清,但我们马上就来看看它们的差异所在。 Fast-forward (—ff) 在当前分支相比于我们要合并的分支没有额外的提交(commit)时,可以执行 fast-forward 合并。Git 很懒,首先会尝试执行最简单的选项:fast-forward!这类合并不会创建新的提交,而是会将我们正在合并的分支上的提交直接合并到当前分支。 完美!现在,我们在 dev 分支上所做的所有改变都合并到了 master 分支上。那么 no-fast-forward 又是什么意思呢? No-fast-foward (—no-ff) 如果你的当前分支相比于你想要合并的分支没有任何提交,那当然很好,但很遗憾现实情况很少如此!如果我们在当前分支上提交我们想要合并的分支不具备的改变,那么 git 将会执行 no-fast-forward 合并。 使用 no-fast-forward 合并时,Git 会在当前活动分支上创建新的 merging commit。这个提交的父提交(parent commit)即指向这个活动分支,也指向我们想要合并的分支! 没什么大不了的,完美的合并!现在,我们在 dev 分支上所做的所有改变都合并到了 master 分支上。 合并冲突 尽管 Git 能够很好地决定如何合并分支以及如何向文件添加修改,但它并不总是能完全自己做决定。当我们想要合并的两个分支的同一文件中的同一行代码上有不同的修改,或者一个分支删除了一个文件而另一个分支修改了这个文件时,Git 就不知道如何取舍了。 在这样的情况下,Git 会询问你想要保留哪种选择?假设在这两个分支中,我们都编辑了 README.md 的第一行。这篇推荐看下: Git 高级用法 。 如果我们想把 dev 合并到 master,就会出现一个合并冲突:你想要标题是 Hello! 还是 Hey!? 当尝试合并这些分支时,Git 会向你展示冲突出现的位置。我们可以手动移除我们不想保留的修改,保存这些修改,再次添加这个已修改的文件,然后提交这些修改。 完成!尽管合并冲突往往很让人厌烦,但这是合理的:Git 不应该瞎猜我们想要保留哪些修改。 变基(Rebasing) 我们刚看到可通过执行 git merge 将一个分支的修改应用到另一个分支。另一种可将一个分支的修改融入到另一个分支的方式是执行 git rebase。 git rebase 会将当前分支的提交复制到指定的分支之上。 完美,现在我们在 dev 分支上获取了 master 分支上的所有修改。 变基与合并有一个重大的区别:Git 不会尝试确定要保留或不保留哪些文件。我们执行 rebase 的分支总是含有我们想要保留的最新近的修改!这样我们不会遇到任何合并冲突,而且可以保留一个漂亮的、线性的 Git 历史记录。 上面这个例子展示了在 master 分支上的变基。但是,在更大型的项目中,你通常不需要这样的操作。git rebase 在为复制的提交创建新的 hash 时会修改项目的历史记录。 如果你在开发一个 feature 分支并且 master 分支已经更新过,那么变基就很好用。你可以在你的分支上获取所有更新,这能防止未来出现合并冲突。 交互式变基(Interactive Rebase) 在为提交执行变基之前,我们可以修改它们!我们可以使用交互式变基来完成这一任务。交互式变基在你当前开发的分支上以及想要修改某些提交时会很有用。 在我们正在 rebase 的提交上,我们可以执行以下 6 个动作: reword:修改提交信息; edit:修改此提交; squash:将提交融合到前一个提交中; fixup:将提交融合到前一个提交中,不保留该提交的日志消息; exec:在每个提交上运行我们想要 rebase 的命令; drop:移除该提交。 很棒!这样我们就能完全控制我们的提交了。如果你想要移除一个提交,只需 drop 即可。 如果你想把多个提交融合到一起以便得到清晰的提交历史,那也没有问题! 交互式变基能为你在 rebase 时提供大量控制,甚至可以控制当前的活动分支。 重置(Resetting) 当我们不想要之前提交的修改时,就会用到这个命令。也许这是一个 WIP 提交或者可能是引入了 bug 的提交,这时候就要执行 git reset。 git reset 能让我们不再使用当前台面上的文件,让我们可以控制 HEAD 应该指向的位置。 软重置 软重置会将 HEAD 移至指定的提交(或与 HEAD 相比的提交的索引),而不会移除该提交之后加入的修改! 假设我们不想保留添加了一个 style.css 文件的提交 9e78i,而且我们也不想保留添加了一个 index.js 文件的提交 035cc。但是,我们确实又想要保留新添加的 style.css 和 index.js 文件!这是软重置的一个完美用例。 输入 git status 后,你会看到我们仍然可以访问在之前的提交上做过的所有修改。这很好,这意味着我们可以修复这些文件的内容,之后再重新提交它们! 硬重置 有时候我们并不想保留特定提交引入的修改。不同于软重置,我们应该再也无需访问它们。Git 应该直接将整体状态直接重置到特定提交之前的状态:这甚至包括你在工作目录中和暂存文件上的修改。 Git 丢弃了 9e78i 和 035cc 引入的修改,并将状态重置到了 ec5be 的状态。 如何用 Git 优雅回退代码, 这篇你一定要看下。关注微信公众号:Java技术栈,在后台回复:git,可以获取我整理的 N 篇 git 教程,都是干货。 还原(Reverting) 另一种撤销修改的方法是执行 git revert。通过对特定的提交执行还原操作,我们会创建一个包含已还原修改的新提交。 假设 ec5be 添加了一个 index.js 文件。但之后我们发现其实我们再也不需要由这个提交引入的修改了。那就还原 ec5be 提交吧! 完美!提交 9e78i 还原了由提交 ec5be 引入的修改。在撤销特定的提交时,git revert 非常有用,同时也不会修改分支的历史。 拣选(Cherry-picking) 当一个特定分支包含我们的活动分支需要的某个提交时,我们对那个提交执行 cherry-pick!对一个提交执行 cherry-pick 时,我们会在活动分支上创建一个新的提交,其中包含由拣选出来的提交所引入的修改。 假设 dev 分支上的提交 76d12 为 index.js 文件添加了一项修改,而我们希望将其整合到 master 分支中。我们并不想要整个 dev 分支,而只需要这个提交! 现在 master 分支包含 76d12 引入的修改了。 取回(Fetching) 如果你有一个远程 Git 分支,比如在 GitHub 上的分支,当远程分支上包含当前分支没有的提交时,可以使用取回。比如当合并了另一个分支或你的同事推送了一个快速修复时。 通过在这个远程分支上执行 git fetch,我们就可在本地获取这些修改。这不会以任何方式影响你的本地分支:fetch 只是单纯地下载新的数据而已。 现在我们可以看到自上次推送以来的所有修改了。这些新数据也已经在本地了,我们可以决定用这些新数据做什么了。 拉取(Pulling) 尽管 git fetch 可用于获取某个分支的远程信息,但我们也可以执行 git pull。git pull 实际上是两个命令合成了一个:git fetch 和 git merge。当我们从来源拉取修改时,我们首先是像 git fetch 那样取回所有数据,然后最新的修改会自动合并到本地分支中。 很好,我们现在与远程分支完美同步了,并且也有了所有最新的修改! Reflog 每个人都会犯错,但犯错其实没啥!有时候你可能感觉你把 git repo 完全搞坏了,让你想完全删了了事。 git reflog 是一个非常有用的命令,可以展示已经执行过的所有动作的日志。包括合并、重置、还原,基本上包含你对你的分支所做的任何修改。 如果你犯了错,你可以根据 reflog 提供的信息通过重置 HEAD 来轻松地重做! 假设我们实际上并不需要合并原有分支。当我们执行 git reflog 命令时,我们可以看到这个 repo 的状态在合并前位于 HEAD@{1}。那我们就执行一次 git reset,将 HEAD 重新指向在 HEAD@{1} 的位置。 我们可以看到最新的动作已被推送给 reflog。 关注公众号Java技术栈回复"面试"获取我整理的2020最全面试题及答案。 推荐去我的博客阅读更多: 1. Java JVM、集合、多线程、新特性系列教程 2. Spring MVC、Spring Boot、Spring Cloud 系列教程 3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程 4. Java、后端、架构、阿里巴巴等大厂最新面试题 觉得不错,别忘了点赞+转发哦!
来源:OSCHINA
发布时间:2020-07-14 13:40:00
Mac下安装配置Maven并在IDEA中配置 博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢! 下载Maven 下载地址 注意看自己系统,mac/linux下载tar.gz,windows下载zip 设置环境变量 首先解压到usr/local下 打开终端, vim ~/.zshrc ,在后面添加路径 export MAVEN_HOME=/usr/local/apache-maven-3.6.3 export PATH=$PATH:$MAVEN_HOME/bin 注意自己的版本和路径,应用配置 source ~/.zshrc 测试 mvn -v ,出现以下表示安装成功 配置阿里云源 打开 /usr/local/apache-maven-3.6.3/conf/settings.xml 配置文件 alimaven central aliyun maven http://maven.aliyun.com/nexus/content/repositories/central/ 测试,在终端输入 mvn help:system ,下载一些默认的jar包到本地仓库 在 idea 中配置 maven 把默认的修改成我们刚才下载好的 因为仓库我没有改,所以没变,如果改了仓库那么就在这里也要配置好 测试 创建一个maven项目 选择maven版本 然后就会自动补充maven项目所需要的依赖 感谢 万能的网络 以及勤劳的自己
来源:OSCHINA
发布时间:2020-07-17 22:20:00
【一、项目背景】 随着互联网时代的快速发展,便捷人民的生活,提高生活质量,外卖系统应运而生。 人们也喜欢享受着“足不出户,美食到家”的待遇,促使网上订餐行业快速发展。 【二、项目目标】 1. 设计一款应用程序-外卖系统,有文字和图片显示,通过选择一种或多种食物,每种食物可以选择一份或多份,点击结算后,进行总价的统计。 2. 实现标题文字滚动和颜色定时变化。 3. 实现消费多少钱免配送费以及消费多少钱满减。 【三、项目实施】 使用eclipse软件开发,先上效果图,如下图所示。 可以看到在界面上有文字和图片显示,通过选择一种或多种食物,每种食物可以选择一份或多份,结算功能,标题文字滚动加颜色变化的功能。 接下来,小编带大家进行具体的实现,具体的实现步骤如下。 【四、实现步骤】 一、首先实现外卖系统购物车的窗口 public static void main(String[] args) { // TODO Auto-generated method stub Takeout t = new Takeout(); t.setTitle("饶洋外卖"); t.setSize(720,550); t.setVisible(true); } 使用new关键字创建Takeout类; setTitle表示设置界面的标题; setSize(宽,高)表示窗体大小; setVisible(true或false)表示窗体是否可见; 二、添加文字实现 1. 使用到组件有JPanel、JLabel; 2. 添加Takeout类的成员变量; public class Takeout extends JFrame { //成员变量 private JPanel panel01 = new JPanel(); private JLabel label01 = new JLabel("欢迎来到饶洋外卖系统!"); 3. 添加文字说明; Takeout类构造函数: Takeout(){ label01.setFont(new Font("黑体",Font.PLAIN,30)); label01.setForeground(Color.BLUE); panel01.add(label01); panel01.setOpaque(false);//设置透明 this.setLayout(new BorderLayout()); this.add(panel01,BorderLayout.NORTH); this.getContentPane().setBackground(c); 4. 文字(label01)设置显示效果 setFont(newFont(String 字体,int 风格,int 字号)); 字体:TimesRoman, Courier, Arial等; 风格:三个常量 lFont.PLAIN, Font.BOLD, Font.ITALIC; 字号:字的大小(磅数); setForegound设置前景色; setOpaque设置控件透明(ture或false); 布局管理器之BorderLayout(边界布局); 边界布局管理器把容器的的布局分为五个位置:CENTER、EAST、WEST、NORTH、SOUTH。依次相应为:上北(NORTH)、下南(SOUTH)、左西(WEST)、右东(EAST),中(CENTER)。 5. 边界布局特征 能够把组件放在这五个位置的随意一个,假设未指定位置,则缺省的位置是CENTER。 南、北位置控件各占领一行,控件宽度将自己主动布满整行。东、西和中间位置占领一行;若东、西、南、北位置无控件,则中间控件将自己主动布满整个屏幕。若东、西、南、北位置中不管哪个位置没有控件,则中间位置控件将自己主动占领没有控件的位置。 它是窗体、框架的内容窗格和对话框等的缺省布局。 6. getContentPane.setBackground(c)初始化一个容器,设置背景RGB颜色需要在成员变量定义。 private Color c = new Color(197,228,251); 三、添加食物文字、数量(按钮)和图片 需要添加JPanel、 JCheckBox、 JButton 、JLabel等。 1. 添加Takeout类的成员变量 private JPanel panel02 = new JPanel(); private JCheckBox check[] = new JCheckBox[9];//文字(多选框) private JButton amount[] = new JButton[9];//数量(按钮) private JLabel food[] = new JLabel[9];//食物图片 private int num[]=new int[9];//数量数组 2. 在Takeout类的构造函数设置组件的属性 参考代码 Takeout(){ …… check[0]=new JCheckBox(" 雪糕 3.0元",false); check[1]=new JCheckBox(" 薯条 6.0元",false); check[2]=new JCheckBox("爆米花 8.0元",false); check[3]=new JCheckBox(" 热狗 10.0元",false); check[4]=new JCheckBox("汉堡包11.0元",false); check[5]=new JCheckBox("巨无霸16.0元",false); check[6]=new JCheckBox(" 可乐 6.0元",false); check[7]=new JCheckBox(" 果汁 8.0元",false); check[8]=new JCheckBox(" 啤酒 6.0元",false); for(int i=0;i<9;i++){ amount[i]=new JButton("1份"); food[i]=new JLabel(new ImageIcon("image//food"+(i+1)+".jpg")); check[i].setOpaque(false); panel02.add(check[i]); panel02.add(amount[i]); panel02.add(food[i]); num[i]=1; amount[i].addActionListener(my); } panel02.setOpaque(false); this.add(panel02,BorderLayout.CENTER); 四、添加状态信息:JTextArea等 1. 添加Takeout类的成员变量 private JTextArea list=new JTextArea(10,20); private String str=""; 2. 在Takeout类的构造函数设置组件的属性 参考代码 Takeout(){ …… list.setText("状态:未选餐!"); list.setBackground(c); panel02.add(list); } 五、添加按钮和总价框:JPanel、 JButton、JLabel、JTextField等 1. 添加Takeout类的成员变量 private JPanel panel03=new JPanel(); private JButton btn_ok = new JButton("结算"); private JButton btn_cancel = new JButton("清空"); private JLabel label02 = new JLabel("总价:"); private double a[]=new double[9];//食物单价 private double total=0;//总价 2. 在Takeout类的构造函数设置组件的属性 参考代码 Takeout(){ …… panel03.add(btn_ok); panel03.add(btn_cancel); panel03.add(label02); panel03.add(text01); panel03.add(label03); this.add(panel03,BorderLayout.SOUTH); panel03.setOpaque(false); a[0]=3.0; a[1]=6.0; a[2]=8.0; a[3]=10.0; a[4]=11.0; a[5]=16.0; a[6]=6.0; a[7]=8.0; a[8]=6.0; 六、事件处理 定义事件处理类,实现事件监听器 1. 在成员变量添加 private MyListener my = new MyListener(); 2. 在Takeout()内添加 btn_ok.addActionListener(my); btn_cancel.addActionListener(my); 3. 添加事件监听器MyListener(自己命名) private class MyListener implements ActionListener{ @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub //添加事件处理代码 for(int i=0;i<9;i++){ if(e.getSource()==amount[i]){ if(num[i]<9) num[i]++; else num[i]=0; amount[i].setText(num[i]+"份"); } } if(e.getSource()==btn_ok){ total=0; str=""; for(int i=0;i<9;i++){ if(check[i].isSelected()==true){ total=total+a[i]*num[i]; str=str+check[i].getText()+" "+amount[i].getText()+"\n"; } } text01.setText(""+total); list.setText("状态:已选餐:\n"+str+"\n"); } 以上e.getSource()==btn_ok代码完成结算功能。 if(e.getSource()==btn_cancel){ for(int i=0;i<9;i++){ check[i].setSelected(false); amount[i].setSelected(false); num[i]=1; amount[i].setText(num[i]+"份"); } total=0; str=""; text01.setText(""+total); list.setText("状态:未选餐!"); label03.setText("满30免费配送,满100立减10"); } 以上e.getSource()==btn_cancel代码完成清空功能。 【五、思考题1】 现在外卖系统商家因业务需求,每一订单需要配送费5元,在活动期间,购满30元免配送费,满100元立减10元,程序应如何修改? 1. 在成员变量添加以下代码。 private final int FEE=5;//配送费 private JLabel label03 = new JLabel("满30免费配送,满100立减10"); 2. 在Takeout()类构造函数中if(e.getSource()==btn_ok)添加以下的代码。 if(total<30){ label03.setText("还差"+(30-total)+"就免费配送,还差"+(100-total)+"就满100减10"); total=total+FEE; str=str+"配送费 5元"; }else if(total<100){//免费配送,不参与满100-10 label03.setText("免费配送,还差"+(100-total)+"就满100减10"); }else{ label03.setText("免费配送,已参与满100减10,"+(total)+"-10"); total=total-FEE-FEE; } 【六、思考题2】 实现“欢迎来到饶洋外卖系统!”文字滚动和颜色定时变化。程序应如何修改? 1. 在成员变量添加以下代码。 private Color color[]=new Color[]{Color.BLACK,Color.RED, Color.BLUE,Color.LIGHT_GRAY, Color.YELLOW,Color.GREEN, Color.MAGENTA }; private Timer timer;//定时器 private int colorIndex=0;//当前颜色的序号 2. 在Takeout()类构造函数中if(e.getSource()==btn_ok)添加以下的代码。 Takeout(){ ..... 添加timer=new Timer(500,new TimerListener()); //定时的时间间隔(单位ms),定时器监听器(要做的事情) timer.start();//启动定时器 } 3. 实现这个TimerListener()函数。 class TimerListener implements ActionListener{//定时器监听器 @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub //定时时间到了,要做的事情: colorIndex++;// 0 1 2 3 4 5 6 7%7=0 8%7=1 9%7=2 label01.setForeground(color[colorIndex/4 % color.length]); String temp=label01.getText(); label01.setText( temp.substring(1,temp.length())+temp.substring(0,1)); //substring(i,j)截取字符串从序号i(包含)到序号j(不包含) i ~ j-1 } } 以上代码可以实现文字的滚动和颜色变化。 这个项目主要是用Java Swing图形界面开发,Swing包括图形用户界面器件,还有Java中为我们提供了Timer来实现定时任务,最主要涉及到了两个类:Timer和TimerTask。 【七、总结】 1. 主要介绍了JPanel、 JCheckBox、 JLabel、 JButton、 JTextField等组件的基本使用,背景颜色的添加,图片图标的设置,以及相应的事件处理。 2.事件处理函数的添加,难点是运用理解构造函数、内部类的创建。 3. 如果需要本文源码,请在公众号后台回复“ 外卖系统 ”四个字获取。 看完本文有收获?请转发分享给更多的人 IT共享之家 想学习更多Python网络爬虫与数据挖掘知识,可前往专业网站: http://pdcfighting.com/ 想学习更多Python网络爬虫与数据挖掘知识,可前往专业网站: http://pdcfighting.com/
来源:OSCHINA
发布时间:2020-07-17 20:50:00
1.有一堆数字,如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字? 解法1: var arr = [...]int{1, 2, 3, 4, 5, 4, 2, 1, 5, 2, 1} var tmp = make([]int, len(arr), len(arr)) for _, v := range arr { tmp[v]++; } for k, v := range a22 { if v == 1 { fmt.Println(k,v) } } 解法2: package main import "fmt" func main() { nums := [...]int{1, 1, 2, 2, 3, 4, 4} res := 0 for _, v := range nums { res ^= v } fmt.Println(res) } 任何数和 00 做异或运算,结果仍然是原来的数,即 a ^ 0=aa⊕0=a。s 任何数和其自身做异或运算,结果是 00,即 a ^ a=0a⊕a=0。 异或运算满足交换律和结合律,即 a ^ b ^ a=b ^ a ^ a=b ^ (a ^ a)=b ^ 0=ba⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b。 2.编写代码统计出字符串 "学习go语言" 中汉字的数量。 解法1: package main import ( "fmt" ) func main() { str := "学习go语言" var count int ch := []rune(str) for _, v := range ch { if v > 256 { // ASCII码0-255 count++ fmt.Println(string(v)) } //if len(string(v)) >= 3 { //UTF8编码下一个中文汉字由3~4个字节组成 // count++ //} } fmt.Println(count) } 解法2: package main import ( "fmt" ) func main() { str := "学习go语言" count := 0 const startCode = 0x2E80 // ^[⺀-鿿]+$ 中日韩文字的正则表达式 const endCode = 0x9FFF for _, v := range str { if startCode < v && endCode > v { count++ fmt.Println(string(v)) } } fmt.Println(count) } 非英文汉字范围:https://www.cnblogs.com/yunsicai/p/4110522.html if条件判断 func ifDemo1() { score := 65 if score >= 90 { fmt.Println("A") } else if score > 75 { fmt.Println("B") } else { fmt.Println("C") } } func ifDemo2() { if score := 65; score >= 90 { fmt.Println("A") } else if score > 75 { fmt.Println("B") } else { fmt.Println("C") } } 先执行score := 65这一条语句,再根据这个变量值进行判断,它的好处是语句执行完了这个变量就销毁了,对程序来说可以节约程序开销。 3 数组 1 求数组 [1, 3, 5, 7, 8] 所有元素的和 2 找出数组中和为指定值的两个元素的下标,比如从数组 [1, 3, 5, 7, 8] 中找出和为8的两个元素的下标分别为 (0,3) 和 (1,2) 。 //求数组[1, 3, 5, 7, 8]所有元素的和 func Sum() { a := [5]int{1, 3, 5, 7, 8} sum := 0 for _, i := range a { sum += i } fmt.Println(sum) } //找出数组中和为指定值的两个元素的下标,比如从数组[1, 3, 5, 7, 8]中找出和为8的两个元素的下标分别为(0,3)和(1,2)。 func Find() { a := [5]int{1, 3, 5, 7, 8} for i, j := range a { for l, k := range a[i+1:] { if j + k == 8 { fmt.Printf("(%d, %d) ", i, l+i+1) } } } } var num = [...]int{1, 3, 5, 7, 8} for _, value := range num { for i := 0; i < len(num)/2; i++ { // 1/2(O*n3) if value + num[i] == 8 { fmt.Println(value, num[i]) } } } 4.请使用内置的 sort 包对数组 var a = [...]int{3, 7, 8, 9, 1} 进行排序 package main import ( "fmt" "sort" ) func main() { var a = [...]int{3, 7, 8, 9, 1} b := a[:] sort.Ints(b) fmt.Println(b) sort.Sort(sort.Reverse(sort.IntSlice(b))) fmt.Println(b) } 5 map 1 写一个程序,统计一个字符串中每个单词出现的次数。比如:”how do you do”中how=1 do=2 you=1。 2 观察下面代码,写出最终的打印结果。 1. package main import ( "fmt" "strings" ) func wordCount(words string) map[string]int { wordsStr := strings.Split(words, " ") m := make(map[string]int, len(wordsStr)) for _, word := range wordsStr { m[word]++ } return m } //func wordCount(words string) (m map[string]int) { 给返回值命名 // wordsStr := strings.Split(words, " ") // m = make(map[string]int, len(wordsStr)) // for _, word := range wordsStr { // m[word]++ // } // return //} func main() { var str = "how do you do" //fmt.Println(wordCount(str)) for k, v := range wordCount(str) { fmt.Println(k, v) } } 2. package main import ( "fmt" ) func main() { type Map map[string][]int m := make(Map) s := []int{1, 2} s = append(s, 3) fmt.Printf("%+v\n", s) m["why"] = s fmt.Printf("%+v\n", m["why"]) s = append(s[:1], s[2:]...) fmt.Printf("%+v\n", s) fmt.Printf("%+v\n", m["why"]) } [1 2 3] [1 2 3] [1 3] [1 3 3] 3??? 6 分金币 /* 你有50枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。 分配规则如下: a. 名字中每包含1个'e'或'E'分1枚金币 b. 名字中每包含1个'i'或'I'分2枚金币 c. 名字中每包含1个'o'或'O'分3枚金币 d: 名字中每包含1个'u'或'U'分4枚金币 写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币? 程序结构如下,请实现 ‘dispatchCoin’ 函数 */ var ( coins = 50 users = []string{ "Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth", } distribution = make(map[string]int, len(users)) ) func main() { left := dispatchCoin() fmt.Println("剩下:", left) } package main import ( "fmt" "strings" ) var ( coins = 50 users = []string{ "Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth", } distribution = make(map[string]int, len(users)) ) func main() { left := dispatchCoin() fmt.Println("剩下:", left) } func dispatchCoin() (left int) { for _, name := range users { coin := countCoin(name) distribution[name] = coin fmt.Printf("%s 的金币为:%d\n", name, coin) } result := 0 for _, value := range distribution { result += value } fmt.Printf("总使用的金币: %d\n", result) return coins - result } func countCoin(name string) (result int) { if &name != nil { name = strings.ToUpper(name) bs := []byte(name) result = 0 for _, value := range bs { if value == 'E' { result++ } else if value == 'I' { result += 2 } else if value == 'O' { result += 3 } else if value == 'U' { result += 4 } } return } return 0 } //------------------------ func dispatchCoin() int { left := coins for _, name := range users { e := strings.Count(name, "e") + strings.Count(name, "E") i := strings.Count(name, "i") + strings.Count(name, "I") o := strings.Count(name, "o") + strings.Count(name, "O") u := strings.Count(name, "u") + strings.Count(name, "U") sum := e*1 + i*2 + o*3 + u*4 distribution[name] = sum left -= sum } fmt.Println(distribution) return left } 7 去重 1 slice&map package main import ( "fmt" ) func RemoveRepByLoop(slc []int) []int{ var result []int for i := range slc { flag := false for j := range result { if slc[i] == result[j] { flag = true break } } if !flag { result = append(result, slc[i]) } } return result } func RemoveRepByMap(slc []int) []int { var result []int tempMap := map[int]byte{} // 存放不重复主键 for _, e := range slc { l := len(tempMap) tempMap[e] = 0 if len(tempMap) != l { // 加入map后,map长度变化,则元素不重复 result = append(result, e) } } return result } func removeDuplicateElement(arr []string) []string { result := make([]string, 0, len(arr)) temp := map[string]struct{}{} for _, item := range arr { if _, ok := temp[item]; !ok { temp[item] = struct{}{} result = append(result, item) } } return result } // 元素去重 func RemoveRep(slc []int) []int{ if len(slc) < 1024 { // 切片长度小于1024的时候,循环来过滤 return RemoveRepByLoop(slc) } else { // 大于的时候,通过map来过滤 return RemoveRepByMap(slc) } } func main() { arr := []int{6, 3, 5, 4, 3, 2, 1} s := []string{"golang", "python", "golang", "JavaScript", "python"} fmt.Println(RemoveRep(arr)) fmt.Println(removeDuplicateElement(s)) } 2 结构体 // setting_string.go setting_int.go string->int package utils import ( "sort" "sync" ) // 实现 set 集合,变相实现 切片去重 type StringSet struct { m map[string]bool sync.RWMutex } func NewStringSet() *StringSet { return &StringSet{ m: map[string]bool{}, } } func (s *StringSet) Add(items ...string) { s.Lock() defer s.Unlock() if len(items) == 0 { return } for _, item := range items { s.m[item] = true } } func (s *StringSet) Remove(items ...string) { s.Lock() defer s.Unlock() if len(items) == 0 { return } for _, item := range items { delete(s.m, item) } } func (s *StringSet) Has(item string) bool { s.RLock() defer s.RUnlock() _, ok := s.m[item] return ok } func (s *StringSet) Len() int { return len(s.List()) } func (s *StringSet) Clear() { s.Lock() defer s.Unlock() s.m = map[string]bool{} } func (s *StringSet) IsEmpty() bool { if s.Len() == 0 { return true } return false } func (s *StringSet) List() []string { s.RLock() defer s.RUnlock() var list []string for item := range s.m { list = append(list, item) } return list } func (s *StringSet) SortList() []string { s.RLock() defer s.RUnlock() var list []string for item := range s.m { list = append(list, item) } sort.Strings(list) return list } //main.go package main import ( "fmt" "goTest/utils" ) func main() { // int 集合 //s := utils.NewIntSet() // 添加数据 //s.Add(5, 2, 4, 3, 5, 6, 7) // 去重后的值 //fmt.Println(s.List()) // 排序后的值 //fmt.Println(s.SortList()) // string 集合 s2 := utils.NewStringSet() // 添加数据 s2.Add("goland", "python", "study", "goalng", "python", "goland") // 去重后的值 fmt.Println(s2.List()) // 排序后的值 fmt.Println(s2.SortList()) } 8 (结构体)使用“面向对象”的思维方式编写一个学生信息管理系统。 1.学生有id、姓名、年龄、分数等信息 2.程序提供展示学生列表、添加学生、编辑学生信息、删除学生等功能 package main import ( "fmt" "os" "sort" ) type Student struct { ID int Name string Age int8 Score int8 } type Class struct { Map map[int]*Student } //添加学生 func (c *Class) AddStudent() { var id int var name string var age int8 var score int8 fmt.Print("输入id: ") _, err := fmt.Scan(&id) fmt.Print("输入姓名: ") _, err = fmt.Scan(&name) fmt.Print("输入年龄: ") _, err = fmt.Scan(&age) fmt.Print("输入分数: ") _, err = fmt.Scan(&score) if err != nil { fmt.Println("保存出错!") } _, isSave := c.Map[id] if isSave { fmt.Println("学生ID已存在!") return } student := &Student{ ID: id, Name: name, Age: age, Score: score, } c.Map[id] = student fmt.Println("保存成功!") } //查看学生列表 func (c *Class) ShowStudent() { fmt.Printf("\t%s\t%s\t%s\t%s\n", "ID", "姓名", "年龄", "分数") sortID := make([]int, 0) for k := range c.Map { sortID = append(sortID, k) } sort.Ints(sortID) for _, k := range sortID { s := c.Map[k] fmt.Printf("\t%d\t%s\t%d\t%d\n", s.ID, s.Name, s.Age, s.Score) } } //删除学生 func (c *Class) DeleteStudent() { fmt.Print("输入要删除的学生ID:") var id int _, err := fmt.Scan(&id) if err != nil { fmt.Println("ID错误") } _, isSave := c.Map[id] if !isSave { fmt.Println("ID不存在!") } delete(c.Map, id) fmt.Println("删除成功!!") } //修改学生信息 func (c *Class) ChangeStudent() { fmt.Print("输入要修改的学生ID:") var id int _, err := fmt.Scan(&id) if err != nil { fmt.Println("ID错误") } var name string var age int8 var score int8 fmt.Print("输入姓名: ") _, err = fmt.Scan(&name) fmt.Print("输入年龄: ") _, err = fmt.Scan(&age) fmt.Print("输入分数: ") _, err = fmt.Scan(&score) if err != nil { fmt.Println("出错了!") } student := &Student{ ID: id, Name: name, Age: age, Score: score, } c.Map[id] = student fmt.Println("修改成功!") } func main() { c := &Class{} c.Map = make(map[int]*Student, 50) for { fmt.Print("1. 添加 2.查看 3.修改 4.删除 0. 退出\n") var todo int8 _, err := fmt.Scan(&todo) if err != nil { fmt.Println("输入有误!") } switch todo { case 1: c.AddStudent() case 2: c.ShowStudent() case 3: c.ChangeStudent() case 4: c.DeleteStudent() case 0: os.Exit(-1) default: fmt.Println("输入有误!") } } } 9 接口 使用接口的方式实现一个既可以往终端写日志也可以往文件写日志的简易日志库。 package main import ( "fmt" "io" "os" ) type Logger interface { Info(string) } type FileLogger struct { filename string } type ConsoleLogger struct {} /*func (fl *FileLogger) Info(msg string) { var f *os.File var err error if _, err = os.Stat(fl.filename); !os.IsNotExist(err) { f, err = os.OpenFile(fl.filename, os.O_APPEND|os.O_WRONLY, 0666) fmt.Println("open file!") } else { f, err = os.Create(fl.filename) if err != nil { panic(err) } fmt.Println("create file!") } defer f.Close() n, err := io.WriteString(f, msg + "\n") if err != nil { panic(err) } fmt.Printf("写入%d个字节\n", n) }*/ func (fl *FileLogger) Info(msg string) { var f *os.File var err error if checkFileIsExist(fl.filename) { f, err = os.OpenFile(fl.filename, os.O_APPEND|os.O_WRONLY, 0666) fmt.Println("File Exist!") } else { f, err = os.Create(fl.filename) fmt.Println("File Not Exist!") } defer f.Close() n, err := io.WriteString(f, msg + "\n") if err != nil { panic(err) } fmt.Printf("写入%d个字节\n", n) } func checkFileIsExist(filename string) bool { if _, err := os.Stat(filename); os.IsNotExist(err) { return false } return true } func (cl *ConsoleLogger) Info(msg string) { fmt.Println(msg) } func main() { var logger Logger fileLogger := &FileLogger{"log.txt"} logger = fileLogger logger.Info("Hello") logger.Info("study golang") consoleLogger := &ConsoleLogger{} logger = consoleLogger logger.Info("Hello!") logger.Info("study golang!") } --------------------------------------------------------------------------- type logSystem interface { console(interface{}) file(interface{}) } type localLog struct{} func (l localLog) console(log interface{}) { logInfo := log.(string) + "\n" _, _ = os.Stdout.WriteString(logInfo) } func (l localLog) file(log interface{}) { logMsg := log.(string) + "\n" filename := "interface.log" _, err := os.Stat(filename) if err != nil { fmt.Println("文件不存在!需要创建!") _, err := os.Create(filename) if err != nil { panic(err) } // fmt.Println(err) file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_EXCL, 0664) //|os.O_CREATE|os.O_EXCL if err != nil { // fmt.Println(err) fmt.Printf("创建打开文件,报错:%s", err) } defer file.Close() if _, err := file.WriteString(logMsg); err == nil { fmt.Println("写入文件成功!") } else { // fmt.Println("文件写入报错!") // fmt.Println(err) fmt.Printf("文件写入报错!,报错信息:%s", err) } } else { fmt.Println("文件存在!") // fmt.Println(fileStat) file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 066) //|os.O_CREATE|os.O_EXCL if err != nil { fmt.Printf("打开文件错误,报错:%s", err) } defer file.Close() if _, err := file.WriteString(logMsg); err == nil { fmt.Println("写入文件成功!") } else { fmt.Println("文件写入报错!") fmt.Println(err) } } } func logger(log string) { // log := log var logger logSystem var application = localLog{} logger = application logger.console(log) logger.file(log) } func main() { var word string = "study golang!" logger(word) } 10 反射 编写代码利用反射实现一个ini文件的解析器程序。 pass
来源:OSCHINA
发布时间:2020-07-17 20:46:00
前言 Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。简单的说,dubbo就是个服务框架,如果没有分布式的需求,其实是不需要用的,只有在分布式的时候,才有dubbo这样的分布式服务框架的需求,并且本质上是个服务调用的东东,说白了就是个远程服务调用的分布式框架(告别Web Service模式中的WSdl,以服务者与消费者的方式在dubbo上注册)。 很多时候,其实我们使用这个技术的时候,可能都是因为项目需要,所以,我们就用了,但是,至于为什么我们需要用到这个技术,可能自身并不是很了解的,但是,其实了解技术的来由及背景知识,对于理解一项技术还是有帮助的。 关于Dubbo的知识总结了个思维导图 Dubbo 面试题 1、为什么要用 Dubbo? 2、Dubbo 的整体架构设计有哪些分层? 3、默认使用的是什么通信框架,还有别的选择吗? 4、服务调用是阻塞的吗? 5、一般使用什么注册中心?还有别的选择吗? 6、默认使用什么序列化框架,你知道的还有哪些? 7、服务提供者能实现失效踢出是什么原理? 8、服务上线怎么不影响旧版本? 9、如何解决服务调用链过长的问题? 10、说说核心的配置有哪些? 11、Dubbo 推荐用什么协议? 12、同一个服务多个注册的情况下可以直连某一个服务吗? 13、画一画服务注册与发现的流程图? 14、Dubbo 集群容错有几种方案? 15、Dubbo 服务降级,失败重试怎么做? 16、Dubbo 使用过程中都遇到了些什么问题? 17、Dubbo Monitor 实现原理? 18、Dubbo 用到哪些设计模式? 19、Dubbo 配置文件是如何加载到 Spring 中的? 20、Dubbo SPI 和 Java SPI 区别? 21、Dubbo 支持分布式事务吗? 22、Dubbo 可以对结果进行缓存吗? 23、服务上线怎么兼容旧版本? 24、Dubbo 必须依赖的包有哪些? 25、Dubbo telnet 命令能做什么? 26、Dubbo 支持服务降级吗? 27、Dubbo 如何优雅停机? 28、Dubbo 和 Dubbox 之间的区别? 29、Dubbo 和 Spring Cloud 的区别? 30、你还了解别的分布式框架吗? 下面是Dubbo 面试题答案解析 1、为什么要用 Dubbo? 随着服务化的进一步发展,服务越来越多,服务之间的调用和依赖关系也越来越复杂,诞生了面向服务的架构体系(SOA),也因此衍生出了一系列相应的技术,如对服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出等行为进行封装的服务框架。就这样为分布式系统的服务治理框架就出现了,Dubbo 也就这样产生了。 2、Dubbo 的整体架构设计有哪些分层? 接口服务层(Service):该层与业务逻辑相关,根据 provider 和 consumer 的业务设计对应的接口和实现 配置层(Config):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心 服务代理层(Proxy):服务接口透明代理,生成服务的客户端 Stub 和 服务端的 Skeleton,以 ServiceProxy 为中心,扩展接口为 ProxyFactory 服务注册层(Registry):封装服务地址的注册和发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry、RegistryService 路由层(Cluster):封装多个提供者的路由和负载均衡,并桥接注册中心,以Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBlancce 监控层(Monitor):RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService 远程调用层(Protocal):封装 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocal、Invoker 和 Exporter 信息交换层(Exchange):封装请求响应模式,同步转异步。以 Request 和Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer 网络 传输 层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec 数据序列化层(Serialize):可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool 3、默认使用的是什么通信框架,还有别的选择吗? 默认也推荐使用 netty 框架,还有 mina。 4、服务调用是阻塞的吗? 默认是阻塞的,可以异步调用,没有返回值的可以这么做。Dubbo 是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个 Future 对象。 5、一般使用什么注册中心?还有别的选择吗? 推荐使用 Zookeeper 作为注册中心,还有 Redis、Multicast、Simple 注册中心,但不推荐。 6、默认使用什么序列化框架,你知道的还有哪些? 推荐使用 Hessian 序列化,还有 Duddo、FastJson、Java 自带序列化。 7、服务提供者能实现失效踢出是什么原理? 服务失效踢出基于 zookeeper 的临时节点原理。 8、服务上线怎么不影响旧版本? 采用多版本开发,不影响旧版本。 9、如何解决服务调用链过长的问题? 可以结合 zipkin 实现分布式服务追踪。 10、说说核心的配置有哪些? 11、Dubbo 推荐用什么协议? 12、同一个服务多个注册的情况下可以直连某一个服务吗? 可以点对点直连,修改配置即可,也可以通过 telnet 直接某个服务。 13、画一画服务注册与发现的流程图? 14、Dubbo 集群容错有几种方案? 15、Dubbo 服务降级,失败重试怎么做? 可以通过 dubbo:reference 中设置 mock="return null"。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+Mock” 后缀。然后在 Mock 类里实现自己的降级逻辑 16、Dubbo 使用过程中都遇到了些什么问题? 在注册中心找不到对应的服务,检查 service 实现类是否添加了 @service 注解无法连接到注册中心,检查配置文件中的对应的测试 ip 是否正确 17、Dubbo Monitor 实现原理? Consumer 端在发起调用之前会先走 filter 链;provider 端在接收到请求时也是先走 filter 链,然后才进行真正的业务逻辑处理。默认情况下,在 consumer 和 provider 的 filter 链中都会有 Monitorfilter。 MonitorFilter 向 DubboMonitor 发送数据 DubboMonitor 将数据进行聚合后(默认聚合 1min 中的统计数据)暂存到ConcurrentMap statisticsMap,然后使用一个含有 3 个线程(线程名字:DubboMonitorSendTimer)的线程池每隔 1min 钟,调用 SimpleMonitorService 遍历发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的 AtomicReference SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000) SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)将 queue 中的数据写入文件(该线程以死循环的形式来写) SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程池每隔 5min 钟,将文件中的统计数据画成图表 18、Dubbo 用到哪些设计模式? Dubbo 框架在初始化和通信过程中使用了多种设计模式,可灵活控制类加载、权限控制等功能。 工厂模式 Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段: private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi on(); Dubbo 里有很多这种代码。这也是一种工厂模式,只是实现类的获取采用了 JDKSPI 的机制。这么实现的优点是可扩展性强,想要扩展实现,只需要在 classpath下增加个文件就可以了,代码零侵入。另外,像上面的 Adaptive 实现,可以做到调用时动态决定调用哪个实现,但是由于这种实现采用了动态代理,会造成代码调试比较麻烦,需要分析出实际调用的实现类。 装饰器模式 Dubbo 在启动和调用阶段都大量使用了装饰器模式。以 Provider 提供的调用链为例,具体的调用链代码是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具体是将注解中含有 group=provider 的 Filter 实现,按照 order 排序,最后的调用顺序是: EchoFilter -> ClassLoaderFilter -> GenericFilter -> ContextFilter -> ExecuteLimitFilter -> TraceFilter -> TimeoutFilter -> MonitorFilter -> ExceptionFilter 更确切地说,这里是装饰器和责任链模式的混合使用。例如,EchoFilter 的作用是判断是否是回声测试请求,是的话直接返回内容,这是一种责任链的体现。而像ClassLoaderFilter 则只是在主功能上添加了功能,更改当前线程的 ClassLoader,这是典型的装饰器模式。 观察者模式 Dubbo 的 Provider 启动时,需要与注册中心交互,先注册自己的服务,再订阅自己的服务,订阅时,采用了观察者模式,开启一个 listener。注册中心会每 5 秒定时检查是否有服务更新,如果有更新,向该服务的提供者发送一个 notify 消息,provider 接受到 notify 消息后,运行 NotifyListener 的 notify 方法,执行监听器方法。 动态代理模式 Dubbo 扩展 JDK SPI 的类 ExtensionLoader 的 Adaptive 实现是典型的动态代理实现。Dubbo 需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生成代理类的方法,能够做到灵活的调用。生成代理类的代码是 ExtensionLoader 的 createAdaptiveExtensionClassCode 方法。代理类主要逻辑是,获取 URL 参数中指定参数的值作为获取实现类的 key。 19、Dubbo 配置文件是如何加载到 Spring 中的? Spring 容器在启动的时候,会读取到 Spring 默认的一些 schema 以及 Dubbo 自定义的 schema,每个 schema 都会对应一个自己的 NamespaceHandler,NamespaceHandler 里面通过 BeanDefinitionParser 来解析配置信息并转化为需要加载的 bean 对象! 20、Dubbo SPI 和 Java SPI 区别? JDK SPI: JDK 标准的 SPI 会一次性加载所有的扩展实现,如果有的扩展吃实话很耗时,但也没用上,很浪费资源。所以只希望加载某个的实现,就不现实了 DUBBO SPI: 1、对 Dubbo 进行扩展,不需要改动 Dubbo 的源码 2、延迟加载,可以一次只加载自己想要加载的扩展实现。 3、增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。 4、Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。 21、Dubbo 支持分布式事务吗? 目前暂时不支持,可与通过 tcc-transaction 框架实现 介绍:tcc-transaction 是开源的 TCC 补偿性分布式事务框架 TCC-Transaction 通过 Dubbo 隐式传参的功能,避免自己对业务代码的入侵。 22、Dubbo 可以对结果进行缓存吗? 为了提高数据访问的速度。Dubbo 提供了声明式缓存,以减少用户加缓存的工作量 其实比普通的配置文件就多了一个标签 cache="true" 23、服务上线怎么兼容旧版本? 可以用版本号(version)过渡,多个不同版本的服务注册到注册中心,版本号不同的服务相互间不引用。这个和服务分组的概念有一点类似。 24、Dubbo 必须依赖的包有哪些? Dubbo 必须依赖 JDK,其他为可选。 25、Dubbo telnet 命令能做什么? dubbo 服务发布之后,我们可以利用 telnet 命令进行调试、管理。Dubbo2.0.5 以上版本服务提供端口支持 telnet 命令 连接服务 telnet localhost 20880 //键入回车进入 Dubbo 命令模式。 查看服务列表 dubbo>ls com.test.TestService dubbo>ls com.test.TestService create delete query · ls (list services and methods) · ls : 显示服务列表。 · ls -l : 显示服务详细信息列表。 · ls XxxService:显示服务的方法列表。 · ls -l XxxService:显示服务的方法详细信息列表。 26、Dubbo 支持服务降级吗? 以通过 dubbo:reference 中设置 mock="return null"。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+Mock” 后缀。然后在 Mock 类里实现自己的降级逻辑 27、Dubbo 如何优雅停机? Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果使用kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行。 28、Dubbo 和 Dubbox 之间的区别? Dubbox 是继 Dubbo 停止维护后,当当网基于 Dubbo 做的一个扩展项目,如加了服务可 Restful 调用,更新了开源组件等。 29、Dubbo 和 Spring Cloud 的区别? 根据微服务架构在各方面的要素,看看 Spring Cloud 和 Dubbo 都提供了哪些支持。 使用 Dubbo 构建的微服务架构就像组装电脑,各环节我们的选择自由度很高,但是最终结果很有可能因为一条内存质量不行就点不亮了,总是让人不怎么放心,但是如果你是一名高手,那这些都不是问题;而 Spring Cloud 就像品牌机,在Spring Source 的整合下,做了大量的兼容性测试,保证了机器拥有更高的稳定性,但是如果要在使用非原装组件外的东西,就需要对其基础有足够的了解。 30、你还了解别的分布式框架吗? 别的还有 spring 的 spring cloud,facebook 的 thrift,twitter 的 finagle 等
来源:OSCHINA
发布时间:2020-07-17 19:33:00
StatefulSet 是一种用于管理有状态应用的 Pod 控制器, TODO: 因依赖Service及存储卷,需要在介绍完Service和存储卷之后再介绍。
来源:OSCHINA
发布时间:2020-07-17 18:38:00
目录 : 技术一面(23问) 技术二面(3大块) JAVA开发技术面试中可能问到的问题(17问) JAVA方向技术考察点(33快) 项目实战(7大块) 必会知识(48点) 面试小技巧注意事项 1. 阿里技术一面 Java IO流的层次结构? 请说出常用的异常类型? SKU的全称是什么,SKU与SPU的区别及关系? FileInputStream在使用完以后,不关闭流,想二次使用可以怎么操作? 设计一个分步式登录系统? Spring加载过程? 自己有没有写过类似Spring这样的AOP事务? Java中 try..catch关闭流的语法糖? 如何设计一个秒杀系统?要考虑什么? 有没有遇到进线上GC,出现的症状是什么样的,怎么解决的? spring的加载过程? atomic与 volatile的区别? Thread的 notify给notifyAll的区别? notifiy是唤醒的那一个线程? Thread.sleep唤醒以后是否需要重新竞争? 单例有多少种写法?有什么区别?你常用哪一种单例,为什么用这种? 问一个Thread.join相关的问题? 商品相关模块系统怎么设计数据模型? 写一个JAVA死锁的列子? 如何解决死锁? GC回收算法,及实现原理? HashMap数据存储结构? key重复了怎么办?是如何解决的? Spring AOP的实现原理,底层用什么实现的? 2. 阿里技术二面 电话面试主要考察3块内容 : Java的相关基础知识,开源框架的原理,JVM,多线程,高并发,中间件等; 之前项目经历,运用的技术,遇到的问题,如何解决,个人有什么收获和成长; 对于技术的热情(平时是否看些技术书籍,逛论坛,写博客,写源代码或程序等); 3. JAVA开发技术面试可能问到的问题 我们主要考核的是网络nio分布式数据库高并发大数据 自定义表格的实现? 动态表单设计? in-jvm(必考)以及jmm缓存模型如何调优? 常用的RPC框架 nio和io 并发编程,设计模式 地图组件? hashmap有什么漏洞会导致他变慢? 如何给hashmap的key对象设计他的hashcode? 泛型通配符?在什么情况下使用? 后端方面:redis?分布式框架dubbo(阿里巴巴开源框架)?设计模式? 场景式的问题:秒杀,能列出常见的排队、验证码、库存扣减方式对系统高并发的影响? 能根据实际的需要构建缓存结构提高提高网站的访问速度,熟练使用ehcache、oscache,了解memcache。 了解基于dns轮询的负载均衡,熟练配置web服务器实现负载均衡,程序级能综合使用基于hash或取模等手段实现软负载。 熟悉分布式数据库设计和优化技术,熟练使用mysql、oracle、SqlServer等主流数据库,熟悉hadoop hbase mangodb redis ehcache、oscache memcache。对于大数据量的数据库处理采用分表分库、数据库读写分离、建立缓存等手段优化性能。 熟练掌握lucene,能基于lucene开发大型的搜索引擎,并能用lucene来改善和优化数据库的like查询。 4. JAVA方向技术考察点(补充) : 掌握Java编程语言,包含io/nio/socket/multi threads/collection/concurrency等功能的使用; 熟练掌握jvm(sun hotspot和ibm j9)内存模型、gc垃圾回收调优等技能; 精通JVM,JMM,MVC架构,熟练使用struts2。 熟练使用spring、struts、ibatis构建应用系统。 熟练使用Servlet,jsp,freemark等前端技术。 熟练使用axis搭建基于SOAP协议的WebService服务接口。 熟练使用MAVEN构建项目工程。 熟练使用tomcat等web服务。 熟练使用mysql等关系型数据库,熟悉mysql集群搭建。 熟练使用redis等NOSQL技术。 熟悉tcp、http协议。 熟悉nginx、haproxy等配置。 熟悉javascript、ajax等技术。 熟悉主流分布式文件系统FastDFS等。 熟悉JMS,可熟练使用ActiveMQ。 底层计算机理解内存管理/数据挖掘系统 可靠性和可用性如何理解~ jsp和sever lap对比 数据库到界面,字符集转化 基栈 jvm优化cup高的时候如果分析和监控 java curb突出细节问题 分布式缓存文档如何分流 迁移数据库垂直分割 高并发如何处理前端高并发应用层 LB设计load balance 负载均衡 防网络攻击 数据日志事件监控后通知 数据库事务实现的底层机制 字符串空格输入的网络攻击 Quartz框架的底层原理 数据库同步中不通过数据库引擎直接读日志等方式同步数据 5. 项目部分 缓存的使用,如果现在需要实现一个简单的缓存,供搜索框中的ajax异步请求调用,使用什么结构? 内存中的缓存不能一直存在,用什么算法定期将搜索权重较低的entry去掉? TCP如何保证安全性 红黑树的问题,B+数 JDK1.8中对HashMap的增强,如果一个桶上的节点数量过多,链表+数组的结构就会转换为红黑树。 项目中使用的单机服务器,如果将它部署成分布式服务器? MySQL的常见优化方式、定为慢查询 手写一个线程安全的单例模式 6. 进阿里必会知识 : 算法和数据结构数组、链表、二叉树、队列、栈的各种操作(性能,场景) 二分查找和各种变种的二分查找 各类排序算法以及复杂度分析(快排、归并、堆) 各类算法题(手写) 理解并可以分析时间和空间复杂度。 动态规划(笔试回回有。。)、贪心。 红黑树、AVL树、Hash树、Tire树、B树、B+树。 图算法(比较少,也就两个最短路径算法理解吧) 计算机网络OSI7层模型(TCP4层)每层的协议 url到页面的过程 HTTPhttp/https 1.0、1.1、2.0 get/post以及幂等性 http协议头相关 网络攻击(CSRF、XSS) TCP/IP三次握手、四次挥手 拥塞控制(过程、阈值) 流量控制与滑动窗口 TCP与UDP比较 子网划分(一般只有笔试有) DDos攻击 (B)IO/NIO/AIO三者原理,各个语言是怎么实现的 Netty Linux内核select poll epoll 数据库(最多的还是mysql,Nosql有redis)索引(包括分类及优化方式,失效条件,底层结构) sql语法(join,union,子查询,having,group by) 引擎对比(InnoDB,MyISAM) 数据库的锁(行锁,表锁,页级锁,意向锁,读锁,写锁,悲观锁,乐观锁,以及加锁的select sql方式) 隔离级别,依次解决的问题(脏读、不可重复读、幻读) 事务的ACID B树、B+树 优化(explain,慢查询,show profile) 数据库的范式。 分库分表,主从复制,读写分离。 Nosql相关(redis和memcached区别之类的,如果你熟悉redis,redis还有一堆要问的) 操作系统:进程通信IPC(几种方式),与线程区别 OS的几种策略(页面置换,进程调度等,每个里面有几种算法) 互斥与死锁相关的 linux常用命令(问的时候都会给具体某一个场景) Linux内核相关(select、poll、epoll) 编程语言(这里只说Java):把我之后的面经过一遍,Java感觉覆盖的就差不多了,不过下面还是分个类。 Java基础(面向对象、四个特性、重载重写、static和final等等很多东西) 集合(HashMap、ConcurrentHashMap、各种List,最好结合源码看) 并发和多线程(线程池、SYNC和Lock锁机制、线程通信、volatile、ThreadLocal、CyclicBarrier、Atom包、CountDownLatch、AQS、CAS原理等等) JVM(内存模型、GC垃圾回收,包括分代,GC算法,收集器、类加载和双亲委派、JVM调优,内存泄漏和内存溢出) IO/NIO相关 反射和代理、异常、Java8相关、序列化 设计模式(常用的,jdk中有的) Web相关(servlet、cookie/session、Spring) 7. 面试技巧 1、 答非所问 : 感觉到答案说出来对方会不待见的时候,可以这么做: 有关联性的答非所问,注意强调自己的想法,争取让面试官能够共情,比如深圳的房价可以引 人共情、电商相关的大平台、稳定、有上升空间等。 2、 面试的状态很重要 : 两个例子 : 上月一个成功offer的候选人,非互联网背景,公司也不是一线的,但是面试时表 现出对阿里很强烈的兴趣,HR事后特地向我们提到,对这位候选人印象很深刻; 另一位今天刚挂的候选人,两轮技术都是一举拿下,最后HR面挂了,HR给我们的反馈是,候 选人刚从自己的创业公司离职,貌似情绪很低落,对自己也没什么想法。 综上所述,面试时既不要唯唯诺诺,过于被动,也不要过于夸张,而是在于展现自己对一个不 错机会的尊重和争取,让面试官觉得你对他们,对这个岗位有很大的兴趣。 8. 注意事项 1、阿里比绝大部分公司更看重情怀和梦想 阿里的八字箴言:乐观、聪明、皮实、自省 2、阿里里面的HR都是懂业务的,会结合对候选人的判断和业务的把控进行筛选, 不要忽悠他 们......相信自己,没有做不到的,只有想不到的。 篇幅有限,这里收集了各方面的,当前公司的,还有自己收集总结的,下面的图片截取的有pdf,有如果有需要的自取. 各大公司面试题集合: 简历模板: 链接:  https://pan.baidu.com/s/1DO6XGkbmak7KIt6Y7JQqyw 提取码:fgj6 不知道会不会失效,如果失效 点击(778490892) 或者扫描下面二维码,进群获取,链接补发不过来,谢谢。 最后 欢迎大家评论区一起交流,相互提升;整理资料不易,如果喜欢文章记得点个赞哈,感谢大家支持!!!
来源:OSCHINA
发布时间:2020-07-14 11:34:00
作者:爱宝贝丶 来源:my.oschina.net/zhangxufeng/blog/3096394 在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的。 这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能够一下子思考出个中奥秘。 本文主要针对这个问题,从源码的角度对其实现原理进行讲解。 1、过程演示 关于Spring bean的创建,其本质上还是一个对象的创建,既然是对象,读者朋友一定要明白一点就是,一个完整的对象包含两部分:当前对象实例化和对象属性的实例化。 在Spring中,对象的实例化是通过反射实现的,而对象的属性则是在对象实例化之后通过一定的方式设置的。 这个过程可以按照如下方式进行理解: 理解这一个点之后,对于循环依赖的理解就已经帮助一大步了,我们这里以两个类A和B为例进行讲解,如下是A和B的声明: @Component public class A {  private B b;  public void setB(B b) {    this.b = b;  } } @Component public class B {  private A a;  public void setA(A a) {    this.a = a;  } } 可以看到,这里A和B中各自都以对方为自己的全局属性。这里首先需要说明的一点是,Spring实例化bean是通过ApplicationContext.getBean()方法来进行的。 如果要获取的对象依赖了另一个对象,那么其首先会创建当前对象,然后通过递归的调用ApplicationContext.getBean()方法来获取所依赖的对象,最后将获取到的对象注入到当前对象中。 这里我们以上面的首先初始化A对象实例为例进行讲解。 首先Spring尝试通过ApplicationContext.getBean()方法获取A对象的实例,由于Spring容器中还没有A对象实例,因而其会创建一个A对象,然后发现其依赖了B对象,因而会尝试递归的通过ApplicationContext.getBean()方法获取B对象的实例,但是Spring容器中此时也没有B对象的实例,因而其还是会先创建一个B对象的实例。 读者需要注意这个时间点,此时A对象和B对象都已经创建了,并且保存在Spring容器中了,只不过A对象的属性b和B对象的属性a都还没有设置进去。 在前面Spring创建B对象之后,Spring发现B对象依赖了属性A,因而此时还是会尝试递归的调用ApplicationContext.getBean()方法获取A对象的实例,因为Spring中已经有一个A对象的实例,虽然只是半成品(其属性b还未初始化),但其也还是目标bean,因而会将该A对象的实例返回。 此时,B对象的属性a就设置进去了,然后还是ApplicationContext.getBean()方法递归的返回,也就是将B对象的实例返回,此时就会将该实例设置到A对象的属性b中。 这个时候,注意A对象的属性b和B对象的属性a都已经设置了目标对象的实例了。读者朋友可能会比较疑惑的是,前面在为对象B设置属性a的时候,这个A类型属性还是个半成品。 但是需要注意的是,这个A是一个引用,其本质上还是最开始就实例化的A对象。 而在上面这个递归过程的最后,Spring将获取到的B对象实例设置到了A对象的属性b中了,这里的A对象其实和前面设置到实例B中的半成品A对象是同一个对象,其引用地址是同一个,这里为A对象的b属性设置了值,其实也就是为那个半成品的a属性设置了值。 下面我们通过一个流程图来对这个过程进行讲解: 图中getBean()表示调用Spring的ApplicationContext.getBean()方法,而该方法中的参数,则表示我们要尝试获取的目标对象。 Spring的核心思想 ,建议大家看下。 图中的黑色箭头表示一开始的方法调用走向,走到最后,返回了Spring中缓存的A对象之后,表示递归调用返回了,此时使用绿色的箭头表示。 从图中我们可以很清楚的看到,B对象的a属性是在第三步中注入的半成品A对象,而A对象的b属性是在第二步中注入的成品B对象,此时半成品的A对象也就变成了成品的A对象,因为其属性已经设置完成了。 一张图搞懂Spring bean的完整生命周期 ,建议阅读下。 2、源码讲解 对于Spring处理循环依赖问题的方式,我们这里通过上面的流程图其实很容易就可以理解,需要注意的一个点就是,Spring是如何标记开始生成的A对象是一个半成品,并且是如何保存A对象的。 这里的标记工作Spring是使用ApplicationContext的属性Set singletonsCurrentlyInCreation来保存的,而半成品的A对象则是通过Map> singletonFactories来保存的,这里的ObjectFactory是一个工厂对象,可通过调用其getObject()方法来获取目标对象。在AbstractBeanFactory.doGetBean()方法中获取对象的方法如下: protected T doGetBean(final String name, @Nullable final Class requiredType,    @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {  // 尝试通过bean名称获取目标bean对象,比如这里的A对象  Object sharedInstance = getSingleton(beanName);  // 我们这里的目标对象都是单例的  if (mbd.isSingleton()) {    // 这里就尝试创建目标对象,第二个参数传的就是一个ObjectFactory类型的对象,这里是使用Java8的lamada    // 表达式书写的,只要上面的getSingleton()方法返回值为空,则会调用这里的getSingleton()方法来创建    // 目标对象    sharedInstance = getSingleton(beanName, () -> {      try {        // 尝试创建目标对象        return createBean(beanName, mbd, args);      } catch (BeansException ex) {        throw ex;      }    });  }  return (T) bean; } 这里的doGetBean()方法是非常关键的一个方法(中间省略了其他代码),上面也主要有两个步骤,第一个步骤的getSingleton()方法的作用是尝试从缓存中获取目标对象,如果没有获取到,则尝试获取半成品的目标对象; 如果第一个步骤没有获取到目标对象的实例,那么就进入第二个步骤,第二个步骤的getSingleton()方法的作用是尝试创建目标对象,并且为该对象注入其所依赖的属性。 关注微信公众号:Java技术栈,在后台回复:spring,可以获取我整理的 N 篇最新 Spring 教程,都是干货。 这里其实就是主干逻辑,我们前面图中已经标明,在整个过程中会调用三次doGetBean()方法,第一次调用的时候会尝试获取A对象实例,此时走的是第一个getSingleton()方法,由于没有已经创建的A对象的成品或半成品,因而这里得到的是null,然后就会调用第二个getSingleton()方法,创建A对象的实例,然后递归的调用doGetBean()方法,尝试获取B对象的实例以注入到A对象中。 此时由于Spring容器中也没有B对象的成品或半成品,因而还是会走到第二个getSingleton()方法,在该方法中创建B对象的实例,创建完成之后,尝试获取其所依赖的A的实例作为其属性,因而还是会递归的调用doGetBean()方法。 此时需要注意的是,在前面由于已经有了一个半成品的A对象的实例,因而这个时候,再尝试获取A对象的实例的时候,会走第一个getSingleton()方法,在该方法中会得到一个半成品的A对象的实例。 然后将该实例返回,并且将其注入到B对象的属性a中,此时B对象实例化完成。然后将实例化完成的B对象递归的返回,此时就会将该实例注入到A对象中,这样就得到了一个成品的A对象。 我们这里可以阅读上面的第一个getSingleton()方法: @Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) {  // 尝试从缓存中获取成品的目标对象,如果存在,则直接返回  Object singletonObject = this.singletonObjects.get(beanName);  // 如果缓存中不存在目标对象,则判断当前对象是否已经处于创建过程中,在前面的讲解中,第一次尝试获取A对象  // 的实例之后,就会将A对象标记为正在创建中,因而最后再尝试获取A对象的时候,这里的if判断就会为true  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {    synchronized (this.singletonObjects) {      singletonObject = this.earlySingletonObjects.get(beanName);      if (singletonObject == null && allowEarlyReference) {        // 这里的singletonFactories是一个Map,其key是bean的名称,而值是一个ObjectFactory类型的        // 对象,这里对于A和B而言,调用图其getObject()方法返回的就是A和B对象的实例,无论是否是半成品        ObjectFactory singletonFactory = this.singletonFactories.get(beanName);        if (singletonFactory != null) {          // 获取目标对象的实例          singletonObject = singletonFactory.getObject();          this.earlySingletonObjects.put(beanName, singletonObject);          this.singletonFactories.remove(beanName);        }      }    }  }  return singletonObject; } 这里我们会存在一个问题就是A的半成品实例是如何实例化的,然后是如何将其封装为一个ObjectFactory类型的对象,并且将其放到上面的singletonFactories属性中的。 这主要是在前面的第二个getSingleton()方法中,其最终会通过其传入的第二个参数,从而调用createBean()方法,该方法的最终调用是委托给了另一个doCreateBean()方法进行的,这里面有如下一段代码: protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)  throws BeanCreationException {  // 实例化当前尝试获取的bean对象,比如A对象和B对象都是在这里实例化的  BeanWrapper instanceWrapper = null;  if (mbd.isSingleton()) {    instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);  }  if (instanceWrapper == null) {    instanceWrapper = createBeanInstance(beanName, mbd, args);  }  // 判断Spring是否配置了支持提前暴露目标bean,也就是是否支持提前暴露半成品的bean  boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences    && isSingletonCurrentlyInCreation(beanName));  if (earlySingletonExposure) {    // 如果支持,这里就会将当前生成的半成品的bean放到singletonFactories中,这个singletonFactories    // 就是前面第一个getSingleton()方法中所使用到的singletonFactories属性,也就是说,这里就是    // 封装半成品的bean的地方。而这里的getEarlyBeanReference()本质上是直接将放入的第三个参数,也就是    // 目标bean直接返回    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));  }  try {    // 在初始化实例之后,这里就是判断当前bean是否依赖了其他的bean,如果依赖了,    // 就会递归的调用getBean()方法尝试获取目标bean    populateBean(beanName, mbd, instanceWrapper);  } catch (Throwable ex) {    // 省略...  }  return exposedObject; } 到这里,Spring整个解决循环依赖问题的实现思路已经比较清楚了。对于整体过程,读者朋友只要理解两点: Spring是通过递归的方式获取目标bean及其所依赖的bean的; Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。 结合这两点,也就是说,Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所有bean,直到某个bean没有依赖其他bean,此时就会将该实例返回,然后反递归的将获取到的bean设置为各个上层bean的属性的。   3、小结 本文首先通过图文的方式对Spring是如何解决循环依赖的问题进行了讲解,然后从源码的角度详细讲解了Spring是如何实现各个bean的装配工作的。 关注公众号Java技术栈回复"面试"获取我整理的2020最全面试题及答案。 推荐去我的博客阅读更多: 1. Java JVM、集合、多线程、新特性系列教程 2. Spring MVC、Spring Boot、Spring Cloud 系列教程 3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程 4. Java、后端、架构、阿里巴巴等大厂最新面试题 觉得不错,别忘了点赞+转发哦!
来源:OSCHINA
发布时间:2020-07-14 11:18:00
1.使用synchronized关键字,利用内部对象锁和条件来控制打印顺序 class Foo { private int idOfPrinter; public Foo() { idOfPrinter = 1; } public synchronized void first(Runnable printFirst) throws InterruptedException { while (idOfPrinter!=1) wait(); printFirst.run(); idOfPrinter = 2; notifyAll(); } public synchronized void second(Runnable printSecond) throws InterruptedException { while (idOfPrinter!=2) wait(); printSecond.run(); idOfPrinter = 3; notifyAll(); } public synchronized void third(Runnable printThird) throws InterruptedException { while (idOfPrinter!=3) wait(); printThird.run(); idOfPrinter = 1; notifyAll(); } } 思路:所有线程试图获取内部对象锁,没获取锁的线程阻塞在那里,成功获取到了但是不满足执行条件(idOfPrinter不是本线程期望的),本线程让出锁并进入等待状态,成功获取了内部对象锁并且满足执行条件(idOfPrinter正是本线程期望的)执行打印,再将idOfPrinter的值赋为下一个书序打印线程期望的id,并执行notifyAll();唤醒所有内部对象锁条件等待集上的线程,使其重新进行锁的竞争,再次获得锁之后,重新从wait()处开始运行,并再次检查执行条件,重复上述过程。 2.使用JUC包中的可重入锁和条件来实现打印顺序的控制 public class Foo { private int idOfPrinter; private ReentrantLock lock; private Condition condition; public Foo() { idOfPrinter = 1; lock = new ReentrantLock(); condition = lock.newCondition(); } public void first(Runnable printFirst) throws InterruptedException { lock.lock(); try { while (idOfPrinter!=1) condition.await(); printFirst.run(); idOfPrinter = 2; condition.signalAll(); }finally { lock.unlock(); } } public void second(Runnable printSecond) throws InterruptedException { lock.lock(); try { while (idOfPrinter!=2) condition.await(); printSecond.run(); idOfPrinter = 3; condition.signalAll(); }finally { lock.unlock(); } } public void third(Runnable printThird) throws InterruptedException { lock.lock(); try { while (idOfPrinter!=3) condition.await(); printThird.run(); idOfPrinter = 1; condition.signalAll(); }finally { lock.unlock(); } } } 思路:思路和上面使用synchronized方法没有任何区别,只是换了一种实现方式,synchronized使用java语言层面实现基于锁的控制,可重入锁是从API层面实现基于锁的控制; 3.使用线程同步器countDownLatch来控制一组协同工作的线程 class Foo { private CountDownLatch twolatch; private CountDownLatch threelatch; public Foo() { twolatch = new CountDownLatch(1); threelatch = new CountDownLatch(1); } public void first(Runnable printFirst) throws InterruptedException { printFirst.run(); twolatch.countDown(); } public void second(Runnable printSecond) throws InterruptedException { twolatch.await(); printSecond.run(); threelatch.countDown(); } public void third(Runnable printThird) throws InterruptedException { threelatch.await(); printThird.run(); } } 思路:三个线程需要按照一定的规则进行协同工作(按先后顺序打印),所以可以用线程同步器来控制这一组线程的行为。代码首先定义了两个JUC包中的线程同步器倒计时门栓,分别用来控制打印two的线程和打印three的线程。打印three的线程成功打印的前提是threeLatch为0,但是threeLatch只有在打印two线程成功打印后才能被修改。打印two的线程成功打印的前提是twoLatch为0,但是twoLatch只有在打印one的线程成功打印后才能被修改。由此可以控制三个线程的执行顺序,先one才能two才能three。 4.使用线程同步器SynchronousQueue来控制一组协同工作的线程 public class Foo { private SynchronousQueue threeToTwo; private SynchronousQueue twoToOne; public Foo() { threeToTwo = new SynchronousQueue(); twoToOne = new SynchronousQueue(); } public void first(Runnable printFirst) throws InterruptedException { printFirst.run(); twoToOne.take(); } public void second(Runnable printSecond) throws InterruptedException { twoToOne.put(1); printSecond.run(); threeToTwo.take(); } public void third(Runnable printThird) throws InterruptedException { threeToTwo.put(2); printThird.run(); } } 思路:这个思路和上面使用倒计时门栓的思路是一样,倒计时门栓和线程同步队列都属于线程同步器。同步队列没有长度可言,它虽然实现了队列接口,但是事实上它只是一种将生产者与消费者匹配的机制,单纯的单方向地将消息同生产者传递到消费者。当producer调用一个同步队列的put方法,producer会阻塞直到cosumer调用take方法;反之,当cosumer调用take方法,consumer会阻塞直到producer调用这个同步队列的put方法。上面的代码正式利用了这个特点,来实现线程打印顺序的控制。 5.使用volatile关键字控制线程的打印顺序 class Foo { private volatile int idOfPrinter; public Foo() { idOfPrinter = 1; } public void first(Runnable printFirst) throws InterruptedException { while (idOfPrinter!=1) Thread.sleep(0,10); printFirst.run(); idOfPrinter=2; } public void second(Runnable printSecond) throws InterruptedException { while (idOfPrinter!=2) Thread.sleep(0,10); printSecond.run(); idOfPrinter=3; } public void third(Runnable printThird) throws InterruptedException { while (idOfPrinter!=3) Thread.sleep(0,10); printThird.run(); idOfPrinter=1; } } 思路:volatile关键字的语义有两个:1.禁止指令重排序;2写线程的修改对其他线程来说是立即可见的。volatile关键字虽然不能保证原子性,但是对于满足以下条件的应用场景使用volatile是合适的:1.程序中的运算结果不依赖于volatile变量的当前值;2.volatile变量不需要与其他状态变量共同参与不变约束。对于本体来说,不需要任何时候只有一个写线程其他都是读线程,所以使用volatile是恰当的。使用volatile关键字能够禁止指令重排序,防止一个执行引擎在执行打印操作之前就将idOfPrinter的值改变导致后一个线程比前一个线程先打印的情况,volatile关键字还能够保证其中任何写线程修改之后其他读线程能够立刻读取到最新的操作;
来源:OSCHINA
发布时间:2020-07-14 10:38:00
作者:柯三 juejin.im/post/5e0443ae6fb9a0162277a2c3 送分题 面试官 :有操作过Linux吗? 我 :有的呀 面试官 :我想查看内存的使用情况该用什么命令 我 : free 或者 top 面试官 :那你说一下用free命令都可以看到啥信息 我 :那,如下图所示 可以看到内存以及缓存的使用情况 total 总内存 used 已用内存 free 空闲内存 buff/cache 已使用的缓存 avaiable 可用内存 面试官 :那你知道怎么清理已使用的缓存吗(buff/cache) 我 :em... 不知道 面试官 : sync; echo 3 > /proc/sys/vm/drop_caches 就可以清理 buff/cache 了,你说说我在线上执行这条命令做好不好? 我 :(送分题,内心大喜)好处大大的有,清理出缓存我们就有更多可用的内存空间, 就跟pc上面xx卫士的小火箭一样,点一下,就释放出好多的内存 面试官 :em...., 回去等通知吧 再谈SQL Join 面试官 :换个话题,谈谈你对join的理解 我 :好的(再答错就彻底完了,把握住机会) 回顾 SQL中的join可以根据某些条件把指定的表给结合起来并将数据返回给客户端 join 的方式有: 5 种 Join 连接及实战案例! inner join 内连接 left join 左连接 right join 右连接 full join 全连接 以上图片源: https://www.cnblogs.com/reaptomorrow-flydream/p/8145610.html 面试官 :在项目开发中如果需要使用 join 语句,如何优化提升性能? 我 :分为两种情况,数据规模小的,数据规模大的。 面试官 : 然后? 我 :对于 数据规模较小 全部干进内存就完事了嗷 数据规模较大 可以通过增加索引来优化 join 语句的执行速度 可以通过冗余信息来减少 join 的次数 尽量减少表连接的次数,一个SQL语句表连接的次数不要超过5次 面试官 :可以总结为 join 语句是相对比较耗费性能,对吗? 我 :是的 面试官 : 为什么? 缓冲区 我 : 在执行join语句的时候必然要有一个比较的过程 面试官 : 是的 我 :逐条比较两个表的语句是比较慢的,因此我们可以把两个表中数据依次读进一个 内存块 中, 以MySQL的InnoDB引擎为例,使用以下语句我们必然可以查到相关的内存区域 show variables like '%buffer%' 如下图所示 join_buffer_size 的大小将会影响我们 join 语句的执行性能 面试官 : 除此之外呢? 一个大前提 我 :任何项目终究要上线,不可避免的要产生数据,数据的规模又不可能太小 面试官 : 是这样的 我 :大部分数据库中的数据最终要保存到 硬盘 上,并且以文件的形式进行存储。 以 MySQL 的InnoDB引擎为例 InnoDB以 页 (page)为基本的IO单位,每个页的大小为16KB InnoDB会为每个表创建用于存储数据的 .ibd 文件 验证 我 :这意味着我们有多少表要连接就需要读多少个文件,虽然可以利用索引,但还是免不了频繁的移动硬盘的磁头 面试官 :也就是说频繁的移动磁头会影响性能对吧 我 :是的,现在的开源框架不都喜欢说自己通过顺序读写大大的提升了性能吗,比如 hbase 、 kafka 面试官 :说的没错,那你认为 Linux 有对此做出优化吗?提示,你可以再执行一次 free 命令看一下 我 :奇怪缓存怎么占用了1.2G多 图片来源: https://www.linuxatemyram.com/ 面试官 : 你有没有想过 buff/cache 里面存的是什么,? 为什么 buff/cache 占了那么多内存,可用内存即 availlable 还有 1.1G ? 为什么你可以通过两条命令来清理 buff/cache 占用的内存,而想要释放 used 只能通过结束进程来实现? 品,你细品 思考了几分钟后 我 :这么随便就释放了 buff/cache 所占用的内存,说明它就不重要, 清除它不会对系统的运行造成影响 面试官 : 不完全对 我 :难道是?想起来《CSAPP》(深入理解计算机系统)里面说过一句话 存储器层次结构的本质是,每一层存储设备都是较低一层设备的缓存 翻译成人话,就是说 Linux会把内存当作是硬盘的高速缓存 相关资料: http://tldp.org/LDP/sag/html/buffer-cache.html 面试官 :现在知道那道送分题应该怎么回答了吧 我 :我.... Join算法 面试官 :再给你个机会,如果让你来实现Join算法你会怎么做? 我 :无索引的话,嵌套循环就完事了嗷。有索引的话,则可以利用索引来提升性能. 面试官 :说回 join_buffer 你认为 join_buffer 里面存储的是什么? 我 :在扫描过程中,数据库会选择一个表把他 要返回以及需要进行和其他表进行比较的数据 放进 join_buffer 面试官 :有索引的情况下是怎么处理的? 我 :这个就比较简单了,直接读取两个表的索引树进行比较就完事了嗷,我这边介绍一下无索引的处理方式 Nested Loop Join 嵌套循环,每次只读取表中的一行数据,也就是说如果outerTable有10万行数据, innerTable有100行数据,需要读取 10000000 次(假设这两个表的文件没有被操作系统给缓存到内存, 我们称之为冷数据表) 当然现在没啥数据库引擎使用这种算法(太慢了) Block nested loop Block 块,也就是说每次都会取一块数据到内存以减少I/O的开销 当没有索引可以使用的时候,MySQL InnoDB 就会使用这种算法 考虑以下两个表 t_a 和 t_b 当无法使用索引执行join操作的时候,InnoDB会自动使用 Block nested loop 算法 总结 上学时,数据库老师最喜欢考数据库范式,直到上班才学会一切以性能为准,能冗余就冗余,实在冗余不了的就 join 如果 join 真的影响到性能。试着调大你的 join_buffer_size , 或者换固态硬盘。 参考资料 《深入理解计算机系统》- 第6章 存储器层次结构 https://www.linuxatemyram.com/play.html 作者通过几个例子来说明硬盘缓存对程序执行性能的影响 https://www.linuxatemyram.com/ Free参数的解释 https://www.thegeekdiary.com/how-to-clear-the-buffer-pagecache-disk-cache-under-linux/ 文章开头送分题命令的解释 https://juejin.im/book/5bffcbc9f265da614b11b731/section/5c061a4de51d451df113c10d MySQL 是怎样运行的:从根儿上理解 MySQL https://mariadb.com/kb/en/block-based-join-algorithms/ 来自MariaDB官方文档解释了Block-Nested-Loop算法的实现 关注公众号Java技术栈回复"面试"获取我整理的2020最全面试题及答案。 推荐去我的博客阅读更多: 1. Java JVM、集合、多线程、新特性系列教程 2. Spring MVC、Spring Boot、Spring Cloud 系列教程 3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程 4. Java、后端、架构、阿里巴巴等大厂最新面试题 觉得不错,别忘了点赞+转发哦!
来源:OSCHINA
发布时间:2020-07-14 09:55:00
(点击图片进入关卡) 一个被困的肉食动物,仍然是一个危险的肉食动物 简介 你陷入了陷阱! 等到食人魔近身,然后攻击,否则你会伤害自己! 函数可以返回一个值,包括一个 boolean 值(true 或 false)。 用这个来决定是否一个食人魔是 inattackrange() ! def inAttackRange(enemy): distance = hero.distanceTo(enemy) if distance <= 3: # return True because the enemy is in range else: # return False because the enemy is out of range 将结果保存到一个变量中,稍后在代码中使用它: canAttack = inAttackRange(target) 默认代码 # 你掉进陷阱里了!别动!你会受伤的! # 这个函数检查敌人是否再攻击范围。 def inAttackRange(enemy): distance = hero.distanceTo(enemy) # 几乎所有的剑都有3的攻击范围。 if distance <= 3: return True else: return False # 只有在触手可及的范围内才能攻击食人魔。 while True: # 找到最近的敌人,并将其储存在一个变量中。 # 调用 inAttackRange(enemy),将 enemy 作为参数 # 把结果保存于 “canAttack” 变量中 # 如果结果存储在一个攻击中 True, 然后下手! pass 概览 你可以在一个函数中使用多个 return 语句,但是只有一个会被使用,因为 return 会导致函数停止执行,并且返回回函数被调用的地方。 def moreThanTen(n): # 如果'n'大于10,则函数返回true if n > 10: return True # 否则'else'内的'return'就会被调用,函数会返回false else: return False isSmall = moreThanTen(5) # isSmall == true 冰冻打击 解法 # 你掉进陷阱里了!别动!你会受伤的! # 这个函数检查敌人是否再攻击范围。 def inAttackRange(enemy): distance = hero.distanceTo(enemy) # 几乎所有的剑都有3的攻击范围。 if distance <= 3: return True else: return False # 只有在触手可及的范围内才能攻击食人魔。 while True: # 找到最近的敌人,并将其储存在一个变量中。 nearestEnemy = hero.findNearestEnemy() # 调用 inAttackRange(enemy),将 enemy 作为参数 # 把结果保存于 “canAttack” 变量中 canAttack = inAttackRange(nearestEnemy) # 如果结果存储在一个攻击中 True, 然后下手! if canAttack: hero.attack(nearestEnemy) 本攻略发于极客战记官方教学栏目,原文地址为: https://codecombat.163.com/news/jikezhanji-bingdongdaji 极客战记——学编程,用玩的
来源:OSCHINA
发布时间:2020-07-14 09:48:00
(点击图片进入关卡) 准备射击!距离: 300 千米!方位角:…… 管他呢! 简介 那个村庄太安静了。 看起来像是埋伏。 盲人巫师是你唯一的朋友,但他是一个非常强大的法师。 你会成为他的代言人。 注意食人魔,并说任何传入的距离。 巫师的力量是有限的,只有在看见食人魔 only when see an ogre。 使用预定义函数查找最近的敌人并返回距离(如果没有敌人,则返回 0)。 如果将函数结果存储在变量中,则可以在代码中使用函数结果。 enemy = hero.findNearestEnemy() 默认代码 # 你的任务是告诉他兽人的距离。 # 这个函数寻找最近的敌人,并返回距离。 # 如果没有敌人,则该函数返回0。 def nearestEnemyDistance(): enemy = hero.findNearestEnemy() result = 0 if enemy: result = hero.distanceTo(enemy) return result while True: # 调用nearestEnemyDistance()和 # 将结果保存在变量enemyDistance中。 enemyDistance = nearestEnemyDistance() # 如果enemyDistance大于0: # 说出enemyDistance变量的值。 概览 函数可以包括很多指令,这会让你头脑清晰。 同样,函数可以避免重复一大段代码。 函数可以返回 (return) 值,从中你可以得到有用的信息。 之前你用 hero.findNearestEnemy() 的时候就见过了吧。 要在函数中返回一个值,用 return 关键字。 在它的后面加上要返回的值(或者变量)。 def someFunction(): ... return 3 # 函数返回 3 你可以把函数返回值存到变量里,给后面的代码用: x = someFunction() # 现在 x 等于 3 hero.say(x) 盲距 解法 # 你的任务是告诉他兽人的距离。 # 这个函数寻找最近的敌人,并返回距离。 # 如果没有敌人,则该函数返回0。 def nearestEnemyDistance(): enemy = hero.findNearestEnemy() result = 0 if enemy: result = hero.distanceTo(enemy) return result while True: # 调用nearestEnemyDistance()和 # 将结果保存在变量enemyDistance中。 enemyDistance = nearestEnemyDistance() # 如果enemyDistance大于0: if enemyDistance > 0: # 说出enemyDistance变量的值。 hero.say(enemyDistance) 本攻略发于极客战记官方教学栏目,原文地址为: https://codecombat.163.com/news/jikezhanji-mangju 极客战记——学编程,用玩的
来源:OSCHINA
发布时间:2020-07-14 09:46:00
简介 今天我们讲讲JDK10中的JVM GC调优参数,JDK10中JVM的参数总共有1957个,其中正式的参数有658个。 其实JDK10跟JDK9相比没有太大的变化,一个我们可以感受到的变化就是引入了本地变量var。 为了方便大家的参考,特意将JDK10中的GC参数总结成了一张PDF,这个PDF在之前的JDK9的基础上进行了增减和修正,欢迎大家下载。 Java参数类型 其实Java参数类型可以分为三类。 第一类叫做标准的java参数。 这一类参数是被所有的JVM实现所支持的,是一些非常常用的JVM参数。 我们可以通过直接执行java命令来查看。 第二类叫做额外的java参数,这些参数是专门为Hotspot VM准备的,并不保证所有的JVM都实现了这些参数。并且这些参数随时有可能被修改。 额外的java参数是以 -X开头的。 我们可以通过java -X来查看。 第三类叫做高级参数。这些参数是开发者选项,主要作用就是JVM调优。这些参数和第二类参数一样,也是不保证被所有的JVM支持的,并且随时都可能变化。 第三类参数是以-XX开头的,我们可以通过java -XX:+PrintFlagsFinal来查看。 神奇的是,我们做JVM调优的参数往往就是这第三类参数,所以,下次如果再有面试官问你JVM调优,你可以直接怼回去:这些参数是不被官方推荐普通使用者使用的,并且随时都可能变化。没必要掌握!那么这个Offer肯定非你莫属。 Large Pages 其实JDK10跟JDK9相比没啥大的变化,这里重点讲解一个特性叫做Large Pages。 Large pages其实不是JDK10的新特性了,它的历史已经很久了。 在讲large Pages之前,我们先讲一下内存分页。 CPU是通过寻址来访问内存空间的。一般来说CPU的寻址能力是有限的。而实际的物理内存地址会远大于CPU的寻址范围。 为了解决这个问题,现代CPU引入了MMU(Memory Management Unit 内存管理单元)和虚拟地址空间的概念。 虚拟地址空间也叫做(Virtual address space),为了不同程序的互相隔离和保证程序中地址的确定性,现代计算机系统引入了虚拟地址空间的概念。简单点讲可以看做是跟实际物理地址的映射,通过使用分段或者分页的技术,将实际的物理地址映射到虚拟地址空间。 同时为了解决虚拟空间比物理内存空间大的问题,现代计算机技术一般都是用了分页技术。 分页技术就是将虚拟空间分为很多个page,只有在需要用到的时候才为该page分配到物理内存的映射,这样物理内存实际上可以看做虚拟空间地址的缓存。 虚拟地址空间和物理地址的映射是通过一个叫做映射存储表的地方来工作的。这个地方一般被叫做页表(page table),页表是存储在物理内存中的。 CPU读取物理内存速度肯定会慢于读取寄存器的速度。于是又引入了TLB的概念。 Translation-Lookaside缓冲区(TLB)是一个页面转换缓存,其中保存了最近使用的虚拟到物理地址转换。 TLB容量是有限的。如果发生TLB miss则需要CPU去访问内存中的页表,从而造成性能损耗。 通过调大内存分页大小,单个TLB条目存储更大的内存范围。这将减少对TLB的压力,并且对内存密集型应用程序可能具有更好的性能。 但是,大页面也可能会对系统性能产生负面影响。例如,当应用程序使用大量大页面内存时,可能会导致常规内存不足,并导致其他应用程序中的过多分页,并使整个系统变慢。同样,长时间运行的系统可能会产生过多的碎片,这可能导致无法保留足够大的页面内存。发生这种情况时,OS或JVM都会恢复为使用常规页面。 Oracle Solaris, Linux, and Windows Server 2003 都支持大页面。 具体各个系统的large page的配置,大家可以自行探索。 JIT调优 JIT我在之前的文章中介绍过很多次了,为了提升java程序的执行效率,JVM会将部分热点代码,使用JIT编译成为机器码。 那么JIT的调试参数也是非常重要的。这里具体讲解一些比较常用JIT调试指令: -XX:+BackgroundCompilation 使用后台编译,也就是说编译线程和前台线程使用是不同的线程。一般来说如果我们需要调试JIT日志的话,需要关闭此选项。 -XX:CICompilerCount=threads 设置编译线程的个数。 -XX:CompileCommand=command,method[,option] 自定义具体方法的编译方式。 比如: -XX:CompileCommand=exclude,java/lang/String.indexOf 意思是把String的indexOf exclude from compiled。 这里的command有下面几种: break - 为编译设置断点 compileonly - exclude所有的方法,除了指定的方法 dontinline - 指定的方法不inline exclude - 编译的时候排除指定的方法 inline - inline指定的方法 log - exclude所有的方法日志,除了指定的方法 option - 传递一个JIT编译的参数 print - 输出生成的汇编代码 quiet - 不打印编译命令 -XX:CompileOnly=methods 指定编译某些命令。 -XX:CompileThreshold=invocations 命令经过多少次解释执行,才会被编译。默认情况下在-server模式,这个值是10,000, 在-client模式,这个值是1,500。 如果分层编译开启之后,这个值会被忽略。 -XX:+DoEscapeAnalysis 开启逃逸分析。 -XX:+Inline 开启inline特性。 -XX:+LogCompilation 输出编译日志。 -XX:+PrintAssembly 输出汇编代码。 -XX:-TieredCompilation 取消分层编译。 上图: 总结 同样的,为JDK10特意准备了一个PDF,下载链接如下: JDK10GC-cheatsheet.pdf 本文链接: http://www.flydean.com/jdk10-gc-cheatsheet/ 最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现! 欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
来源:OSCHINA
发布时间:2020-07-14 09:15:00
方法引用就是通过类名或方法名引用已经存在的方法来简化lambda表达式。那么什么时候需要用方法引用呢?如果lamdba体中的内容已经有方法实现了,我们就可以使用方法引用。 一、方法引用的三种语法格式 1. 对象::实例方法名 lamdba写法: @Test void test1(){ Consumer con = x -> System.out.println(x); } 方法引用写法: @Test void test2(){ PrintStream out = System.out; Consumer con = out::println; } consumer接口: @FunctionalInterface public interface Consumer { void accept(T t); } 注意:被调用的方法的参数列表和返回值类型需要与函数式接口中抽象方法的参数列表和返回值类型要一致。 2. 类::静态方法名 lamdba写法: @Test void test3(){ Comparator com = (x, y) -> Integer.compare(x,y); } 方法引用写法: @Test void test4(){ Comparator com = Integer::compare; } Comparator接口: @FunctionalInterface public interface Comparator { int compare(T o1, T o2); } Integer类部分内容: public final class Integer extends Number implements Comparable { public static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); } } 注意:被调用的方法的参数列表和返回值类型需要与函数式接口中抽象方法的参数列表和返回值类型要一致。 3. 类::实例方法名 lamdba写法: @Test void test5(){ BiPredicate bp = (x,y) -> x.equals(y); } 方法引用写法: @Test void test6(){ BiPredicate bp = String::equals; } BiPredicate接口: @FunctionalInterface public interface BiPredicate { boolean test(T t, U u); } 注意:第一个参数是这个实例方法的调用者,第二个参数是这个实例方法的参数时,就可以使用这种语法。 二、构造器引用 类::new lamdba写法: @Test void test7(){ Supplier supplier = ()->new Person(); } 构造器引用写法: @Test void test8(){ Supplier supplier = Person::new; } Supplier接口: @FunctionalInterface public interface Supplier { T get(); } Person类: @Data public class Person implements Serializable { private static final long serialVersionUID = -7008474395345458049L; private String name; private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } } 注意:person类中有两个构造器,要调用哪个构造器是函数式接口决定的,也就是Supplier接口中的get()方法是无参的,那么就调用的是person中的无参构造器。 三、数组引用 Type::new lamdba写法: @Test void test9(){ Function fun = x -> new String[x]; } 数组引用写法: @Test void test10(){ Function fun = String[]::new; } Function接口部分内容: @FunctionalInterface public interface Function { R apply(T t); } 总结 方法应用及构造器引用其实可以理解为lamdba的另一种表现形式 方法引用被调用的方法的参数列表和返回值类型需要与函数式接口中抽象方法的参数列表和返回值类型要一致 方法引用中使用类::实例方法的条件是第一个参数是这个实例方法的调用者,第二个参数是这个实例方法的参数 构造器引用需要调用的构造器的参数列表要与函数式接口中抽象方法的参数列表一致
来源:OSCHINA
发布时间:2020-07-14 09:01:00
还是有点懵逼 ... 函数式编程的主要思想是流程先行, 而非数据先行, 先指定流程, 然后输入数据, 所以操作的数据总是在参数的最后, 并非传统的面向数据的编程方式, 把数据放到前面进行后续操作 https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch10.html#%E5%8D%8F%E8%B0%83%E4%BA%8E%E6%BF%80%E5%8A%B1 https://legacy.gitbook.com/book/llh911001/mostly-adequate-guide-chinese/details 圖解Functor,Applicative和Monad https://zhuanlan.zhihu.com/p/28840109 https://github.com/fantasyland/fantasy-land https://zhuanlan.zhihu.com/p/44046407 https://www.cnblogs.com/Abbey/p/11000520.html https://blog.csdn.net/pingyan158/article/details/52764736 https://zhuanlan.zhihu.com/p/88741757 https://www.jianshu.com/p/001ff0dd3c30 https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ 说的不错 在命名的时候,我们特别容易把自己限定在特定的数据上(本例中是 articles )。这种现象很常见,也是重复造轮子的一大原因。 + 纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。 + “作用”我们可以理解为一切除结果计算之外发生的事情。 副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。 副作用可能包含,但不限于: 更改文件系统 往数据库插入记录 发送一个 http 请求 可变数据 打印/log 获取用户输入 DOM 查询 访问系统状态 纯函数总能够根据输入来做缓存 curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。 当我们谈论 纯函数 的时候,我们说它们接受一个输入返回一个输出。curry 函数所做的正是这样:每传递一个参数调用函数,就返回一个新函数处理剩余的参数。这就是一个输入对应一个输出啊。 + 哪怕输出是另一个函数,它也是纯函数。当然 curry 函数也允许一次传递多个参数,但这只是出于减少 () 的方便。 + 这就是 组合 (compose,以下将称之为组合): 组合的结合律 var associative = compose(f, compose(g, h)) == compose(compose(f, g), h); 两个组合的函数, 接受的参数应该在输入和输出关系上一致, 即前者的出口类型对应后者的入口类型 var compose = function ( f,g ) { return function ( x ) { return f(g(x)); }; }; f 和 g 都是函数, x 是在它们之间通过“管道”传输的值。 利用组合率优化代码 var law = compose(map(f), map(g)) == map(compose(f, g));
来源:OSCHINA
发布时间:2020-07-14 08:45:05
笨办法学 Python是Zed Shaw 编写的一本Python入门书籍。适合对计算机了解不多,没有学过编程,但对编程感兴趣的朋友学习使用。这本书以习题的方式引导读者一步一步学习编 程,从简单的打印一直讲到完整项目的实现。也许读完这本书并不意味着你已经 学会了编程,但至少你会对编程语言以及编程这个行业有一个初步的了解。 《笨办法学python第四版》 该版本相比于第三版优化了页面,实例代码使用都是彩色,使得学习者一看就能区分内容的性质。 本书区别于其它入门书籍的特点如下: 注重实践。本书提供了足够的练习代码,如果你完成了所有的练习(包括加分习题),那你已经写了上万行的代码。要知道很多职业程序员一年也就写几万行代码而已。 注重能力培养。除了原序言提到的“读和写”、 “注重细节”、以及“发现不同”这样的基本能力以外,本书还培养了读者自己专研问题和寻求答案的能力。 注重好习惯的养成。本书详细地讲解了怎样写出好的代码、好的注释、好的项目。这会让你在后续的学习中少走很多弯路。 百度网盘地址: 链接: https://pan.baidu.com/s/1KQKo_DC-XVqUh7X-7jyKxw 提取码:1234
来源:OSCHINA
发布时间:2020-08-05 20:41:00
接触到《优柔有情人》其实只是一个偶然的契机,目的是为了听中村悠一的声音而已,事实上,第一次听完《穷途》和《俎上》之后也只是觉得好笑而已,甚至有点羞赧于自己的恶趣味。Drama没有听完,然后就去看了漫画,第一次看这部漫画是极为草率的,但是《俎上》最后一段话还是让我的心揪紧了好久好久: “我的隘口是你,你的隘口是我。我們之間的鴻溝,永遠沒有填平的一天。所以才要不停地架橋,萬一橋被洪流衝毀,那就再架一座橋。隨時抱著易碎品的人生嗎?——那樣沒有關系,我已經不在乎了。下個禮拜或是20年後,總有一天你會離去,或許更早,或許更晚。等到你累到無法去愛,就是我真正離去的時候。暮然回首人生,或許會发現什麽也沒有剩下。那樣也沒關系,我會目送著你離開,親眼見證這段戀情之死。在此之前,讓我為這條通往死亡的道路,再多裝飾一些美麗的花朵吧。” 后来又重新去听drama,两位声优的演技都非常精湛,甚至到最后,Yusa几乎已经声嘶力竭了。不得不说,我确实会和Yuyi产生一样的疑惑,为什么恭一会回到今之濑的身边,但是对于两人之间的心态,仔细看漫画之后还是能够渐渐体会出来。 这部作品的受众是三十岁的主妇群,而我不过是一个二十出头的女生,我一直在想,这部作品对于我来说意味着什么?确实,它是把我们日常生活中所有的情绪都放大了,无论是恭一的“随风倒”式的顺从,还是今之濑纯情无比却又近乎疯狂的执着的情爱态度,对于我来说其实都是遥远的,但是我似乎确实能够在这部作品当中感受到一种说不太清的真实感,恭一和今之濑从一开始其实都是彼此相似的,无论是恭一因为父亲的童年阴影导致的“只和她爱我胜过我爱她的女人交往”“不断寻找最爱自己的人“,”拥有[确定自己是被爱的]这份担保“的人生态度,承认自己对于今之濑的依恋,依恋着这样365天24小时都会爱着自己的他,依恋着这种安稳舒适的日子,还是今之濑的”自我沉浸和自我伤怀“,自愿成为恭一的猎物,逐渐沦陷,一次次被伤害,但是又一次次地忍耐和坚持,似乎都是一种极度的自我中心主义,另一方面,恭一“拼了命去玩这样一场恋人游戏”: “它就好像一场实验。好比研究一只考拉不吃桉树叶能否存活。需要花时间才能得到结论,但是考拉很可能就真的死掉了。” 面对女人那样摇摆不定的恭一,却在面对今之濑的一次次的询问和诱导的时候,这样坚定地一次次地拒绝说出爱的承诺,哪怕最后的最后,恭一也还在说,“爱的言语没有那么容易” 并且一直在追求和今之濑对等程度的感情,尝试去理解同性之爱,理解今之濑的世界。只不过,他不知道自己面对这样任性,这样爱自己,这样不安,这样容易嫉妒的今之濑的责任和依恋感算不算是爱,事实上,或许恭一也是第一次面临着这样的来自自我的质问,而这种质问,却又是非这样的全新体验不可,非这样疯狂炽烈,又无比脆弱的爱不可。 而今之濑呢?他极度不安,极度担心自己成为恭一追求自己“真命天女”的阻碍,在恭一的试探之下也只是搬来了寥寥的行李,抱着一种注定要分开,甚至最后也无法从内心里真正接受恭一对他的接纳, ”总觉得自己污染了不该触碰的身体……就像现在,我都还在担心,要怎么退回我们原本的距离“,今之濑长久的默默注视,面对着自认为“自己是一个无趣的男人”的恭一,能够说出他是“最棒的男人”那样的话,他们之间似乎又充满着极度温柔缠绵的彼此注视,彼此理解和彼此追求,两人之间的关系似乎是这样奇怪而又诡秘的极度自我中心主义和极度利他主义的结合,追逐者和被追逐者之间的倒错和相互追逐,到最后几乎分不清究竟是谁在前进,谁在后退,抑或是你进我退,仔细想想,很可能是爱得太深了的缘故。 相对于恭一在面对西村,夏生时候的轻率,面对小环的时候,恭一虽然承认被她的作为女子的美好的酮体所吸引,但是却并没有想要进一步发展的愿望,但是小环的出现却让今之濑的不安全感达到了极限,他在这段关系当中所有的不自信和不安都爆发出来了,很喜欢水城写的这段话: “你应该听过,有句话叫急流勇退。你真的对我不错,我的愿望你都一一实现了,这样就够了。能走的路我都试过了,不过已经到尽头了。我和你再也无路可走,前方只剩死路一条……讲这种事很扫兴吧。我就要满三十了。很想找个人过安定日子。但是你不行,因为你不是对的人,对你来说,我也不是对的人。我们分手吧。” 其实这里,或许不是恭一无路可走,而是今之濑已经到达极限了吧,他陷入了自我沉浸当中,以为自己真的无路可走了,事实上,他真的有好好注视过恭一吗?但是我们似乎无法责怪他,不仅是因为我们每个人实际上都喜欢自我沉浸,自说自话,而且是因为今之濑对于恭一迟迟不肯给出他想要的爱的言语和承诺的不安也确实达到极点了。 而恭一呢?事实上,恭一的无奈和自责也并不比今之濑少吧,或许这部作品是从恭一的视角来写的,又或许是水城对于恭一的刻画更丰满一些,恭一这一段独白也让人心头一紧: “我知道,为了维持这段关系,你付出的远比我多,这些我都知道。我占尽所有又是,你却一直在吃苦受罪。我都做了些什么呢?是否一直在等今之濑主动发难,还是我真的以为自己可以给今之濑幸福?再怎么高估自己,也该有个限度。” 人们都说,面对真正的爱的时候,人会变得脆弱,变得不自信,或许恭一这个时候也是如此吧,我相信恭一如今之濑所说的,他是一个温柔无比的人,他担心自己给不了今之濑幸福,自己理解不了同性之爱,如果说他曾经幻想自己可以成功完成这次实验,自己可以玩好这场恋人游戏,但是面对着如此脆弱和不安的今之濑,他还是退缩了,心疼了,他内心深处或许真的是自卑的,真的觉得自己是不值得被爱的,真的是给不了别人幸福的,于是他没有挽留,他最终还是咽下了那句“我……对你……”,我对你什么呢?恭一自己也不知道。 “不管之前再怎么顺利,该结束的时候,一点小事都能成为导火索。早知会有这么一天,怪的是,分手竟让我觉得释然。我还真是冷漠啊……我果然没爱过今之濑。一切都只是我的自我满足,幸好没傻傻说出来。” 今之濑无数次幻想,自己要是是女人有多好,能够全心全意只考虑恭一一个人,能够相夫教子,他始终执着于恭一对于“女人”的迷恋,事实上,虽然恭一也幻想过今之濑是一个女人,但是恭一却从来没有现实地把今之濑当成“女人”去爱,他始终希望自己能够真正“爱上”作为男人的今之濑,他虽然会摇摆,但是却真正地在努力尝试,无论为爱人过生日,还是准备红酒,把爱人的生日当作手机密码等等。读书笔记(https://www.yuananren.com)他觉得自己的恋爱游戏失败了,惊讶于自己说出分手之后的释然,震惊于自己的冷漠,似乎又可以感觉到恭一的言不由衷,遗憾与释然交织,委屈与惭愧交织的复杂情愫。 很喜欢两人从海边返程之后的这一段描写: “和来时相反,回程我们都没有说话。一回到家,今之濑立刻整理自己的行李,简短打声招呼就离开了。尽管我们都半同居了,他的行李却少得可怜。此时我才惊觉,原来他早就料到会有这天来临,才没有在我家落地生根。这么说来,今之濑一直没有退掉原本租的地方。我以浪费房租为借口拐着弯暗示他搬来一起住。今之濑也只是笑笑,如今我才明白,原来他一点都不看好我们的关系。我和今之濑的关系,就这样莫名地落幕了。 究竟谁真的处于上风呢?谁对这段关系有着更高的期待呢?或许恭一和我们一样感到困惑吧。在今之濑走后,恭一一直在想,今之濑对于自己来说意味着什么?这样一段从未有过的全新经历对于自己来说意味着什么?水城老师这一段对于同性之爱写得真的有很动人的现实张力: “硬要给幸福挑缺点的话,再幸福也会变成不行,例如不能把今之濑的事告诉别人,轻蔑、美化或好奇,都不是我所乐见的反应。当然也不希望博得同情。成为别人妄想的主角,更非我所愿。我宁愿不让任何人知道。“ “不过,就算是这样好了。今之濑可曾在我的人生中造成任何不幸?今之濑,你根本就不明白。没有人知道,我对你到底有什么感觉。在那段身陷泥沼般的日子中,我每天过的有多幸福。” 最后的恭一还是后悔崩溃了,面对小环说出了真心话: “他明明对我付出许多,是个值得信赖的对象,可是,我只会逼他说出我想听的话,享尽站在优势的快感……我从来没把他当成自己人。我应该告诉他,我们还有救,还有未来可走!虽然心里懊恼,我却没说出口!竟然就这样放弃了!我太没骨气了!我是个卑鄙小人!这种人活该一辈子孤单!” 崩溃脆弱的恭一依然和以往一样选择向女人寻求安慰,甚至在和小环发生关系之后,终于承认自己和今之濑是“真心相爱,我是真的爱过他。” 当恭一在心底祈求,“神啊,如果您真的存在,请一定要让那个男人得到幸福他是我的男人,此生唯一有过的男人,求您了。”他大概才真正明白,爱上一个人是什么滋味吧。 恭一和小环在一起之后,他对于自己和今之濑的交往进行了重新定位,水城老师在这里的用笔轻描淡写多了,但还是可以隐隐感觉到恭一努力压抑着的感情: “过去,我曾经假扮过同性恋。因为那个男人,成天嚷着他爱我,难得有人无条件爱着自己,算了就接受吧。我们的孽缘就这样一天拖过一天。我被对方各种手段弄得感动不已,不但对他产生感情,也真的爱上他了。我爱他……我自认是如此,对货真价实的男同志而言,应该会耻笑着说,那种儿戏才不是恋爱呢。就算没有人理解也没关系,对我而言,那段日子是特别的。既不美丽也不丑陋,就只是一段重要的过去。” 恭一觉得,是今之濑教会了他爱,今之濑以及有关他的那段过往,变成了恭一的一部分,于是他尝试着用今之濑那里学来的爱去爱小环: “我在最糟的状态下,和小环开始交往,起初落后没关系,后面再迎头赶上就可以了,关于我的种种劣行,她都无条件地接纳了。我爱上的,也就是她的这份纯真,仔细想想,当她知道父亲另有家庭时,依旧保有原本的善良个性。这就表示她的心胸宽大,以后就算我做了什么傻事,她多半也能原谅我吧。这么可爱的女人,我应该要怎样去爱她,这在以前不是就学过了吗。要是连爱人也不会,那个人在我身上花的功夫就算白费了,那个人已经成了我的一部分,就像地层一样层层累积,然后才有了现在的我。” 小环问恭一,今之濑是一个什么样的人,恭一笑着描述道:“怎样啊,缠人精,爱嫉妒,个性阴沉。上一秒才说不在乎,下一秒就因为小事闹得歇斯底里,现在回想起来,应该是死缠烂打型吧。” 但是总算,面对小环的询问,他终于可以大大方方地承认自己喜欢过他了:“嗯,包括闹别扭地地方,我都觉得很可爱。他乐于为我忙得团团转,因为有他,我才什么都不用烦恼。他……真的很可爱。”然后可以看出日本人对于“可爱”这个词的经典理解了,小环说:“我现在知道你有多喜欢她了。就算不喜欢一个人,还是能称赞她漂亮或温柔,但是一定要有爱,才会认为她可爱。” 恭一承认说:“……或许吧。我最喜欢他的一点,就是他喜欢我喜欢到不得了。”虽然恭一的描述依然这样自我中心主义,但是好像又让人有种那么真实和坦率的现实感。 其实他哪里是真的那样利己呢?最终没能说出口的挽留恰恰也是他自己对于给人幸福,对于自己理解的“对等”,因为今之濑的爱过于沉重而无法“对等”的无力感罢了: “他对我说,你不是对的人,我们就闪电分手了。……很无聊吧。我本来可以安抚他的,以前交往的对象生气时,我只要稍微顺着他们的意去做,对方通常就会选择原谅……然后就是不停反复。可是,他看起来似乎很痛苦……所以我不能。我努力过了,也自认很珍惜他,但是只有这样并不够,到头来还是无法满足他。所以,我决定以后再也不要用挽回或安抚来粉饰太平。其实我早就该这么做了。若说有什么遗憾……大概就是这点吧。其实我真的过得很幸福……”可是恭一明白,自己不能为了自己的自私,幸福感和安适而让今之濑一直痛苦下去。 小环说:“……这不是恭一的错。怪只怪那个人太爱你了。我好像可以明白她的心情。泰国深爱一个人的话,自己也会变得不再是自己。” 恭一疑惑着,“命中注定的对象真的存在吗?如果是的话,只能有一个人吗?这么多人的人生在世上交错,不会只有一个人让自己得到幸福吧,我认为,人的命运并没有那么错若,一定还有许多其他可能。——你一定也是如此吧。”我想,恭一一定是最希望今之濑获得幸福的那个人吧。 恭一终于在听到小环联系今之濑的时候动摇了,“原来你就在这么近的地方”,听到他的消息,知道他过得不错,明明在意到不行今之濑是不是有了新的对象却只能自我压抑,但是恭一拉开窗帘,看到窗外寒冷的月光的时候,他的情绪洪流最终还是倾泻而出了: “今之濑,今晚特别寒冷,真想抚摸你的头发。” 但是,恭一依然还是不自信的啊: “……愚蠢!他不需要这种半吊子的温柔!真正的同性恋,能给他的爱比我深得多了,那是我所无法想象……一直渗透到心灵最深处的爱。”不得不说,恭一也是个彻头彻尾不可救药的浪漫主义者啊,他在说同性恋,实际上是在说今之濑啊…… 因为小环的缘故,两人却又戏剧般地重逢了,今之濑依旧卑微地描述着自己如何痛不欲生,因为太过痛苦而忘记“恭一”的名字,不希望彼此心中的位置就像这次遗忘一样完全被抹去,于是两人又进行了新一波让人肝痛的争吵,恭一心里其实也在怨恨着今之濑吧,他既然那样爱自己,为什么又会崩溃绝望到认为两人的关系已经穷途末路?难道不是因为今之濑只是迷恋于在自己的恋爱故事当中扮演着一个“悲剧英雄”而对恭一作为异性恋变为同性恋的所有挣扎视而不见?不得不说,正如今之濑抓住了恭一的死穴,恭一又何尝不是揭开了今之濑的弱点呢?真是奇妙的爱啊……明明两人都是这样的行动上的“纯情利己主义”者抑或是“现实利己主义者”,但是两人却又是那样为彼此着想,这样利他,这样彼此了解又彼此伤害,但是正如今之濑执着地拼命以为恭一只爱女人,不能接受甚至于不敢相信真正成为同性恋的恭一,甚至自己爱上的也不过是那个“不爱男人,只爱女人“的恭一,用三岛在《禁色》当中的一段话来说, ”你是一座墙壁,对于外敌来说就是万里长城。你是绝对不会爱上我的情人。正因为这样我才敬慕你,现在还是这样敬慕你。“ 而恭一却又执着地希望自己能够给今之濑真正的”同性之爱“,于是两者的关系因此发生了奇妙而又悲剧性的错位。” 恭一描述着水城老师留白的那段分手后的时间: “……能够哭出来的人还算幸福呢。真正伤心的时候,我会封闭心灵,什么都感觉不到。就只能眼睁睁看着一切变成过去,拿个盒子把它盖起来,……算了,反正那些都不重要了。” 今之濑在泪流不已之际惊讶地看到恭一恋恋不愿丢弃的烟灰缸,大概像是真的抓住最后一根稻草一样,用尽力气作出垂死挣扎了,然后恭一最终还是压抑不住而动摇了,他的心声听起来依然是那样赤裸、狡猾直白甚至肮脏: “再没有比今之濑更糟糕的对象了……话说回来,抱怨归抱怨,俗话说,小别胜新婚,像他这种不管分手几次都会回头要求复合的笨蛋,还是教我怜爱得不得了。我就是喜欢冷眼看着他,巴着我大腿不放的模样,真的不想放开他。如果说人不为己,天诛地灭,就让我一辈子自私下去吧。我的眼中只有欲望,现在的我,眼中只有欲望。“ 而在恭一对今之濑说出“你真可爱“并且承认”可爱是“可以去爱”之后,今之濑终于慢慢不再那样卑微了并向恭一提出了与小环分手的要求,虽然今之濑依然不安,但是他的“羁绊”的答案却给了恭一最后的安定,你要的幸福,我很可能给不了你,但是如果你只想要一个羁绊,那么我会以此为底线,努力让你幸福。 然后回到了开头最打动我的那一段恭一的独白: “我的隘口是你,你的隘口是我。我們之間的鴻溝,永遠沒有填平的一天。所以才要不停地架橋,萬一橋被洪流衝毀,那就再架一座橋。隨時抱著易碎品的人生嗎?——那樣沒有關系,我已經不在乎了。下個禮拜或是20年後,總有一天你會離去,或許更早,或許更晚。等到你累到無法去愛,就是我真正離去的時候。暮然回首人生,或許會发現什麽也沒有剩下。那樣也沒關系,我會目送著你離開,親眼見證這段戀情之死。在此之前,讓我為這條通往死亡的道路,再多裝飾一些美麗的花朵吧。” 听drama和初看漫画的时候,总会非常心疼处在似乎处在绝对下风,低到尘埃当中去的今之濑,但是细看完漫画之后,才发现哪里有什么下风呢?爱必然要是某种程度上势均力敌的啊,即便是恭一和今之濑这样奇异又充满张力的又利己又利他的纯爱,恭一最后内心依然是不安的吧……但是对于这样怀抱着易碎品的人生,他也已经无畏了,这样看来,好像恭一却又完完全全处在下风。不管这两人的爱算不算扭曲,也不知道今之濑是否能够真正接受真正爱上自己的恭一,但是总觉得这句话是没错的:爱是穷途末路下的,英雄主义的一跃。
来源:OSCHINA
发布时间:2020-08-05 20:18:00
题目:输入一个正数n,输出所有和为n 连续正数序列。 例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以输出3 个连续序列1-5、4-6 和7-8。 fn main() { sum_is_n_continuous(15isize); println!("和为n 连续正数序列: {:?}", 1145); } //================================== 和为n的连续正数序列 // 和为n的连续正数序列 入口方法 // 构成队列然后循环开始查找 // n: isize 数值n fn sum_is_n_continuous(n: isize){ // 先将数值拆解成连续队列 let mut data_list: Vec = Vec::new(); for i in 1..n { data_list.push(i); } // 从0开始循环查找 for j in 0..(n - 1) { let (s, e) = sum_with_start(j, n, &data_list); if s >= 0 && e > 0 { // 这里编译器要警告 好像是是没用的比较 println!("找到 连续和为{:?} 的队列 {:?}", n, &data_list[s..e + 1]); } } } // 和为n的连续正数序列 根据给定队列 计算起点开始 和为sum的,起点和终点坐标 // s: isize 起点坐标 // sum: isezi 和值 // l: Vec 连续队列 // (usize, usize) 连续和是n的起始终结点角标 fn sum_with_start(s: isize, sum: isize, l: &Vec) -> (usize, usize){ // 队列长度 let _len: usize = l.len() as usize; // 当前计算和值 let mut _sum: isize = 0; // 当前终点角标 索引值 let mut _t_idx: usize = s as usize; while _t_idx < _len {// 只要索引值小于长度就继续循环 let _n = l[_t_idx]; // 取得当前角标下队列值值 _sum += _n;// 计算和 if _sum < sum { // 检查和,小于就是要继续计算 _t_idx += 1;//加角标 continue;//继续 } else if _sum == sum {//和等于,找到了,传送起点终点角标 return (s as usize, _t_idx); } else if _sum > sum {//和大于,过了设置为00,离开 return (0usize, 0usize); } } (0usize, 0usize)//保证每个地方都有返回。返回00 }
来源:OSCHINA
发布时间:2020-08-05 18:51:00
在Java语言中,处理空指针往往是一件很头疼的事情,一不小心,说不定就搞出个线上Bug,让你的绩效考核拿到3.25。 最近新出的Java14,相信大家都有所耳闻,那么今天就来看看,面对NullPointerException,Java14有哪些更好的处理方式呢? 1.传统的 NullPointerException 我们编码过程中呢,经常会使用链式调用的方式来写代码,这样写起来很方便,也很清晰,但是,一旦出现NullPointerException,那就头大了,因为你很难知道异常是在什么时候开始发生的。 举个简单的例子,就比如下面的代码,要找到公司某个员工的户籍所在地,我们这样来调用 String city = employee.getDetailInfos().getRegistryAddress().getCity(); 在链式调用的过程中,如果employee, getDetailInfos(),或者 getRegistryAddress() 为空,JVM就会抛出 NullPointerException 那么导致异常的根本原因是什么?如果不使用调试器,很难确定哪个变量为空。而且,JVM也只会打印导致异常的方法、文件名和行号,仅此而已。那么下面,我将带大家了解Java 14如何通过 JEP 358 解决这个问题。 2.增强型 NullPointerException SAP在2006年为其商业JVM实现了增强型的 NullPointerException。2019年2月,它被提议作为OpenJDK社区的一个增强,之后很快,它成为了一个JEP。所以,该功能在2019年10月完成并在JDK 14版本推出。 本质上,JEP 358 旨在通过描述某个变量是 “null” 来提高 JVM 生成的 “NullPointerException” 的可读性。JEP 358通过在方法、文件名和行号旁边描述为 null 的变量,带来了一个详细的 NullPointerException 消息。它通过分析程序的字节码指令来工作。因此,它能够精确地确定哪个变量或表达式是null。最重要的是,JDK 14中默认关闭详细的异常消息。要启用它,我们需要使用命令行选项: -XX:+ShowCodeDetailsInExceptionMessages 2.1 详细的异常信息 考虑在激活 ShowCodeDetailsInExceptionMessages 标志的情况下再次运行代码: Exception in thread "main" java.lang.NullPointerException: Cannot invoke "RegistryAddress.getCity()" because the return value of "com.developlee.java14.helpfulnullpointerexceptions.HelpfulNullPointerException$DetailInfos.getRegistryAddress()" is null at com.developlee.java14.helpfulnullpointerexceptions.HelpfulNullPointerException.main(HelpfulNullPointerException.java:10) 这一次,从附加信息中,我们知道员工的个人详细信息丢失的注册地址导致了我们的异常。从这个增强中获得的信息可以节省我们调试所用的时间。 JVM由两部分组成详细的异常消息。第一部分表示失败的操作,这是引用为 null 的结果,而第二部分标识了 null 引用的原因: Cannot invoke "String.toLowerCase()" because the return value of "getEmailAddress()" is null 为了生成异常消息,JEP 358 重构了将空引用推送到操作数堆栈上的部分源代码。 3. 技术方面 现在我们已经很好地理解了如何使用增强的NullPointerExceptions标识 null 引用,让我们来看看它的一些技术方面。 首先,只有当JVM本身抛出一个 NullPointerException 时,才会进行详细的消息计算,如果我们在Java代码中显式抛出异常,则不会执行计算。原因是因为:在这些情况下,很可能已经在异常构造函数中传递了一条有意义的消息。 其次,**JEP 358 ** 懒汉式地计算消息,这意味着只有当我们打印异常消息时才调用增强的NullPointerException,而不是当异常发生时就调用。因此,对于通常的JVM流程不应该有任何性能影响,在那里我们可以捕获并重新抛出异常,因为咱并不会只想打印异常消息。 最后,详细的异常消息可能包含源代码中的局部变量名。因此,我们可以认为这是一个潜在的安全风险。但是,只有在运行使用激活的 -g 标记编译的代码时,才会发生这种情况,该标记会生成调试信息并将其添加到类文件中。请考虑一个简单的示例,我们已编译该示例以包含以下附加调试信息: Employee employee = null; employee.getName(); 当执行以上代码时,异常信息中会打印本地变量名称: "com.developlee.java14.helpfulnullpointerexceptions.HelpfulNullPointerException$Employee.getName()" because "employee" is null 相反,在没有额外调试信息的情况下,JVM 只提供它在详细消息中所知道的变量: Cannot invoke "com.developlee.java14.helpfulnullpointerexceptions.HelpfulNullPointerException$Employee.getName()" because "" is null JVM 打印编译器分配的变量索引,而不是本地变量名(employee)。 关于NullPointerException的处理到这里就结束了,通过Java14增强的NullPointerException,我们可以很快速的定位代码问题的原因所在,更快的调试代码,节约时间,提高效率。 已经安装了Java14的朋友可以试试看哦~ 来源:锅外的大佬 欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长
来源:OSCHINA
发布时间:2020-08-05 18:33:00
印象里面Length会统计字符串长度会比较慢!但是没想到是Length比较块!丢脸了! #region --空字符串判断最快测试-- /// /// 空字符串判断最快测试 /// public class FastCheckEmptyString { /// /// 空字符串判断最快测试 /// public static void Run() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 20000; i++) { sb.Append("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); } string str = sb.ToString(); System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch(); st.Start(); for (int i = 0; i < 100000000; i++) { if (str == "") { } } st.Stop(); Console.WriteLine("str == '' : " + st.ElapsedMilliseconds); st.Restart(); //st.Start(); for (int i = 0; i < 100000000; i++) { if (str == String.Empty) { } } st.Stop(); Console.WriteLine("str == String.Empty : " + st.ElapsedMilliseconds); //st = new System.Diagnostics.Stopwatch(); st.Restart(); //st.Start(); for (int i = 0; i < 100000000; i++) { if (str.Length == 0) { } } st.Stop(); Console.WriteLine("str.Length == 0 : " + st.ElapsedMilliseconds); st.Restart(); //st.Start(); for (int i = 0; i < 100000000; i++) { if (!string.IsNullOrEmpty(str)) { } } st.Stop(); Console.WriteLine("!string.IsNullOrEmpty(str) : " + st.ElapsedMilliseconds); //end } } #endregion 测试结果采样: str == '' : 495 str == String.Empty : 413 str.Length == 0 : 343 !string.IsNullOrEmpty(str) : 317 IsNullOrEmpty() 王者!,其次是Length,其他两种都稍差! 话说回来!还是优化 结构 和 其他缓慢点,优化算法才是王道! 优化这个 性价比不高!
来源:OSCHINA
发布时间:2020-08-05 17:30:00
常用的原生函数有: String() Number() Boolean() Array() Object() Function() RegExp() Date() Error() Symbol() 原生函数可以被当作构造函数来使用 var a = new String("abc"); console.log(typeof a); console.log(a instanceof String); console.log(Object.prototype.toString.call(a)); 内部属性[[Class]] 所有typeof返回值为“object”的对象(如数组)都包含一个内部属性[[Class]](可以把它看作一个内部的分类,而非传统的面向对象意义上的类)。 这个属性无法直接访问,一般通过Object.prototype.toString()来查看。 console.log(Object.prototype.toString.call([1,2,3])); console.log(Object.prototype.toString.call(/regex-literal/i)); 封装对象包装 封装对象(object wrapper)扮演着十分重要的角色。由于基本类型值没有.length和.toString()这样的属性和方法,需要通过封装对象才能访问,此时JavaScript会自动为基本类型值包装一个封装对象。 封装对象释疑 var a = "abc"; var b = new String(a); var c = Object(a); console.log(typeof a); console.log(typeof b); console.log(typeof c); console.log(b instanceof String); console.log(c instanceof String); console.log(Object.prototype.toString.call(b)); console.log(Object.prototype.toString.call(c)); 拆封 valueof()返回封装对象中的基本类型值 var a = new String("abc"); var b = new Number(42); var c = new Boolean(true); console.log(a.valueOf()); console.log(b.valueOf()); console.log(c.valueOf()); 在需要用到封装对象中的基本类型值的地方会发生隐式拆封。 var a = new String("abc"); var b = a+""; console.log(typeof a); console.log(typeof b); 原生函数作为构造函数 Array()构造函数只带一个参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。 Object()、Function()和RegExp() var c = new Object(); c.foo = "bar"; c; // { foo: "bar" } var d = { foo: "bar" }; d; // { foo: "bar" } var e = new Function( "a", "return a * 2;" ); var f = function(a) { return a * 2; } function g(a) { return a * 2; } var h = new RegExp( "^a*b+", "g" ); var i = /^a*b+/g; new Object()来创建对象,因为这样就无法像常量形式那样一次设定多个属性,而必须逐一设定。 构造函数Function只在极少数情况下有用,比如动态定义函数参数和函数体的时候。不要把 Function(..) 当作 eval(..) 的替代品,你基本上不会通过这种方式来定义函数。 var name = "Kyle"; var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" ); var matches = someText.match( namePattern ); 强烈建议使用常量形式(如 /^a*b+/g )来定义正则表达式,这样不仅语法简单,执行效率也更高,因为 JavaScript 引擎在代码执行前会对它们进行预编译和缓存。与前面的构造函数不同, RegExp(..) 有时还是很有用的,比如动态定义正则表达式时。 Date()和Error() 创建日期对象必须使用 new Date() 。 Date(..) 可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。 Date(..) 主要用来获得当前的 Unix 时间戳(从 1970 年 1 月 1 日开始计算,以秒为单位).该值可以通过日期对象中的 getTime() 来获得或静态函数 Date.now() if (!Date.now) { Date.now = function(){ return (new Date()).getTime(); }; } 构造函数 Error(..) (与前面的 Array() 类似)带不带 new 关键字都可。创建错误对象(error object)主要是为了获得当前运行栈的上下文(大部分 JavaScript 引擎通过只读属性 .stack 来访问)。栈上下文信息包括函数调用栈信息和产生错误的代码行号,以便于调试(debug)。 错误对象通常与 throw 一起使用: function foo(x) { if (!x) { throw new Error( "x wasn’t provided" ); } // .. } 通常错误对象至少包含一个 message 属性,有时也不乏其他属性(必须作为只读属性访问),如 type 。 Symbol() 一个基本数据类型 ——符号(Symbol)。符号是具有唯一性的特殊值(并非绝对),用它来命名对象属性不容易导致重名。 符号可以用作属性名,但无论是在代码还是开发控制台中都无法查看和访问它的值,只会显示为诸如 Symbol(Symbol.create) 这样的值。 ES6 中有一些预定义符号,以 Symbol 的静态属性形式出现,如 Symbol.create 、 Symbol.iterator 等,可以这样来使用: obj[Symbol.iterator] = function(){ /*..*/ }; 可以使用 Symbol(..) 原生构造函数来自定义符号。但它比较特殊,不能带 new 关键字,否则会出错: var mysym = Symbol( "my own symbol" ); mysym; // Symbol(my own symbol) mysym.toString(); // "Symbol(my own symbol)" typeof mysym; // "symbol" var a = { }; a[mysym] = "foobar"; Object.getOwnPropertySymbols( a ); // [ Symbol(my own symbol) ] 原生原型 原生构造函数有自己的 .prototype 对象,如 Array.prototype 、 String.prototype 等。 这些对象包含其对应子类型所特有的行为特征。 例如,将字符串值封装为字符串对象之后,就能访问 String.prototype 中定义的方法。 • String#indexOf(..) 在字符串中找到指定子字符串的位置。 • String#charAt(..) 获得字符串指定位置上的字符。 • String#substr(..) 、 String#substring(..) 和 String#slice(..) 获得字符串的指定部分。 • String#toUpperCase() 和 String#toLowerCase() 将字符串转换为大写或小写。 • String#trim() 去掉字符串前后的空格,返回新的字符串。 以上方法并不改变原字符串的值,而是返回一个新字符串。 var a = " abc "; a.indexOf( "c" ); // 3 a.toUpperCase(); // " ABC " a.trim(); // "abc" 其他构造函数的原型包含它们各自类型所特有的行为特征,比如 Number#tofixed(..) (将数字转换为指定长度的整数字符串)和 Array#concat(..) (合并数组)。所有的函数都可以调用 Function.prototype 中的 apply(..) 、 call(..) 和 bind(..) 。 将原型作为默认值 Function.prototype 是一个空函数, RegExp.prototype 是一个“空”的正则表达式(无任何匹配),而 Array.prototype 是一个空数组。对未赋值的变量来说,它们是很好的默认值。 function isThisCool(vals,fn,rx){ vals= vals || Array.prototype; fn = fn || Function.prototype; rx = rx || RegExp.prototype; return rx.test( vals.map(fn).join("") ); } console.log(isThisCool()); 这种方法的一个好处是 .prototypes 已被创建并且仅创建一次。
来源:OSCHINA
发布时间:2020-08-06 22:18:00
1. 前言 今天继续搭建我们的 kono Spring Boot 脚手架,上一文集成了一些基础的功能,比如统一返回体、统一异常处理、快速类型转换、参数校验等常用必备功能,并编写了一些单元测试进行验证,今天把国内最流行的 ORM 框架 Mybatis 也集成进去。使用的 Spring Boot 版本为 2.3.2.RELEASE 。 Gitee: https://gitee.com/felord/kono 1.0.0.MYBATIS 分支 GitHub: https://github.com/NotFound403/kono 1.0.0.MYBATIS 分支 2. 集成Mybatis的步骤 集成 Mybatis 的步骤并不是特别复杂,我将它们分为三个步骤,接下来进入正题。 3. 依赖集成 首先我将 Mybatis 的 Starter 加入 kono-dependencies 进行依赖管理,并在 kono-app 引入: org.mybatis.spring.boot mybatis-spring-boot-starter 通过 IDEA 的 Maven 插件可以看出我们引入了 Mybatis 的 Starter 之后,高性能的数据源连接池就被集成了进来。但是数据库驱动需要我们自行引入,这里我们直接引用 MySQL 的依赖: mysql mysql-connector-java 4. 配置 配置是最重要的部分,我们来分层次讲解配置。 4.1 先配置数据源 数据源在 application.yml 中的配置前缀为 spring.datasource 。那么基本的配置如下: spring: datasource: # 连接池实现的限定名,这里使用hikari连接池。一般不用配置这个,会自动去类路径下加载,这是一个可选的配置。 # type: com.zaxxer.hikari.HikariDataSource # 数据库的驱动JDBC驱动程序的类全限定名,它其实会根据下面的url配置自动检测,这是一个可选配置。 # driver-class-name: com.mysql.cj.jdbc.Driver # 数据库的JDBC链接 url: jdbc:mysql://ip:port/database # 数据库用户名 username: # 数据库密码 password: hikari 连接池的配置可以根据需要自行通过 spring.datasource.hikari 进行配置,这里先采用默认,后续如果有需要再进行修改。 com.mysql.jdbc.Driver 已经标记为过时,现在请使用 com.mysql.cj.jdbc.Driver 。 4.2 mybatis配置 mybatis 的基本配置也不是特别多,你只需要让 Mybatis 知道从哪里加载你定义的 Mapper 接口,从哪里加载对应的 *Mapper.xml 文件,然后配置一些 mybatis 的特性,复杂的骚操作可以从我以往 mybatis 相关的文章去看一下。 我新建了一张表 user_info ,并创建了对应的实体类 UserInfo ,接着就是定义 Mapper 接口的位置。假如说我把所有的 Mapper 接口放在 cn.felord.kono.mapper 包下,那么就应该使用 @MapperScan 来标识这个路径,引导 mybatis 找到这些 Mapper 接口。 import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Configuration; /** * mybatis configuration. * * @author felord.cn */ @MapperScan("cn.felord.kono.mapper") @Configuration public class MybatisConfiguration { } 接着我们编写 UserInfo 对应的 UserInfoMapper 接口,并增加一个新增方法。这里可以根据IDE去插件市场下载一些 mybatis 插件来方便我们开发,关键字搜索 mybatis 即可,这里我随便找了一个插件安装。通过 IDEA 的 ALT+ENTER 快捷键调出了一个生成 UserInfoMapper 对应 XML 的菜单,我们可以使用它来生成 XML 文件以及对应方法的语句。 XML 文件的位置我们放在 resources 下的 mapper 文件夹中,编译后就是类路径下的 mapper 文件夹中,所以需要在 application.yml 进行如下配置。 mybatis: mapper-locations: classpath:mapper/*Mapper.xml 对应的 UserInfoMapper.xml 文件: insert into user_info (name, age) VALUES (#{name}, #{age}) 然后测试一下新增成功了没有问题,但是查询的单元测试却没有通过: @Test void testUserInfoMapperFindById(){ UserInfo userInfo = userInfoMapper.findById(3); System.out.println("userInfo = " + userInfo); Assertions.assertEquals(3,userInfo.getUserId()); } 这是因为驼峰转下划线的问题造成了, user_id 无法注入到 userId 中,所以要声明以下配置来支持下划线转驼峰就可以了: mybatis: configuration: map-underscore-to-camel-case: true 如何打印SQL语句呢?只需要声明 Mapper 接口包的日志级别为 DEBUG 就可以了。 logging: level: cn.felord.kono.mapper: debug 5. 总结 到这里基本的 mybatis 整合就完成了,你可以从项目仓库拉下来,自己配置一个数据库跑一下。多多关注: 码农小胖哥 继续来和我一起整合脚手架。 关注公众号:Felordcn获取更多资讯 个人博客:https://felord.cn
来源:OSCHINA
发布时间:2020-08-06 22:01:00
import time import hashlib filtpath="test.txt" with open(filtpath, "r") as f: print(time.strftime('%Y-%m-%d %H:%M:%S') + " 开始..." + filtpath) line_num = 0 file_num = 1 for line in f.readlines(): line = line.strip('\n') #去掉列表中每一个元素的换行符 #print(line) line_num += 1 if line_num % 50000 == 0 : file_num += 1 print(file_num) with open( filtpath[0:-4]+ '_'+ str(file_num) + '.txt','a') as file_handle: file_handle.write(line) file_handle.write('\n') print("line_num=" + str(line_num)) print(time.strftime('%Y-%m-%d %H:%M:%S') + " 结束..." + filtpath)
来源:OSCHINA
发布时间:2020-08-06 21:23:00
本文主要讲解几种常见并行模式, 具体目录结构如下图. 单例 单例是最常见的一种设计模式, 一般用于全局对象管理, 比如xml配置读写之类的. 一般分为懒汉式, 饿汉式. 懒汉式: 方法上加synchronized public static synchronized Singleton getInstance() { if (single == null) { single = new Singleton(); } return single; } 这种方式, 由于每次获取示例都要获取锁, 不推荐使用, 性能较差 懒汉式: 使用双检锁 + volatile private volatile Singleton singleton = null; public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } 本方式是对直接在方法上加锁的一个优化, 好处在于只有第一次初始化获取了锁. 后续调用getInstance已经是无锁状态. 只是写法上稍微繁琐点. 至于为什么要volatile关键字, 主要涉及到jdk指令重排, 详见之前的博文: Java内存模型与指令重排 懒汉式: 使用静态内部类 public class Singleton { private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return LazyHolder.INSTANCE; } } 该方式既解决了同步问题, 也解决了写法繁琐问题. 推荐使用改写法. 缺点在于无法响应事件来重新初始化INSTANCE. 饿汉式 public class Singleton1 { private Singleton1() {} private static final Singleton1 single = new Singleton1(); public static Singleton1 getInstance() { return single; } } 缺点在于对象在一开始就直接初始化了. Future模式 该模式的核心思想是异步调用. 有点类似于异步的ajax请求. 当调用某个方法时, 可能该方法耗时较久, 而在主函数中也不急于立刻获取结果. 因此可以让调用者立刻返回一个凭证, 该方法放到另外线程执行,后续主函数拿凭证再去获取方法的执行结果即可, 其结构图如下 jdk中内置了Future模式的支持, 其接口如下: 通过FutureTask实现 注意其中两个耗时操作. 如果doOtherThing耗时2s, 则整个函数耗时2s左右. 如果doOtherThing耗时0.2s, 则整个函数耗时取决于RealData.costTime, 即1s左右结束. public class FutureDemo1 { public static void main(String[] args) throws InterruptedException, ExecutionException { FutureTask future = new FutureTask(new Callable() { @Override public String call() throws Exception { return new RealData().costTime(); } }); ExecutorService service = Executors.newCachedThreadPool(); service.submit(future); System.out.println("RealData方法调用完毕"); // 模拟主函数中其他耗时操作 doOtherThing(); // 获取RealData方法的结果 System.out.println(future.get()); } private static void doOtherThing() throws InterruptedException { Thread.sleep(2000L); } } class RealData { public String costTime() { try { // 模拟RealData耗时操作 Thread.sleep(1000L); return "result"; } catch (InterruptedException e) { e.printStackTrace(); } return "exception"; } } 通过Future实现 与上述FutureTask不同的是, RealData需要实现Callable接口. public class FutureDemo2 { public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService service = Executors.newCachedThreadPool(); Future future = service.submit(new RealData2()); System.out.println("RealData2方法调用完毕"); // 模拟主函数中其他耗时操作 doOtherThing(); // 获取RealData2方法的结果 System.out.println(future.get()); } private static void doOtherThing() throws InterruptedException { Thread.sleep(2000L); } } class RealData2 implements Callable{ public String costTime() { try { // 模拟RealData耗时操作 Thread.sleep(1000L); return "result"; } catch (InterruptedException e) { e.printStackTrace(); } return "exception"; } @Override public String call() throws Exception { return costTime(); } } 另外Future本身还提供了一些额外的简单控制功能, 其API如下 // 取消任务 boolean cancel(boolean mayInterruptIfRunning); // 是否已经取消 boolean isCancelled(); // 是否已经完成 boolean isDone(); // 取得返回对象 V get() throws InterruptedException, ExecutionException; // 取得返回对象, 并可以设置超时时间 V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; 生产消费者模式 生产者-消费者模式是一个经典的多线程设计模式. 它为多线程间的协作提供了良好的解决方案。 在生产者-消费者模式中,通常由两类线程,即若干个生产者线程和若干个消费者线程。 生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务。 生产者和消费者之间则通过共享内存缓冲区进行通信, 其结构图如下 PCData为我们需要处理的元数据模型, 生产者构建PCData, 并放入缓冲队列. 消费者从缓冲队列中获取数据, 并执行计算. 生产者核心代码 while(isRunning) { Thread.sleep(r.nextInt(SLEEP_TIME)); data = new PCData(count.incrementAndGet); // 构造任务数据 System.out.println(data + " is put into queue"); if (!queue.offer(data, 2, TimeUnit.SECONDS)) { // 将数据放入队列缓冲区中 System.out.println("faild to put data : " + data); } } 消费者核心代码 while (true) { PCData data = queue.take(); // 提取任务 if (data != null) { // 获取数据, 执行计算操作 int re = data.getData() * 10; System.out.println("after cal, value is : " + re); Thread.sleep(r.nextInt(SLEEP_TIME)); } } 生产消费者模式可以有效对数据解耦, 优化系统结构. 降低生产者和消费者线程相互之间的依赖与性能要求. 一般使用BlockingQueue作为数据缓冲队列, 他是通过锁和阻塞来实现数据之间的同步, 如果对缓冲队列有性能要求, 则可以使用基于CAS无锁设计的ConcurrentLinkedQueue. 分而治之 严格来讲, 分而治之不算一种模式, 而是一种思想. 它可以将一个大任务拆解为若干个小任务并行执行, 提高系统吞吐量. 我们主要讲两个场景, Master-Worker模式, ForkJoin线程池. Master-Worker模式 该模式核心思想是系统由两类进行协助工作: Master进程, Worker进程. Master负责接收与分配任务, Worker负责处理任务. 当各个Worker处理完成后, 将结果返回给Master进行归纳与总结. 假设一个场景, 需要计算100个任务, 并对结果求和, Master持有10个子进程. Master代码 public class MasterDemo { // 盛装任务的集合 private ConcurrentLinkedQueue workQueue = new ConcurrentLinkedQueue(); // 所有worker private HashMap workers = new HashMap<>(); // 每一个worker并行执行任务的结果 private ConcurrentHashMap resultMap = new ConcurrentHashMap<>(); public MasterDemo(WorkerDemo worker, int workerCount) { // 每个worker对象都需要持有queue的引用, 用于领任务与提交结果 worker.setResultMap(resultMap); worker.setWorkQueue(workQueue); for (int i = 0; i < workerCount; i++) { workers.put("子节点: " + i, new Thread(worker)); } } // 提交任务 public void submit(TaskDemo task) { workQueue.add(task); } // 启动所有的子任务 public void execute(){ for (Map.Entry entry : workers.entrySet()) { entry.getValue().start(); } } // 判断所有的任务是否执行结束 public boolean isComplete() { for (Map.Entry entry : workers.entrySet()) { if (entry.getValue().getState() != Thread.State.TERMINATED) { return false; } } return true; } // 获取最终汇总的结果 public int getResult() { int result = 0; for (Map.Entry entry : resultMap.entrySet()) { result += Integer.parseInt(entry.getValue().toString()); } return result; } } Worker代码 public class WorkerDemo implements Runnable{ private ConcurrentLinkedQueue workQueue; private ConcurrentHashMap resultMap; @Override public void run() { while (true) { TaskDemo input = this.workQueue.poll(); // 所有任务已经执行完毕 if (input == null) { break; } // 模拟对task进行处理, 返回结果 int result = input.getPrice(); this.resultMap.put(input.getId() + "", result); System.out.println("任务执行完毕, 当前线程: " + Thread.currentThread().getName()); } } public ConcurrentLinkedQueue getWorkQueue() { return workQueue; } public void setWorkQueue(ConcurrentLinkedQueue workQueue) { this.workQueue = workQueue; } public ConcurrentHashMap getResultMap() { return resultMap; } public void setResultMap(ConcurrentHashMap resultMap) { this.resultMap = resultMap; } } public class TaskDemo { private int id; private String name; private int price; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } } 主函数测试 MasterDemo master = new MasterDemo(new WorkerDemo(), 10); for (int i = 0; i < 100; i++) { TaskDemo task = new TaskDemo(); task.setId(i); task.setName("任务" + i); task.setPrice(new Random().nextInt(10000)); master.submit(task); } master.execute(); while (true) { if (master.isComplete()) { System.out.println("执行的结果为: " + master.getResult()); break; } } ForkJoin线程池 该线程池是jdk7之后引入的一个并行执行任务的框架, 其核心思想也是将任务分割为子任务, 有可能子任务还是很大, 还需要进一步拆解, 最终得到足够小的任务. 将分割出来的子任务放入双端队列中, 然后几个启动线程从双端队列中获取任务执行. 子任务执行的结果放到一个队列里, 另起线程从队列中获取数据, 合并结果. 假设我们的场景需要计算从0到20000000L的累加求和. CountTask继承自RecursiveTask, 可以携带返回值. 每次分解大任务, 简单的将任务划分为100个等规模的小任务, 并使用fork()提交子任务. 在子任务中通过THRESHOLD设置子任务分解的阈值, 如果当前需要求和的总数大于THRESHOLD, 则子任务需要再次分解,如果子任务可以直接执行, 则进行求和操作, 返回结果. 最终等待所有的子任务执行完毕, 对所有结果求和. public class CountTask extends RecursiveTask{ // 任务分解的阈值 private static final int THRESHOLD = 10000; private long start; private long end; public CountTask(long start, long end) { this.start = start; this.end = end; } public Long compute() { long sum = 0; boolean canCompute = (end - start) < THRESHOLD; if (canCompute) { for (long i = start; i <= end; i++) { sum += i; } } else { // 分成100个小任务 long step = (start + end) / 100; ArrayList subTasks = new ArrayList(); long pos = start; for (int i = 0; i < 100; i++) { long lastOne = pos + step; if (lastOne > end) { lastOne = end; } CountTask subTask = new CountTask(pos, lastOne); pos += step + 1; // 将子任务推向线程池 subTasks.add(subTask); subTask.fork(); } for (CountTask task : subTasks) { // 对结果进行join sum += task.join(); } } return sum; } public static void main(String[] args) throws ExecutionException, InterruptedException { ForkJoinPool pool = new ForkJoinPool(); // 累加求和 0 -> 20000000L CountTask task = new CountTask(0, 20000000L); ForkJoinTask result = pool.submit(task); System.out.println("sum result : " + result.get()); } } ForkJoin线程池使用一个无锁的栈来管理空闲线程, 如果一个工作线程暂时取不到可用的任务, 则可能被挂起. 挂起的线程将被压入由线程池维护的栈中, 待将来有任务可用时, 再从栈中唤醒这些线程. 作者:大道方圆 cnblogs.com/xdecode/p/9137793.html 欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长
来源:OSCHINA
发布时间:2020-08-06 21:09:00
RSF 是个什么东西? 一个高可用、高性能、轻量级的分布式服务框架。支持容灾、负载均衡、集群。一个典型的应用场景是,将同一个服务部署在多个Server上提供 request、response 消息通知。使用RSF可以点对点调用,也可以分布式调用。部署方式上:可以搭配注册中心,也可以独立使用。 渊源 RSF 的核心思想参考了淘宝HSF、Dubbo 等优秀框架。功能上大体相似,但是实现逻辑完全不同。因此没有什么历史包袱。总的来说对比淘宝HSF少了历史包袱,相比Dubbo更加轻量化。而且还支持了虚拟机房,对于多机房部署的产品可以省下大量带宽成本,同时也降低了远程调用时间。 RSF虽然在功能上与两位前辈出入不大,使用RSF最直观的感受就是简单方便,配置少、依赖少,功能强大。 特点 RSF 的最大特点就是在强大的功能支持下,依然可以保持:最简单、最轻量。我们先轻描淡写的说一下RSF 的特点。 简单容易(三个一):1 行代码发布服务、1 行代码订阅服务、1 行代码使用服务 体积轻薄:RSF 是基于 Hasor 构建,此外还依赖了 netty 和 groovy。因此包含 RSF 在内引入的JAR包总数只有 5 个,其中 RSF 只有大约 700KB 的体积。 工作原理 RSF 是专门为集群、高可用系统进行设计的分布式 RPC 服务框架。服务提供者可以是一个集群,服务的消费者也可以是一个集群,两者混合在一个集群里也是ok的。同时为了增强融灾 RSF 的注册中心也是支持集群的。 所以基于 RSF 构建的服务系统不会存在任何单点问题。 框架分层 RSF 的架构设计上遵循了自顶而下明确的分层设计,每一层都有专注的工作职责。大体分为 9 个层次。他们如下所示:,你也可以理解这是 RSF 的架构设计。 第一层:是业务系统中的服务,一个服务的状态可以是提供者(Provider)、也可以是消费者(Customer),或者两者共存。 总之在这一层,出现的不是服务接口,就是服务的接口的实现 。 第二层:是一个应用程序到框架的接入层。分为提供者(Provider)、消费者(Customer)两个部分。 对于提供者(Provider)来说这一层就是框架的一个交互 API , 负责将服务接口信息提取出来让 RSF 框架可以识别到 。而对于消费者(Customer)来说,这一层的目的就是将服务接口进行动态代理。 通过代理拦截所有远程方法调用 ,这一点类似于AOP。 第三层:这一层中所有来自动态代理的接口调用都会统一转换成 RsfRequest ,同时方法的返回值也会封装成为 RsfResponse。 可以说这一层是专门为扩展性设计准备的 ,开发者在这一层中可以围绕着 RsfFilter、RsfRequest、RsfResponse 接口进行扩展。 第四层:这是一个典型的 职责链 ,职责链的开端是承接调用请求,末端承接着方法的调用。在整个职责链中开发者几乎可以为所欲为。你可以中断整个 RPC,自己 mock 数据。也可以偷梁换柱调用其它服务然后返回结果。 第五层:这一层是也是消费者(Customer)专有的设计,这一层是一个比较重要的地方,它负责维护管理并且提供服务的IP地址。举个例子:我们有 1 个服务,这个服务拥有 10 个服务提供者。那么这 10 个服务提供者的服务地址和端口信息都是在这一层维护的。当执行远程调用的时候,这一层会提供IP地址出来。 提供IP地址这个操作,有必要稍微展开说一下。向 QoS流控,跨机房调用、服务路由。这些非常重要的功能都是由这一层来提供支持。这一层用一句话来表示: 它就是地址管理器 。 第六层:这一层用“ 调度器 ”来总结说明是最贴切的。 对于提供者(Provider)来说,在这一层基于队列提供了一个 Server 的保护屏障。这个保护屏障可以保证当遇到 Client 疯狂的调用请求时,可以合理的进行回绝以保证 Server 自己不会被冲垮。对于消费者(Customer)来说,在这一层提供了请求管理器,并且提供了一个最大请求并发的控制器。 这一层可以说是 RSF 的中枢神经,因为调度器就是 RSF 线程模型的最终实现。有关线程模型在后面会有专门文章介绍一下( https://my.oschina.net/u/1166271/blog/779361 )。 第七层:是提供序列化功能,开发者想自定义序列化规则。也是由这一层提供的支持。默认 RSF 采用 Hessian 4.0.7 作为默认序列化库。同时框架内置了 Java、Json 两个策略可以选用。 如果你请求时候使用的 Hessian,数据响应想要用 JSON 。也是可以被支持的。 第八层:这一层是最底层,负责网络数据的传输。因此,在这一层 RSF 内置了一套比较完整的 RSF 数据传输协议。文章在这里: https://my.oschina.net/u/1166271/blog/342091 第九层:就是计算机的 Socket 网络通信了。如果你想,这一层可以是 TCP 也可以是 UDP。不过 RSF 采用了 TCP 长链接。
来源:OSCHINA
发布时间:2018-03-27 14:44:00
1 准备项目 1-1 eureka注册中心 pom依赖: org.springframework.cloud spring-cloud-starter-eureka-server application.yml server: port: 8761 spring: application: name: eureka eureka: client: register-with-eureka: false fetch-registry: false server: waitTimeInMsWhenSyncEmpty: 0 serviceUrl: defaultZone: http://localhost:${server.port}/eureka/ 启动类: @SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } } 1-2 zuul网关 pom依赖: org.springframework.cloud spring-cloud-starter-eureka org.springframework.cloud spring-cloud-starter-zuul application.yml server: port: 8080 spring: application: name: gateway eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true 启动类,添加 @EnableZuulProxy 注解 @SpringBootApplication @EnableZuulProxy public class GateWayApplication { public static void main(String[] args) { SpringApplication.run(GateWayApplication.class, args); } } 1-3 测试项目 准备两个测试项目:user 和 user-hello : 项目名称 spring.application.name 包含API接口 user user-hello user user-hello /hello/sayHello /sayHello pom依赖,无特殊依赖,仅加入 eureka 依赖即可: org.springframework.cloud spring-cloud-starter-eureka application.yml server: port: 8082 spring: application: name: user-hello eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true 2 测试路由 项目接口信息 项目名称 spring.application.name 包含API接口 user user-hello user user-hello /hello/sayHello /sayHello 2-1 默认路由 我们在zuul网关的服务中没有进行特殊配置,则此时的路由转发规则为:项目名称 + 具体接口,则上面接口的路由为: http://127.0.0.1:8080/user/hello/sayHello http://127.0.0.1:8080/user-hello/sayHello 2-2 指定路由 下面 user 项目的访问路径为: http://127.0.0.1:8080/gate01/hello/sayHello zuul: routes: user: path: /gate01/** serviceId: user 或者: zuul: routes: user: /gate01/** 2-3 路由优先级 路由匹配规则:“/?” 会匹配一个位置字符,“/*” 可以匹配多个字符,但是不能匹配子级路径,“/**” 可以匹配子级路径,示例如下: /user/? ---> /user/a /user/b /user/* ---> /user/aa /user/bbb /user/** ---> /user/aa/bb /user/b/c 这样的话,我们的两个项目,如果路由配置如下: zuul: routes: user-hello: path: /gate03/hello/** serviceId: user-hello user: path: /gate03/** serviceId: user 假如请求接口为:http://127.0.0.1:8080/gate03/hello/sayHello,那么 user 和 user-hello 都能匹配到该路由,这个时候由 yml 配置文件中的顺序决定,会访问到上边的 user-hello 中。 2-4 统一前缀 http://127.0.0.1:8080/api/gate01/hello/sayHello zuul: prefix: /api routes: user: /gate01/** 2-5 忽略特定路由 ignoredServices : 忽略 user-hello 服务的转发 , 但是如果配置文件中同时定义了 serviceId 指向该服务 , 那么该忽略无效 ignored-patterns : 忽略 API 地址包括 "hello" 的路由转发 zuul: ignoredServices: user-hello ignored-patterns: /**/hello/** 2-6 代码配置路由 新建 GatewayConfig 类配置文件 , 其中 PatternServiceRouteMapper 的两个参数分别为匹配微服务以及匹配路由的正则表达式 PatternServiceRouteMapper(String servicePattern, String routePattern) 如下 , 假如我们有个微服务名称为 user-hello , 那么 下面的 (?^.+)-hello 恰好能匹配到我们的微服务 , 于是该微服务的转发路由为 /user/test @Configuration public class GatewayConfig { /** * 路由匹配规则 */ @Bean public PatternServiceRouteMapper serviceRouteMapper() { return new PatternServiceRouteMapper("(?^.+)-hello", "${name}/test"); } } 3 代码 core-simple : https://code.aliyun.com/995586041/core-simple.git gateway : https://code.aliyun.com/995586041/gateway.git gateway-user : https://code.aliyun.com/995586041/gateway-user.git gateway-user-hello : https://code.aliyun.com/995586041/gateway-user-hello.git
来源:OSCHINA
发布时间:2018-05-09 09:23:00
1 config配置中心 1-1 pom依赖 这里引入 spring-cloud-config-server 即可,spring-boot-starter-security 只是为了给配置中心加一个访问验证,可以忽略该引用: org.springframework.cloud spring-cloud-config-server org.springframework.boot spring-boot-starter-security org.springframework.cloud spring-cloud-dependencies Camden.SR3 pom import 1-2 配置文件 application.yml 研发项目时,公司肯定会有几套环境,生产、测试、开发、本地等,所以我们的配置文件也应该是分别对应的,下图中是一个示例:config-file 是远程的git仓库,test 是项目下的一级文件夹,test下有两个文件夹分别存放 prd 和 dev 环境的配置文件,application.properties 是公用配置文件,config-test.properties中的 config-test 是其中一个微服务的名称,这样微服务公共的配置放到 application.properties,微服务个性化的配置在各自的配置文件中。注意:微服务的个性化配置会覆盖 application.properties 中的相同配置。 下面的配置默认加载远程仓库 test/dev 下的配置,如果密码中含有特殊字符,可以加转义符“\”,或者直接用单引号 “ ‘ ” 将密码引起来,这里用户名默认为user,密码:12345 spring: cloud: config: server: git: uri: https://code.aliyun.com/995586041/config-file.git searchPaths: test/dev username: 995586041@qq.com password: ',******' repos: prd: pattern: "*/prd*" uri: https://code.aliyun.com/995586041/config-file.git searchPaths: test/prd username: 995586041@qq.com password: \,****** dev: pattern: "*/dev*" uri: https://code.aliyun.com/995586041/config-file.git searchPaths: test/dev username: 995586041@qq.com password: ',******' server: port: 8888 security: user: password: ${CONFIG_SERVICE_PASSWORD:12345} 1-3 启动类 加上 @EnableConfigServer 注解即可 @SpringBootApplication @EnableConfigServer public class ConfigApplication { public static void main(String[] args) { SpringApplication.run(ConfigApplication.class, args); } } 1-4 访问 http://localhost:8888/test/application,如果上面设置了访问密码,会提示输入用户名和密码,填入相关信息进行验证,然后可以看到相关配置信息: 2 测试配置中心 1-1 pom文件 org.springframework.cloud spring-cloud-starter-eureka org.springframework.cloud spring-cloud-starter-config 1-2 配置文件 bootstrap.properties 这里配置文件使用 bootstrap.properties,项目启动时会先加载 bootstrap.properties 然后再加载 application.properties,这里我们指定 prd ,这样我们就可以指定了配置文件,注意这里的微服务名称为:config-test ,下边会用到 server.port = 8082 spring.profiles.active = prd spring.application.name = config-test spring.cloud.config.uri = http://127.0.0.1:8888 spring.cloud.config.fail-fast = true spring.cloud.config.username = user spring.cloud.config.password = 12345 spring.cloud.config.profile=prd goldleaf.test01 = test01 ${spring.profiles.active} bootstrap.properties goldleaf.test05 = test05 ${spring.profiles.active} bootstrap.properties goldleaf.test06 = test06 ${spring.profiles.active} bootstrap.properties 1-3 启动类及测试接口 可以看到,我们这里并没有加有关配置中心的特殊注解,我只是写了一个测试接口 @SpringBootApplication @RestController public class UserApplication { @Value("${goldleaf.test01}") private String test01; @Value("${goldleaf.test02}") private String test02; @Value("${goldleaf.test03}") private String test03; @Value("${goldleaf.test05}") private String test05; @Value("${goldleaf.test06}") private String test06; public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } @GetMapping("/test") public String getConfig() { StringBuilder builder = new StringBuilder(); builder.append("test01:" + test01 + "\r\n"); builder.append("test02:" + test02 + "\r\n"); builder.append("test03:" + test03 + "\r\n"); builder.append("test05:" + test05 + "\r\n"); builder.append("test06:" + test06 + "\r\n"); return builder.toString(); } } 1-4 测试 现在我们有了三个配置文件:项目里的 bootstrap.properties、远程的 application.properties 和远程的 config-test.properties。配置说明:1). 在上面三个文件中分别定义了 test01、test02、test03,用来说明三个配置文件中的配置都是起作用的;2). 在上面三个文件中同时定义了test05,用来说明三个文件的优先级;3). 在 bootstrap.properties 和 application.properties 中同时定义了 test06 ,用来确定二者的优先级。启动项目之后,现在我们通过访问 config-test 的test接口查看一下结果: test01-03,说明三个文件中的配置都有加载;test05 说明微服务的个性化配置会覆盖前面的配置,优先级最高;test06 说明默认情况下远程仓库的 application.properties 优先级高于 bootstrap.properties 优先级顺序:远程config-test > 远程application.properties > 本地bootstrap.properties 3 本地覆盖远程配置 在远程仓库的git仓库中添加下列配置: # 允许本地配置覆盖远程配置 spring.cloud.config.allowOverride=true spring.cloud.config.overrideNone=true spring.cloud.config.overrideSystemProperties=false 然后访问测试接口:http://127.0.0.1:8082/test,test05、test06加载的是本地bootstrap.properties中的配置信息: 4 手动刷新配置 开发的时候,难免会更改某些配置,如果每次更改配置都进行服务的重新发布,有点让人头大,所以我们在更改配置文件之后,手动刷新一下配置。 在非config端的pom文件中增加 spring-boot-starter-actuator 依赖: org.springframework.cloud spring-cloud-starter-eureka org.springframework.cloud spring-cloud-starter-config org.springframework.boot spring-boot-starter-actuator 修改git仓库的配置文件之后,POST方式调用服务的 /refresh 端点,如:http://127.0.0.1:8082/refresh 这里会返回有哪些配置点被修改: 查看结果: 5 项目代码 core-simple 项目: https://code.aliyun.com/995586041/core-simple.git config-server: https://code.aliyun.com/995586041/config.git config-client: https://code.aliyun.com/995586041/config-client.git config-file: https://code.aliyun.com/995586041/config-file.git
来源:OSCHINA
发布时间:2018-05-07 21:52:00
上一个项目的开发中需要实现从word中读取表格数据的功能,在JAVA社区搜索了很多资料,终于找到了两个相对最佳的方案,因为也得到了不少网友们的帮助,所以不敢独自享用,在此做一个分享。 两个方案分别是:一,用POI的TableIterator获取表格中的数据;二,用PageOffice来获取。   为什么说是两个相对最佳的方案呢?因为两个方案都各有优缺点,POI的优点很明显,就是免费,这正是PageOffice的缺点,PageOffice是一个国产的商业Office组件;POI的缺点有点多,接口复杂调用起来比较麻烦,尤其是不好读取word指定位置处的内容。由于获取表格数据的代码是在服务器端执行的,所以要求很高的代码质量,要考虑到代码执行效率问题、用户请求并发问题、大文档执行慢阻塞页面的问题等等,POI的架构属于仿VBA接口的模型,比VBA代码还要复杂,在调用方便上未做任何优化,光看代码都觉得头疼。所以在实际使用的过程中会遇到这些问题需要自己解决,相对来说这正是PageOffice的优点,使用PageOffice的话,就不会遇到这些问题,因为PageOffice的获取word中表格数据的工作是在客户端执行的,确实也符合了分布式计算思想,减轻服务器端压力,还有个强悍的功能,PageOffice可以从word表格中用很简单一句代码把图片提取出来!!!   PageOffice虽是收费的,但是事半功倍,而且还能实现许多POI无法实现的功能。如果确实预算紧张,还是需要用POI,再难用也要捏着鼻子用了……,闲话少撤,看代码实现。   PageOffice获取word表格中数据的核心代码: WordDocument doc = new WordDocument(request,response); DataRegion dataReg = doc.openDataRegion("PO_table"); Table table = dataReg.openTable(1); String cellValue = table.openCellRC(1,2).getValue(); //获取书签“PO_table”中表格里第1行第2列单元格的值 doc.close();   以上代码是从例子代码里拷贝出来的,可以从PageOffice的官网下载中心下载“PageOffice for JAVA ”,把PageOffice开发包里的Samples4运行起来,看示例(二、16、获取Word文件中表格的数据)里面的具体代码和实现效果。   需要说明一点,PageOffice中提到了一个数据区域(DataRegion)的概念,其实所谓的数据区域本质上就是书签,但是这个书签必须以“PO_” 开头。把表格放到数据区域中貌似不方便,但是好处很大,如果word文件中有多个表格的话,可以用数据区域去指定PageOffice获取word中哪个表格的数据,定位非常方便,比方说PO_Table的书签里有一个表格,那么不管这个表格在整个word文件中是第几个表(word中的表格没有名称只有Index,从文件头到末尾依次编号的)用doc.openDataRegion("PO_table").openTable(1);总是可以获取到这个表格的数据,非常方便,用POI就不行了,表格、图片位置移动,代码必须重写。   就写这么多吧,做个共享,希望对大家都有帮助。
来源:OSCHINA
发布时间:2018-05-07 15:35:00
一、需求背景   在项目开发中,经常会遇到导出Excel报表文件的情况,因为很多情况下,我们需要打印Excel报表,虽然在网页上也可以生成报表,但是打印网上里的报表是无法处理排版问题的,所以最好的方式,还是生成Excel文件。 PageOffice封装了一组用于动态输出数据到Excel文档的相关类,全部包含在com.zhuozhengsoft.pageoffice.excelwriter 命名空间之中。PageOffice对Excel的赋值操作分两种方式:1. 单元格赋值,这个很好理解,sheet.openCell("D5"),返回值就是一个Cell对象;2. 针对一个区域赋值。这个区域在PageOffice的概念里就是Table对象,比如:sheet.openTable("C9:H15")的返回值就是就是Table对象,这个Table就是”C9:H15”这个区域。下面就针对这两种操作方式来分别介绍。 二、 给Excel单元格赋值   创建Workbook对象,操作指定sheet中的指定单元格,在打开Excel文件后通过PageOfficeCtrl对象的setWriter方法把数据写入到Excel文件中: Workbook wb = new Workbook(); Sheet sheet = wb.openSheet("销售订单"); sheet.openCell("D5").setValue(“北京某某公司”); PageOfficeCtrl poCtrl1 = new PageOfficeCtrl(request); poCtrl1.setServerPage("poserver.do"); poCtrl1.setWriter(wb); poCtrl1.webOpen("{模板文件路径}", OpenModeType.xlsSubmitForm, "");   通过上面的代码可以看出,给Excel单元格赋值,首先需要创建Workbook对象,然后通过此对象的OpenSheet方法,获取到Sheet对象,再通过Sheet对象的OpenCell方法就可以获取的Cell对象,进行赋值或其他操作。   Sheet对象有两个方法可以获取到Cell对象:1. openCell(String CellAddress),参数为单元格引用字符串。例如:"A1";2. openCellRC(int Row, int Col),参数为excel单元格的行数和列数。所以上面给Excel单元格赋值的代码改成下面的代码也是可以的。 sheet.openCellRC(5,4).setValue(“北京某某公司”); 三、设置Cell的样式      这些属性不但可以用来设置单元格的前景色、背景色、边框、字体和对齐方式,甚至可以设置公式,基本上所有的单元格设置需求都可以实现。比如:设置一个单元格的背景色为为绿色。    Workbook wb = new Workbook(); wb.openSheet("Sheet1").openCell("E16").setBackColor(new Color(0, 128, 128));   果要设置单元格的字体,就需要操作Font对象进行设置;如果要设置单元格的边框样式,就需要操作Border对象进行设置。使用Border对象设置Excel的单元格样式,是可以分别对单元格的上下左右边框单独设置样式的,所以再复杂的表格样式用PageOffice也可以“绘制”出来。PageOffice中的Table对象可以设置Table的Border样式,所以在此不作详细的叙述,下面单独用一个章节来叙述Border的设置。 四、操作Excel中的区域(Table)    PageOffice开发平台中,针对Excel文件的处理增加了一个“Table”的概念,一个Table指的就是一个区域,例如:sheet.OpenTable("C9:H15")的返回值就是就是Table对象,这个Table所操作的区域就是”C9:H15”。 为何需要这个Table的概念呢?下面就说明一下使用Table对象的优点。 在实际的项目需求中,常常会需要在Excel 中循环的插入多条数的数据,比如:需要在excel中以B11单元格为起始位置,插入10条包含6个字段的数据,如果使用Cell对象写一个循环程序给单元格赋值会是这样的: DataTable dt = new DataTable(); for (int i = 0; i < 10; i++) // 10条数据 { sheet.OpenCellRC(“B”+(11+i).ToString())Value = dt.Rows[i][0].ToString(); sheet.OpenCellRC(“C”+(11+i).ToString())Value = dt.Rows[i][1].ToString(); sheet.OpenCellRC(“D”+(11+i).ToString())Value = dt.Rows[i][2].ToString(); sheet.OpenCellRC(“E”+(11+i).ToString())Value = dt.Rows[i][3].ToString(); sheet.OpenCellRC(“F”+(11+i).ToString())Value = dt.Rows[i][4].ToString(); sheet.OpenCellRC(“G”+(11+i).ToString())Value = dt.Rows[i][5].ToString(); }   如果使用Table对象编程,就与操作数据集的概念一样,代码也更灵活,代码如下: DataTable dt = new DataTable(); PageOffice.ExcelWriter.Table table1 = sheet.OpenTable("B11:G20"); for (int i = 0; i < 10; i++) // 10条数据 { for (int j = 0; j <6; j++) // 6个字段 { table1.DataFields[j].Value = dt.Rows[i][j].ToString(); } table1.NextRow(); } table1.Close();   通过Cell实现这个赋值操作还有一个方法,使用OpenCellRC方法,通过行列号操作会更简单,代码如下: DataTable dt = new DataTable(); for (int i = 0; i < 10; i++) // 10条数据 { for (int j = 0; j <6; j++) // 6个字段 { sheet.OpenCellRC(11+i,2+j )Value = dt.Rows[i][j].ToString(); } }   但是这个代码相对于Table对象的操作来说有点晦涩,只是看OpenCellRC中的参数是不容易立刻知道操作的是哪个单元格的,还有一个情况是通过Cell赋值无法做到的,那就是在已有的表格模板中插入不定行数的数据。例如:下图中的模板在“合计”之前只有10行空白行,怎么动态插入50条数据并且数据行的样式也统一呢?这种情况使用Cell是无法解决问题的,但是前面使用Table给Excel赋值的代码就可以解决这个问题。使用Table赋值的特点是:在赋值的过程中,如果Table所包含的区域行数不够,那么Table会自动插入行,并且循环重复使用Table区域中各行的样式,直到所有的数据都填充完毕。
来源:OSCHINA
发布时间:2018-05-11 11:33:00
修改页面出现默认值,.默认值为value="${dNotice.noticeName}"
来源:OSCHINA
发布时间:2018-05-19 14:17:00
RabbitMQ 即一个消息队列,主要是用来实现应用程序的异步和解耦,同时也能起到消息缓冲,消息分发的作用。 消息中间件在互联网公司的使用中越来越多,消息中间件最主要的作用是解耦,中间件最标准的用法是生产者生产消息传送到队列,消费者从队列中拿取消息并处理,生产者不用关心是谁来消费,消费者不用关心谁在生产消息,从而达到解耦的目的。在分布式的系统中,消息队列也会被用在很多其它的方面,比如:分布式事务的支持,RPC的调用等等。 RabbitMQ具有高可用性、高性能、灵活性等特点。 RabbitMQ介绍 RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。RabbitMQ主要是为了实现系统之间的双向解耦。当生产者大量产生数据时,消费者无法快速消费,那么需要一个中间层。保存这个数据。 AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。 RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。 相关概念 通常我们谈到队列服务, 会有三个概念: 发消息者、队列、收消息者,RabbitMQ 在这个基本概念之上, 多做了一层抽象, 在发消息者和 队列之间, 加入了交换器 (Exchange). 这样发消息者和队列就没有直接联系, 转而变成发消息者把消息给交换器, 交换器根据调度策略再把消息再给队列。 左侧 P 代表 生产者,也就是往 RabbitMQ 发消息的程序。 中间即是 RabbitMQ, 其中包括了 交换机 和 队列。 右侧 C 代表 消费者,也就是往 RabbitMQ 拿消息的程序。 那么, 其中比较重要的概念有 4 个,分别为:虚拟主机,交换机,队列,和绑定。 虚拟主机:一个虚拟主机持有一组交换机、队列和绑定。为什么需要多个虚拟主机呢?很简单,RabbitMQ当中, 用户只能在虚拟主机的粒度进行权限控制。 因此,如果需要禁止A组访问B组的交换机/队列/绑定,必须为A和B分别创建一个虚拟主机。每一个RabbitMQ服务器都有一个默认的虚拟主机“/”。 交换机: Exchange 用于转发消息,但是它不会做存储 ,如果没有 Queue bind 到 Exchange 的话,它会直接丢弃掉 Producer 发送过来的消息。 这里有一个比较重要的概念: 路由键 。消息到交换机的时候,交互机会转发到对应的队列中,那么究竟转发到哪个队列,就要根据该路由键。 绑定:也就是交换机需要和队列相绑定,这其中如上图所示,是多对多的关系。 交换机(Exchange) 交换机的功能主要是接收消息并且转发到绑定的队列,交换机不存储消息,在启用ack模式后,交换机找不到队列会返回错误。交换机有四种类型:Direct、topic、Headers、Fanout Direct:direct 类型的行为是”先匹配, 再投送”. 即在绑定时设定一个 routing_key , 消息的 routing_key 匹配时, 才会被交换器投送到绑定的队列中去. Topic:按规则转发消息(最灵活) Headers:设置header attribute参数类型的交换机 Fanout:转发消息到所有绑定队列 Direct Exchange Direct Exchange是RabbitMQ默认的交换机模式,也是最简单的模式,根据key全文匹配去寻找队列。 第一个 X - Q1 就有一个 binding key,名字为 orange; X - Q2 就有 2 个 binding key,名字为 black 和 green。 当消息中的 路由键 和 这个 binding key 对应上的时候,那么就知道了该消息去到哪一个队列中。 Ps:为什么 X 到 Q2 要有 black,green,2个 binding key呢,一个不就行了吗? - 这个主要是因为可能又有 Q3,而Q3只接受 black 的信息,而Q2不仅接受black 的信息,还接受 green 的信息。 Topic Exchange Topic Exchange 转发消息主要是根据通配符。 在这种交换机下,队列和交换机的绑定会定义一种路由模式,那么,通配符就要在这种路由模式和路由键之间匹配后交换机才能转发消息。 在这种交换机模式下: 路由键必须是一串字符,用句号( . ) 隔开,比如说 agreements.us,或者 agreements.eu.stockholm 等。 路由模式必须包含一个 星号( * ),主要用于匹配路由键指定位置的一个单词,比如说,一个路由模式是这样子:agreements..b.*,那么就只能匹配路由键是这样子的:第一个单词是 agreements,第四个单词是 b。 #就表示相当于一个或者多个单词,例如一个匹配模式是agreements.eu.berlin.#,那么,以agreements.eu.berlin开头的路由键都是可以的。 具体代码发送的时候还是一样,第一个参数表示交换机,第二个参数表示routing key,第三个参数即消息。如下: rabbitTemplate.convertAndSend("testTopicExchange","key1.a.c.key2", " this is RabbitMQ!"); topic 和 direct 类似, 只是匹配上支持了”模式”, 在”点分”的 routing_key 形式中, 可以使用两个通配符: * 表示一个词. # 表示零个或多个词. Headers Exchange headers 也是根据规则匹配, 相较于 direct 和 topic 固定地使用 routing_key , headers 则是一个自定义匹配规则的类型. 在队列与交换器绑定时, 会设定一组键值对规则, 消息中也包括一组键值对( headers 属性), 当这些键值对有一对, 或全部匹配时, 消息被投送到对应队列. Fanout Exchange Fanout Exchange 消息广播的模式,不管路由键或者是路由模式, 会把消息发给绑定给它的全部队列 ,如果配置了routing_key会被忽略。 springboot集成RabbitMQ springboot集成RabbitMQ非常简单,如果只是简单的使用配置非常少,springboot提供了spring-boot-starter-amqp项目对消息各种支持。 简单使用 1、配置pom包,主要是添加spring-boot-starter-amqp的支持 org.springframework.boot spring-boot-starter-amqp 2、配置文件 配置rabbitmq的安装地址、端口以及账户信息 spring.application.name=spirng-boot-rabbitmq spring.rabbitmq.host=192.168.0.110 spring.rabbitmq.port=5672 spring.rabbitmq.username=admin spring.rabbitmq.password=admin 3、队列配置 @Configuration public class RabbitConfig { @Bean public Queue Queue() { return new Queue("rabbit"); } } 3、发送者 rabbitTemplate是springboot 提供的默认实现 public class RabbitSender { @Autowired private AmqpTemplate rabbitTemplate; public void send() { String context = "rabbit" + new Date(); System.out.println("Sender : " + context); this.rabbitTemplate.convertAndSend("rabbit", context); } } 4、接收者 @Component @RabbitListener(queues = "rabbit") public class RabbitReceiver { @RabbitHandler public void process(String rabbit) { System.out.println("Receiver : " + rabbit); } } 5、测试 @RunWith(SpringRunner.class) @SpringBootTest public class RabbitMqTest { @Autowired private RabbitSender rabbitSender; @Test public void rabbit() throws Exception { rabbitSender.send(); } } 注意,发送者和接收者的queue name必须一致,不然不能接收 多对多使用 一个发送者,N个接收者或者N个发送者和N个接收者会出现什么情况呢? 一对多发送 对上面的代码进行了小改造,接收端注册了两个Receiver,Receiver1和Receiver2,发送端加入参数计数,接收端打印接收到的参数,下面是测试代码,发送一百条消息,来观察两个接收端的执行效果 @Test public void oneToMany() throws Exception { for (int i=0;i<100;i++){ neoSender.send(i); } } 结果如下: Receiver 1: spirng boot neo queue ****** 11 Receiver 2: spirng boot neo queue ****** 12 Receiver 2: spirng boot neo queue ****** 14 Receiver 1: spirng boot neo queue ****** 13 Receiver 2: spirng boot neo queue ****** 15 Receiver 1: spirng boot neo queue ****** 16 Receiver 1: spirng boot neo queue ****** 18 Receiver 2: spirng boot neo queue ****** 17 Receiver 2: spirng boot neo queue ****** 19 Receiver 1: spirng boot neo queue ****** 20 根据返回结果得到以下结论 一个发送者,N个接受者,经过测试会均匀的将消息发送到N个接收者中 多对多发送 复制了一份发送者,加入标记,在一百个循环中相互交替发送 @Test public void manyToMany() throws Exception { for (int i=0;i<100;i++){ neoSender.send(i); neoSender2.send(i); } } 结果如下: Receiver 1: spirng boot neo queue ****** 20 Receiver 2: spirng boot neo queue ****** 20 Receiver 1: spirng boot neo queue ****** 21 Receiver 2: spirng boot neo queue ****** 21 Receiver 1: spirng boot neo queue ****** 22 Receiver 2: spirng boot neo queue ****** 22 Receiver 1: spirng boot neo queue ****** 23 Receiver 2: spirng boot neo queue ****** 23 Receiver 1: spirng boot neo queue ****** 24 Receiver 2: spirng boot neo queue ****** 24 Receiver 1: spirng boot neo queue ****** 25 Receiver 2: spirng boot neo queue ****** 25 结论:和一对多一样,接收端仍然会均匀接收到消息 高级使用 对象的支持 springboot以及完美的支持对象的发送和接收,不需要格外的配置。 //发送者 public void send(User user) { System.out.println("Sender object: " + user.toString()); this.rabbitTemplate.convertAndSend("object", user); } ... //接收者 @RabbitHandler public void process(User user) { System.out.println("Receiver object : " + user); } 结果如下: Sender object: User{name='neo', pass='123456'} Receiver object : User{name='neo', pass='123456'} Topic Exchange topic 是RabbitMQ中最灵活的一种方式,可以根据routing_key自由的绑定不同的队列 首先对topic规则配置,这里使用两个队列来测试 @Configuration public class TopicRabbitConfig { final static String message = "topic.message"; final static String messages = "topic.messages"; @Bean public Queue queueMessage() { return new Queue(TopicRabbitConfig.message); } @Bean public Queue queueMessages() { return new Queue(TopicRabbitConfig.messages); } @Bean TopicExchange exchange() { return new TopicExchange("exchange"); } @Bean Binding bindingExchangeMessage(Queue queueMessage, TopicExchange exchange) { return BindingBuilder.bind(queueMessage).to(exchange).with("topic.message"); } @Bean Binding bindingExchangeMessages(Queue queueMessages, TopicExchange exchange) { return BindingBuilder.bind(queueMessages).to(exchange).with("topic.#"); } } 使用queueMessages同时匹配两个队列,queueMessage只匹配”topic.message”队列 public void send1() { String context = "hi, i am message 1"; System.out.println("Sender : " + context); this.rabbitTemplate.convertAndSend("exchange", "topic.message", context); } public void send2() { String context = "hi, i am messages 2"; System.out.println("Sender : " + context); this.rabbitTemplate.convertAndSend("exchange", "topic.messages", context); } 发送send1会匹配到topic.#和topic.message 两个Receiver都可以收到消息,发送send2只有topic.#可以匹配所有只有Receiver2监听到消息 Fanout Exchange Fanout 就是我们熟悉的广播模式或者订阅模式,给Fanout交换机发送消息,绑定了这个交换机的所有队列都收到这个消息。 Fanout 相关配置 @Configuration public class FanoutRabbitConfig { @Bean public Queue AMessage() { return new Queue("fanout.A"); } @Bean public Queue BMessage() { return new Queue("fanout.B"); } @Bean public Queue CMessage() { return new Queue("fanout.C"); } @Bean FanoutExchange fanoutExchange() { return new FanoutExchange("fanoutExchange"); } @Bean Binding bindingExchangeA(Queue AMessage,FanoutExchange fanoutExchange) { return BindingBuilder.bind(AMessage).to(fanoutExchange); } @Bean Binding bindingExchangeB(Queue BMessage, FanoutExchange fanoutExchange) { return BindingBuilder.bind(BMessage).to(fanoutExchange); } @Bean Binding bindingExchangeC(Queue CMessage, FanoutExchange fanoutExchange) { return BindingBuilder.bind(CMessage).to(fanoutExchange); } } 这里使用了A、B、C三个队列绑定到Fanout交换机上面,发送端的routing_key写任何字符都会被忽略: public void send() { String context = "hi, fanout msg "; System.out.println("Sender : " + context); this.rabbitTemplate.convertAndSend("fanoutExchange","", context); } 结果如下: Sender : hi, fanout msg ... fanout Receiver B: hi, fanout msg fanout Receiver A : hi, fanout msg fanout Receiver C: hi, fanout msg 结果说明,绑定到fanout交换机上面的队列都收到了消息
来源:OSCHINA
发布时间:2018-05-30 09:59:00
1. 问题描述 1.1 实体类的命名 1.2 数据库列 1.3 结果 导致 不能讲结果封装到对象上 2. 解决办法 (注意代码添加的位置) 2.1 添加 代码 2.2 测试
来源:OSCHINA
发布时间:2018-06-12 11:33:00
#prepareBeanFactory protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { // 设置 BeanFactory 的类加载器,我们知道 BeanFactory 需要加载类,也就需要类加载器, // 这里设置为当前 ApplicationContext 的类加载器 beanFactory.setBeanClassLoader(this.getClassLoader()); // 设置 BeanExpressionResolver beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader())); beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, this.getEnvironment())); // 添加一个 BeanPostProcessor,这个 processor 比较简单, // 实现了 Aware 接口的几个特殊的 beans 在初始化的时候,这个 processor 负责回调 beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this)); // 下面几行的意思就是,如果某个 bean 依赖于以下几个接口的实现类,在自动装配的时候忽略它们, // Spring 会通过其他方式来处理这些依赖 beanFactory.ignoreDependencyInterface(EnvironmentAware.class); beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class); beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class); beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class); beanFactory.ignoreDependencyInterface(MessageSourceAware.class); beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); /** * 下面几行就是为特殊的几个 bean 赋值,如果有 bean 依赖了以下几个,会注入这边相应的值, * 之前我们说过,"当前 ApplicationContext 持有一个 BeanFactory",这里解释了第一行 * ApplicationContext 继承了 ResourceLoader、ApplicationEventPublisher、MessageSource * 所以对于这几个,可以赋值为 this,注意 this 是一个 ApplicationContext * 那这里怎么没看到为 MessageSource 赋值呢?那是因为 MessageSource 被注册成为了一个普通的 bean */ beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); beanFactory.registerResolvableDependency(ResourceLoader.class, this); beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); beanFactory.registerResolvableDependency(ApplicationContext.class, this); // 这个 BeanPostProcessor 也很简单,在 bean 实例化后,如果是 ApplicationListener 的子类, // 那么将其添加到 listener 列表中,可以理解成:注册事件监听器 beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this)); //先不管这一句 if (beanFactory.containsBean("loadTimeWeaver")) { beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); } //个人理解:以下提现了spring的思想,一切东西要注入到bean工厂,这样以后处理过程中,获取方式,使用方式将统一 // 如果没有定义 "environment" 这个 bean,那么 Spring 会 "手动" 注册一个 if (!beanFactory.containsLocalBean("environment")) { beanFactory.registerSingleton("environment", this.getEnvironment()); } // 如果没有定义 "systemProperties" 这个 bean,那么 Spring 会 "手动" 注册一个 if (!beanFactory.containsLocalBean("systemProperties")) { beanFactory.registerSingleton("systemProperties", this.getEnvironment().getSystemProperties()); } // 如果没有定义 "systemEnvironment" 这个 bean,那么 Spring 会 "手动" 注册一个 if (!beanFactory.containsLocalBean("systemEnvironment")) { beanFactory.registerSingleton("systemEnvironment", this.getEnvironment().getSystemEnvironment()); } }
来源:OSCHINA
发布时间:2020-07-21 15:55:00
obtainFreshBeanFactory 前提说明在springboot的createApplicationContext的时候,BeanFactory已经创建完成了 protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { this.refreshBeanFactory(); return this.getBeanFactory(); } refreshBeanFactory是一个抽象方法,spring中两个类实现了这个方法 AbstractRefreshableApplicationContext GenericApplicationContext 会走哪一个呢,看看类继承图吧 因为springboot创建的是一个AnnotationConfigServletWebServerApplicationContext 他的架构图如下 //----- GenericApplicationContext和AbstractRefreshableApplicationContext 这里简要说明一下这两个类 AbstractApplicationContext是对ApplicationContext的一个简单抽象实现。 AbstractApplicationContext有两大子类GenericApplicationContext和AbstractRefreshableApplicationContext。 GenericApplictionContext 及其子类持有一个单例的固定的DefaultListableBeanFactory实例,在创建GenericApplicationContext实例的时候就会创建DefaultListableBeanFactory实例。 固定的意思就是说,即使调用refresh方法,也不会重新创建BeanFactory实例。 AbstractRefreshableApplicationContext 它实现了所谓的热刷新功能,它内部也持有一个DefaultListableBeanFactory实例,每次刷新refresh()时都会销毁当前的BeanFactory实例并重新创建DefaultListableBeanFactory实例。 1. refreshBeanFactory GenericApplicationContext protected final void refreshBeanFactory() throws IllegalStateException { if (!this.refreshed.compareAndSet(false, true)) { throw new IllegalStateException("GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once"); } else { this.beanFactory.setSerializationId(this.getId()); } } 很简单设置个id而已 从这里也可以明显看出 不允许重新刷新 2.getBeanFactory public final ConfigurableListableBeanFactory getBeanFactory() { return this.beanFactory; } 也很简单,返回了自己
来源:OSCHINA
发布时间:2020-07-21 15:54:00
在实际开发中,经常需要排查一条消息是否成功发送到底层MQ中,或者查看MQ中消息的内容,以及如何将消息发送给指定的/所有的消费者组重新消费。本文对RocketMQ提供到的查询机制和背后原理进行深入的介绍。文章主要包括4个部分: 消息查询介绍:介绍消息查询中使用到的Message Key 、Unique Key、Message Id 的区别 消息查询工具:分别介绍命令行工具、管理平台、客户端API这三种工具的详细用法,以及如何让消费者重新消费特定的消息。 核心实现原理:介绍Message Key & Unique Key与Message Id的实现机制上区别,Unique Key在Exactly Once语义下的作用,以及为什么Message Id查询效率更高。 索引机制:介绍Message Key & Unique Key底层使用的哈希索引机制 1 消息查询介绍 RocketMQ提供了3种消息查询方式: 按照Message Key 查询:消息的key是业务开发同学在发送消息之前自行指定的,通常会把具有业务含义,区分度高的字段作为消息的key,如用户id,订单id等。 按照Unique Key查询:除了业务开发同学明确的指定消息中的key,RocketMQ生产者客户端在发送发送消息之前,会自动生成一个UNIQ_KEY,设置到消息的属性中,从逻辑上唯一代表一条消息。 按照Message Id 查询:Message Id 是消息发送后,在Broker端生成的,其包含了Broker的地址,和在CommitLog中的偏移信息,并会将Message Id作为发送结果的一部分进行返回。Message Id中属于精确匹配,可以唯一定位一条消息,不需要使用哈希索引机制,查询效率更高。 RocketMQ有意弱化Unique Key与Message Id的区别,对外都称之为Message Id。在通过RocketMQ的命令行工具或管理平台进行查询时,二者可以通用。在根据Unique Key进行查询时,本身是有可能查询到多条消息的,但是查询工具会进行过滤,只会返回一条消息。种种情况导致很多RocketMQ的用户,并未能很好对二者进行区分。 业务开发同学在使用RocketMQ时,应该养成良好的习惯,在发送/消费消息时,将这些信息记录下来,通常是记录到日志文件中,以便在出现问题时进行排查。 以生产者在发送消息为例,通常由以下3步组成: //1 构建消息对象Message Message msg = new Message(); msg.setTopic("TopicA"); msg.setKeys("Key1"); msg.setBody("message body".getBytes()); try{ //2 发送消息 SendResult result = producer.send(msg); //3 打印发送结果 System.out.println(result); }catch (Exception e){ e.printStackTrace(); } 第1步:构建消息 构建消息对象Message,在这里我们通过setKeys方法设置消息的key,如果有多个key可以使用空格" "进行分割 第2步:发送消息 发送消息,会返回一个SendResult对象表示消息发送结果。 第3步:打印发送结果 结果中包含Unique Key和Message Id,如下所示: SendResult [ sendStatus=SEND_OK, msgId=C0A801030D4B18B4AAC247DE4A0D0000, offsetMsgId=C0A8010300002A9F000000000007BEE9, messageQueue=MessageQueue [topic=TopicA, brokerName=broker-a, queueId=0], queueOffset=0] 其中: sendStatus:表示消息发送结果的状态 msgId:注意这里的命名虽然是msgId,但实际上其是Unique Key offsetMsgId:Broker返回的Message ID 。在后文中,未进行特殊说明的情况下,Message ID总是表示offsetMsgId。 messageQueue:消息发送到了哪个的队列,如上图显示发送到broker-a的第0个的队列 queueOffset:消息在队列中的偏移量,每次发送到一个队列时,offset+1 事实上,用户主动设置的Key以及客户端自动生成的Unique Key,最终都会设置到Message对象的properties属性中,如下图所示: 其中: KEYS:表示用户通过setKeys方法设置的消息key, UNIQ_KEY:表示消息发送之前由RocketMQ客户端自动生成的Unique Key。细心的读者发现了其值与上述打印SendResult结果中的msgId字段的值是一样的,这验证了前面所说的msgId表示的实际上就是Unique Key的说法。 在了解如何主动设置Key,以及如何获取RocketMQ自动生成的Unique Key和Message Id后,就可以利用一些工具来进行查询。 2 消息查询工具 RocketMQ提供了3种方式来根据Message Key、Unique Key、Message Id来查询消息,包括: 命令行工具:主要是运维同学使用 管理平台:运维和开发同学都可以使用 客户端API:主要是开发同学使用 这些工具除了可以查询某条消息的内容,还支持将查询到的历史消息让消费者重新进行消费,下面分别进行讲述。 2.1 命令行工具 RocketMQ自带的mqadmin命令行工具提供了一些子命令,用于查询消息,如下: $ sh bin/mqadmin The most commonly used mqadmin commands are: ... queryMsgById 按照Message Id查询消息 queryMsgByKey 按照Key查询消息 queryMsgByUniqueKey 按照UNIQ_KEY查询消息 ... 此外,还有一个queryMsgByOffset子命令,不在本文讲述范畴内 2.1.1 按照Message Key查询 mqadmin工具的queryMsgByKey子命令提供了根据key进行查询消息的功能。注意,由于一个key可能对应多条消息,查询结果只会展示出这些消息对应的Unique Key,需要根据Unique Key再次进行查询。 queryMsgByKey子命令使用方法如下所示: $ sh bin/mqadmin queryMsgByKey -h usage: mqadmin queryMsgByKey [-h] -k [-n ] -t -h,--help 打印帮助信息 -k,--msgKey 指定消息的key,必须提供 -n,--namesrvAddr 指定nameserver地址 -t,--topic 指定topic,必须提供 例如,要查询在TopicA中,key为Key1的消息 $ sh bin/mqadmin queryMsgByKey -k Key1 -t TopicA -n localhost:9876 #Message ID #QID #Offset C0A80103515618B4AAC2429A6E970000 0 0 C0A80103511B18B4AAC24296D2CB0000 0 0 C0A8010354C418B4AAC242A281360000 1 0 C0A8010354C718B4AAC242A2B5340000 1 1 这里,我们看到输出结果中包含了4条记录。其中: Message ID列:这里这一列的名字显示有问题,实际上其代表的是Unique Key QID列:表示队列的ID,注意在RocketMQ中唯一地位一个队列需要topic+brokerName+queueId。这里只显示了queueId,其实并不能知道在哪个Broker上。 Offset:消息在在队列中的偏移量 在查询到Unique Key之后,我们就可以使用另外一个命令:queryMsgByUniqueKey,来查询消息的具体内容。 2.1.2 按照Unique Key查询 mqadmin工具的 queryMsgByUniqueKey 的子命令有2个功能: 根据Unique Key查询消息,并展示结果 让消费者重新消费Unique Key对应的消息 我们将分别进行讲述。queryMsgByUniqueKey子命令的使用方式如下: $ sh bin/mqadmin queryMsgByUniqueKey -h usage: mqadmin queryMsgByUniqueKey [-d ] [-g ] [-h] -i [-n ] -t -d,--clientId 消费者 client id -g,--consumerGroup 消费者组名称 -h,--help 打印帮助信息 -i,--msgId 消息的Unique Key,或者Message Id -n,--namesrvAddr NameServer地址 -t,--topic 消息所属的Topic,必须提供 这里对-i 参数进行下特殊说明,其即可接受Unique Key,即SendResult中的msgId字段;也可以接受Message Id,即SendResult中的offsetMsgId字段。 根据Unique Key查询消息: 通过-i 参数指定Unique Key,通过-t 参数指定topic,如: $ sh bin/mqadmin queryMsgByUniqueKey -i C0A80103511B18B4AAC24296D2CB0000 -t TopicA -n localhost:9876 Topic: TopicA Tags: [null] Keys: [Key1] Queue ID: 0 Queue Offset: 0 CommitLog Offset: 507625 Reconsume Times: 0 Born Timestamp: 2019-12-13 22:19:40,619 Store Timestamp: 2019-12-13 22:19:40,631 Born Host: 192.168.1.3:53974 Store Host: 192.168.1.3:10911 System Flag: 0 Properties: {KEYS=Key1, UNIQ_KEY=C0A80103511B18B4AAC24296D2CB0000, WAIT=true} Message Body Path: /tmp/rocketmq/msgbodys/C0A80103511B18B4AAC24296D2CB0000 对于消息体的内容,会存储到Message Body Path字段指定到的路径中。可通过cat命令查看(仅适用于消息体是字符串): $ cat /tmp/rocketmq/msgbodys/C0A80103511B18B4AAC24296D2CB0000 message body 指定消费者重新消费: queryMsgByUniqueKey子命令还接收另外两个参数:-g参数用于指定消费者组名称,-d参数指定消费者client id。指定了这两个参数之后,消息将由消费者直接消费,而不是打印在控制台上。 首先,通过consumerStatus命令,查询出消费者组下的client id信息,如: $ sh bin/mqadmin consumerStatus -g group_X -n localhost:9876 001 192.168.1.3@26868 V4_5_0 1576300822831/192.168.1.3@26868 Same subscription in the same group of consumer Rebalance OK 这里显示了消费者组group_X下面只有一个消费者,client id为192.168.1.3@26868。 接着我们可以在queryMsgByUniqueKey子命令中,添加-g和-d参数,如下所示: $ sh bin/mqadmin queryMsgByUniqueKey \ -g group_X \ -d 192.168.1.3@26868 \ -t TopicA \ -i C0A80103511B18B4AAC24296D2CB0000 \ -n localhost:9876 ConsumeMessageDirectlyResult [ order=false, autoCommit=true, consumeResult=CR_SUCCESS, remark=null, spentTimeMills=1] 可以看到,这里并没有打印出消息内容,取而代之的是消息消费的结果。 在内部,主要是分为3个步骤来完成让指定消费者来消费这条消息,如下图所示: 第1步: 命令行工具给所有Broker发起QUERY_MESSAGE请求查询消息,因为并不知道UNIQ_KEY这条消息在哪个Broker上,且最多只会返回一条消息,如果超过1条其他会过滤掉;如果查询不到就直接报错。 第2步: 根据消息中包含了Store Host信息,也就是消息存储在哪个Broker上,接来下命令行工具会直接给这个Broker发起CONSUME_MESSAGE_DIRECTLY请求,这个请求会携带msgId,group和client id的信息 第3步: Broker接收到这个请求,查询出消息内容后,主动给消费者发送CONSUME_MESSAGE_DIRECTLY通知请求,注意虽然与第2步使用了同一个请求码,但不同的是这个请求中包含了消息体的内容,消费者可直接处理。注意:这里并不是将消息重新发送到Topic中,否则订阅这个Topic的所有消费者组,都会重新消费这条消息。 2.1.3 根据Message Id进行查询 前面讲解生产者发送消息后,返回的SendResult对象包含一个offsetMsgId字段,这也就是我们常规意义上所说的Message Id,我们也可以根据这个字段来查询消息。 根据Message Id查询使用queryMsgById子命令,这个命令有3个作用: 根据Message Id查询消息 通知指定消费者重新消费这条消息,与queryMsgByUniqueKey类似,这里不再介绍 将消息重新发送到Topic中,所有消费者组都将重新消费 queryMsgById子命令用法如下所示: $ sh bin/mqadmin queryMsgById -h usage: mqadmin queryMsgById [-d ] [-g ] [-h] -i [-n ] [-s ] [-u ] -d,--clientId 消费者id -g,--consumerGroup 消费者组名称 -h,--help 打印帮助信息 -i,--msgId Message Id -n,--namesrvAddr Name server 地址 -s,--sendMessage 重新发送消息 -u,--unitName unit name 参数说明如下: -d和-g参数:类似于queryMsgById命令,用于将消息发送给某个消费者进行重新消费 -i 参数:指定Message Id,即SendResult对象的offsetMsgId字段,多个值使用逗号","分割。 -s参数:是否重新发送消息到Topic。如果同时指定了-d和-g参数,-s参数不生效。 根据Message Id查询消息: 下图根据SendResult的offsetMsgId字段,作为-i参数,来查询一条消息: $ sh bin/mqadmin queryMsgById -i C0A8010300002A9F000000000007BEE9 -n localhost:9876 OffsetID: C0A8010300002A9F000000000007BEE9 OffsetID: C0A8010300002A9F000000000007BEE9 Topic: TopicA Tags: [null] Keys: [Key1] Queue ID: 0 Queue Offset: 0 CommitLog Offset: 507625 Reconsume Times: 0 Born Timestamp: 2019-12-13 22:19:40,619 Store Timestamp: 2019-12-13 22:19:40,631 Born Host: 192.168.1.3:53974 Store Host: 192.168.1.3:10911 System Flag: 0 Properties: {KEYS=Key1, UNIQ_KEY=C0A80103511B18B4AAC24296D2CB0000, WAIT=true} Message Body Path: /tmp/rocketmq/msgbodys/C0A80103511B18B4AAC24296D2CB0000 与queryMsgByUniqueKey子命令输出基本类似,主要是在输出开头多出了OffsetID字段,即offsetMsgId。需要注意的是,queryMsgById不能接受Unqiue Key作为查询参数。 重新发送消息到topic: 在指定-s参数后,消息将重新发送到topic,如下(输出进行了格式化): $ sh bin/mqadmin queryMsgById -i C0A8010300002A9F000000000007BEE9 -n localhost:9876 -s true prepare resend msg. originalMsgId=C0A8010300002A9F000000000007BEE9 SendResult [ sendStatus=SEND_OK, msgId=C0A80103511B18B4AAC24296D2CB0000, offsetMsgId=C0A80103000078BF000000000004D923, messageQueue=MessageQueue [topic=TopicA, brokerName=broker-b, queueId=1], queueOffset=1] 可以看到,这里因为消息是重新发送到了Topic中,因此与我们之前使用生产者发送消息一样,输出的是一个SendResult。在这种情况下,订阅这个Topic的所有消费者组都会重新消费到这条消息。 在实际开发中,如果多个消费者组订阅了某个Topic的消息,如果所有的消费者都希望重新消费,那么就应该使用-s参数。如果只是某个消费者希望重新消费,那么应该指定-g和-d参数。 另外,我们看到发送前打印的originalMsgId和发送后SendResult中的offsetMsgId值并不一样,这是因为消息发送到Topic重新进行了存储,因此值不相同。这也是为什么我们说Message Id可以唯一对应一条消息的原因。 而输出的SendResult结果中的msgId,即Unique Key,并没有发生变化,因此尽管名字是Unique Key,但是实际上还是有可能对应多条消息的。而前面根据queryMsgByUniqueKey查询之所以只有一条消息,实际上是进行了过滤。 2.2 管理平台 RocketMQ提供的命令行工具,虽然功能强大,一般是运维同学使用较多。通过RocketMQ提供的管理平台进来行消息查询,则对业务开发同学更加友好。在管理平台的消息一栏,有3个TAB,分别用于:根据Topic时间范围查询、Message Key查询、Message Id查询,下面分别进行介绍。 根据Topic时间范围查询: 按 Topic 查询属于范围查询,不推荐使用,因为时间范围内消息很多,不具备区分度。查询时,尽可能设置最为精确的时间区间,以便缩小查询范围,提高速度。最多返回2000条数据。 根据Message Key查询: 按 Message Key 查询属于模糊查询,仅适用于没有记录 Message ID 但是设置了具有区分度的 Message Key的情况。 目前,根据Message Key查询,有一个很大局限性:不能指定时间范围,且最多返回64条数据。如果用户指定的key重复率比较高的话,就有可能搜不到。 根据Message Id查询: 按 Message ID 查询属于精确查询,速度快,精确匹配,只会返回一条结果,推荐使用。在这里,传入Unique Key,offsetMsgId都可以。 查看消息详情: 在按照Topic 时间范围查询,按照Message Key查询,结果列表有一个Message Detail按钮,点击可以看到消息详情:包括消息key、tag、生成时间,消息体内容等。在详情页面,也可以将消息直接发送给某个消费者组进行重新消费。 需要注意的是,在消息体展示的时候,只能将消息体转换成字符串进行展示,如果消息的内容是protobuf、thrift、hessian编码的,那么将显示一堆乱码。 如果公司内部有统一的IDL/Schema管理平台,则可以解决这个问题,通过为每个Topic关联一个IDL,在消息展示时,可以根据IDL反序列化后在进行展示。 2.3 客户端API 除了通过命令行工具和管理平台,还可以通过客户端API的方式来进行查询,这其实是最本质的方式,命令行工具和管理平台的查询功能都是基于此实现。 在org.apache.rocketmq.client.MQAdmin接口中,定义了以下几个方法用于消息查询: //msgId参数:仅接收SendResult中的offsetMsgId,返回单条消息 MessageExt viewMessage(final String msgId) //msgId参数:传入SendResult中的offsetMsgId、msgId都可以,返回单条消息 MessageExt viewMessage(String topic,String msgId) //在指定topic下,根据key进行查询,并指定最大返回条数,以及开始和结束时间 QueryResult queryMessage(final String topic, final String key, final int maxNum, final long begin,final long end) 对于MQAdmin接口,可能部分同学比较陌生。不过我们常用的DefaultMQProducer、DefaultMQPushConsumer等,都实现了此接口,因此都具备消息查询的能力,如下所示: 对于命令行工具,底层实际上是基于MQAdminExt接口的实现来完成的。 细心的读者会问,相同的查询功能在在多处实现是不是太麻烦了?事实上,这只是对外暴露的接口,在内部,实际上都是基于MQAdminImpl这个类来完成的。 viewMessage方法: 两种viewMessage方法重载形式,都只会返回单条消息。下面以生产者搜索为例,讲解如何使用API进行查询: //初始化Producer DefaultMQProducer producer = new DefaultMQProducer(); producer.setNamesrvAddr("127.0.0.1:9876"); producer.start(); //根据UniqueKey查询 String uniqueKey = "C0A8010354C418B4AAC242A281360000"; MessageExt msg = producer.viewMessage("TopicA", uniqueKey); //打印结果:这里仅输出Unique Key与offsetMsgId MessageClientExt msgExt= (MessageClientExt) msg; System.out.println("Unique Key:"+msgExt.getMsgId()//即UNIQUE_KEY +"\noffsetMsgId:"+msgExt.getOffsetMsgId()); 输出结果如下: Unique Key:C0A8010354C418B4AAC242A281360000 offsetMsgId:C0A8010300002A9F000000000007BF94 如果我们把offsetMsgId当做方法参数传入,也可以查询到相同的结果。这是因为,在方法内部实际上是分两步进行查询的: 先把参数当做offsetMsgId,即Message Id进行查询 如果失败,再尝试当做Unique Key进行查询。 源码如下所示: DefaultMQProducer#viewMessage(String,String) @Override public MessageExt viewMessage(String topic, String msgId) {//省略异常声明 try { //1 尝试当做offsetMsgId进行查询 MessageId oldMsgId = MessageDecoder.decodeMessageId(msgId); return this.viewMessage(msgId); } catch (Exception e) { //查询失败直接忽略 } //2 尝试当做UNIQ_KEY进行查询 return this.defaultMQProducerImpl.queryMessageByUniqKey(topic, msgId); } 前面提到,Unique Key只是从逻辑上代表一条消息,实际上在Broker端可能存储了多条,因此在当做Unique Key进行查询时,会进行过滤,只取其中一条。源码如下所示: MQAdminImpl#queryMessageByUniqKey public MessageExt queryMessageByUniqKey(String topic,String uniqKey) { //根据uniqKey进行查询 QueryResult qr = this.queryMessage(topic, uniqKey, 32, MessageClientIDSetter.getNearlyTimeFromID(uniqKey).getTime() - 1000, Long.MAX_VALUE, true); //对查询结果进行过滤,最多只取一条 if (qr != null && qr.getMessageList() != null && qr.getMessageList().size() > 0) { return qr.getMessageList().get(0); } else { return null; } } 我们也可以通过另外只接收一个参数的viewMessage方法进行查询,但是需要注意的是,参数只能是offsetMsgId,不能是Unique Key。 String offsetMsgId = "C0A8010300002A9F000000000007BF94"; producer.viewMessage(offsetMsgId); queryMessage方法: 其是根据消息Key进行查询,这里不再介绍API如何使用。则与前面两种viewMessage方法重载不同,其返回的是一个QueryResult对象,包含了多条消息。 主要是注意这个方法接收时间范围参数,相比较于管理平台更加灵活。管理平台按照消息Key查询,默认最多返回64条消息,且不能支持指定时间范围,如果消息Key重复度较高,那么可能有些消息搜索不到。如果是在指定时间范围内返回64条消息,如果没有发现想找到的消息,再选择其他时间范围,则可以规避这个问题。 3 实现原理 Unqiue Key & Message Key都需要利用RocketMQ的哈希索引机制来完成消息查询,由于建立索引有一定的开销,因此Broker端提供了相关配置项来控制是否开启索引。关于RocketMQ索引机制将在后面的文章进行详细的介绍。 Message Id是在Broker端生成的,其包含了Broker地址和commit Log offset信息,可以精确匹配一条消息,查询消息更好。下面分别介绍 Unqiue Key & Message Id的生成和作用。 3.1 Unique Key的生成与作用 3.1.1 Unique Key生成 Unique Key是生产者发送消息之前,由RocketMQ 客户端自动生成的,具体来说,RocketMQ发送消息之前,最终都要通过以下方法: DefaultMQProducerImpl#sendKernelImpl private SendResult sendKernelImpl(final Message msg, final MessageQueue mq, final CommunicationMode communicationMode, final SendCallback sendCallback, final TopicPublishInfo topicPublishInfo, final long timeout) {//省略异常声明 //...略 try { //如果不是批量消息,则生成Unique Key if (!(msg instanceof MessageBatch)) { MessageClientIDSetter.setUniqID(msg); } //...略 如上所示,如果不是批量消息,会通过 MessageClientIDSetter 的 setUniqID 方法为消息设置Unique key,该方法实现如下所示: MessageClientIDSetter#setUniqID public static void setUniqID(final Message msg) { // Unique Key不为空的情况下,才进行设置 if (msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX,) == null) { msg.putProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX,, createUniqID()); } } 如果消息的Unique Key属性为null,就通过createUniqID()方法为消息创建一个新的Unique Key,并设置到消息属性中。之所以要判断Unique Key是否为null与其作用有关。 3.1.2 Unique Key作用 了解Unique Key的作用对于我们理解消息重复的原因有很大的帮助。RocketMQ并不保证消息投递过程中的Exactly Once语义,即消息只会被精确消费一次,需要消费者自己做幂等。而通常导致消息重复消费的原因,主要包括: 生产者发送时消息重复:RocketMQ对于无序消息发送失败,默认会重试2次。对于有序消息和普通有序消息为什么不进行重试,可参考: RocketMQ NameServer详解 消费者Rebalance时消息重复:这里不做介绍,可参考 RocketMQ Rebalance机制详解 导致生产者发送重复消息的原因可能是:一条消息已被成功发送到服务端并完成持久化,由于网络超时此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败,此时生产者将再次尝试发送消息。 在重试发送时,sendKernelImpl会被重复调用,意味着setUniqID方法会被重复调用,不过由于setUniqID方法实现中进行判空处理,因此重复设置Unique Key。在这种情况下,消费者后续会收到两条内容相同并且 Unique Key 也相同的消息(offsetMsgId不同,因为对Broker来说存储了多次)。 那么消费者如何判断,消费重复是因为重复发送还是Rebalance导致的重复消费呢? 消费者实现MessageListener接口监听到的消息类型是MessageExt,可以将其强制转换为MessageClientExt,之后调用getMsgId方法获取Unique Key,调用getOffsetMsgId获得Message Id。如果多消息的Unique Key相同,但是offsetMsgId不同,则有可能是因为重复发送导致。 3.1.3 批量发送模式下的Unique Key DefaultMQProducer提供了批量发送消息的接口: public SendResult send(Collection msgs) 在内部,这批消息首先会被构建成一个MessageBatch对象。在前面sendKernelImpl方法中我们也看到了,对于MessageBatch对象,并不会设置Unique Key。这是因为在将批量消息转换成MessageBatch时,已经设置过了。 可能有一部分同学会误以为一个批量消息中每条消息Unique Key是相同的,其实不然,每条消息Unique Key都不同。 这里通过一个批量发送案例进行说明: //构建批量消息 ArrayList msgs = new ArrayList<>(); Message msg1 = new Message("Topic_S",("message3").getBytes()); Message msg2 = new Message("Topic_S",("message4").getBytes()); msgs.add(msg1); msgs.add(msg2); //发送 SendResult result = producer.send(msgs); //打印 System.out.println(result); 输出如下所示: SendResult [sendStatus=SEND_OK, msgId=C0A80103583618B4AAC24CDC29F10000,C0A80103583618B4AAC24CDC29F10001, offsetMsgId=C0A80103000051AF00000000000B05BD,C0A80103000051AF00000000000B065B, messageQueue=MessageQueue [topic=Topic_S, brokerName=broker-c, queueId=2], queueOffset=3] 可以看到,此时输出的msgId(即Unique Key)和offsetMsgId都会包含多个值。客户端给批量消息中每条消息设置不同的Unqiue Key,可以参考DefaultMQProducer#batch方法源码: private MessageBatch batch(Collection msgs) throws MQClientException { MessageBatch msgBatch; try { //1 将消息集合转换为MessageBatch msgBatch = MessageBatch.generateFromList(msgs); //2 迭代每个消息,逐一设置Unique Key for (Message message : msgBatch) { Validators.checkMessage(message, this); MessageClientIDSetter.setUniqID(message); } //3 设置批量消息的消息体 msgBatch.setBody(msgBatch.encode()); } catch (Exception e) { throw new MQClientException("Failed to initiate the MessageBatch", e); } return msgBatch; } 3.2 Message Id生成 SendResult中的offsetMsgId,即常规意义上我们所说的Message Id是在Broker端生成的,用于唯一标识一条消息,在根据Message Id查询的情况下,最多只能查询到一条消息。Message Id总共 16 字节,包含消息存储主机地址,消息 Commit Log offset。如下图所示: RocketMQ内部通过一个 MessageId 对象进行表示: public class MessageId { private SocketAddress address; //broker地址 private long offset; //commit log offset 并提供了一个 MessageDecoder 对象来创建或者解码MessageId。 public static String createMessageId(final ByteBuffer input, final ByteBuffer addr, final long offset) public static MessageId decodeMessageId(final String msgId) Broker端在顺序存储消息时,首先会通过createMessageId方法创建msgId。源码如下所示: CommitLog.DefaultAppendMessageCallback#doAppend public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank,final MessageExtBrokerInner msgInner) { //1 PHY OFFSET:即Commit Log Offset 或者称之为msgOffsetId long wroteOffset = fileFromOffset + byteBuffer.position(); //2 hostHolder用于维护broker地址信息 this.resetByteBuffer(hostHolder, 8); //3 创建msgOffsetId String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset); 而客户端在根据msgId向Broker查询消息时,首先会将通过MessageDecoder的decodeMessageId方法,之后直接向这个broker进行查询指定位置的消息。 参见:MQAdminImpl#viewMessage public MessageExt viewMessage(String msgId) {//省略异常声明 //1 根据msgId解码成MessageId对象 MessageId messageId = null; try { messageId = MessageDecoder.decodeMessageId(msgId); } catch (Exception e) { throw new MQClientException(ResponseCode.NO_MESSAGE, "query message by id finished, but no message."); } //2 根据MessageId中的Broker地址和commit log offset信息进行查询 return this.mQClientFactory.getMQClientAPIImpl().viewMessage( RemotingUtil.socketAddress2String(messageId.getAddress()), messageId.getOffset(), timeoutMillis); } 由于根据Message Id进行查询,实际上是直接从特定Broker的CommitLog中的指定位置进行查询的,属于精确匹配,并不像用户设置的key,或者Unique Key那么样,需要使用到哈希索引机制,因此效率很高。 4 总结 RocketMQ提供了3种消息查询方式:Message Key & Unique Key & Message Id RocketMQ提供了3种消息查询工具:命令行、管理平台、客户端API,且支持将查询到让特定/所有消费者组重新消费 RocketMQ有意对用户屏蔽Unique Key & Message Id区别,很多地方二者可以通用 Message Key & Unique Key 需要使用到哈希索引机制,有额外的索引维护成本 Message Id由Broker和commit log offset组成,属于精确匹配,查询效率更好 免费学习视频欢迎关注云图智联: https://e.yuntuzhilian.com/
来源:OSCHINA
发布时间:2020-07-21 15:12:00
程序开始启动 当系统上电后根据BOOT的引导配置选择启动方式,默认是Flash启动,这时系统开始把所有的代码段搬到RAM中去运行 CPU就从内存中(RAM)获取数据和指令,根据相关指令来控制系统运行 数据存放位置:除了特定IO操作存到EEPROM里面,其他变量的使用全在RAM区,其中就有堆和栈。堆是由用户手动分配malloc,在整个程序运行期间都有效除非手动释放free。而栈在一个函数体内存在,例如main方法由系统自动创建,如果出现递归创建则很容易导致栈溢出,当系统执行完该函数功能后,栈空间数据也由系统自动销毁。 ROM和RAM数据比较 堆和栈的实际定义大小可查看,在.map文件中 总结 内存大小直接体现该系统所能同时运行的任务数,而 所有系统运行中产生的数据存放位置全都在RAM区 。除了读写磁盘
来源:OSCHINA
发布时间:2020-07-21 15:07:00
关于新的activiti新团队与原有的团队重要开发人员我们罗列一下,细节如下: Tijs Rademakers,算是activiti5以及6比较核心的leader了。现在是flowable框架的leader。 Joram Barrez 算是activiti5以及6比较核心的leader了。目前从事flowable框架开发。 Salaboy Activiti Cloud BPM leader(Activiti Cloud BPM 也就是目前的activiti7框架) Tijs Rademakers以及Salaboy目前是两个框架的leader。 特此强调一点:activiti5以及activiti6、flowable是Tijs Rademakers团队开发的。 Activiti7是 Salaboy团队开发的。activiti6以及activiti5代码目前有 Salaboy团队进行维护。因为Tijs Rademakers团队去开发flowable框架了,所以activiti6以及activiti5代码已经交接给了 Salaboy团队(可以理解为离职之前工作交接)。目前的activiti5以及activiti6代码还是原Tijs Rademakers原有团队开发的。Salaboy团队目前在开发activiti7框架。对于activiti6以及activiti5的代码官方已经宣称暂停维护了。activiti7就是噱头 内核使用的还是activiti6。并没有为引擎注入更多的新特性,只是在activiti之外的上层封装了一些应用。 注意:activiti6的很多框架bug在flowable框架中已经修复的差不多了。 activiti5以及ativiti6的核心开发团队是Tijs Rademakers团队。activiti6最终版本由Salaboy团队发布的。 activiti6核心代码是Tijs Rademakers团队开发的,为何是Salaboy团队发布的呢?很简单,因为这个时候Tijs Rademakers团队已经去开发flowable去了。flowable是基于activiti-6.0.0.Beta4 分支开发的。下面我们截图一些flowable的发展。 目前Flowable已经修复了activiti6很多的bug,可以实现零成本从activiti迁移到flowable。 flowable目前已经支持加签、动态增加实例中的节点、支持cmmn、dmn规范。这些都是activiti6目前版本没有的。 1、flowable已经支持所有的历史数据使用mongdb存储,activiti没有。 2、flowable支持事务子流程,activiti没有。 3、flowable支持多实例加签、减签,activiti没有。 4、flowable支持httpTask等新的类型节点,activiti没有。 5、flowable支持在流程中动态添加任务节点,activiti没有。 6、flowable支持历史任务数据通过消息中间件发送,activiti没有。 7、flowable支持java11,activiti没有。 8、flowable支持动态脚本,,activiti没有。 9、flowable支持条件表达式中自定义juel函数,activiti没有。 10、flowable支持cmmn规范,activiti没有。 11、flowable修复了dmn规范设计器,activit用的dmn设计器还是旧的框架,bug太多。 12、flowable屏蔽了pvm,activiti6也屏蔽了pvm(因为6版本官方提供了加签功能,发现pvm设计的过于臃肿,索性直接移除,这样加签实现起来更简洁、事实确实如此,如果需要获取节点、连线等信息可以使用bpmnmodel替代)。工作流框架项目源码:www.1b23.com 13、flowable与activiti提供了新的事务监听器。activiti5版本只有事件监听器、任务监听器、执行监听器。 14、flowable对activiti的代码大量的进行了重构。 15、activiti以及flowable支持的数据库有h2、hsql、mysql、oracle、postgres、mssql、db2。其他数据库不支持的。使用国产数据库的可能有点失望了,需要修改源码了。 16、flowable支持jms、rabbitmq、mongodb方式处理历史数据,activiti没有。
来源:OSCHINA
发布时间:2020-07-21 14:22:06