暂时未有相关云产品技术能力~
前面的几篇文章,分别讲述了如何 使用 Jenkins 自动化部署前端 和 Jenkins 自动化部署后端的 Maven 项目,接下来讲一讲 Jenkins 构建后的邮件通知,相对来说也是比较实用的一个点吧~所有的操作都是可以实现的,希望感兴趣的读者,可以实操一下,有服务器或者虚拟机的小伙伴们都可以尝试尝试,不懂的可以留言,我也非常开心收到的评价和回复。今天有小伙伴问我,2G 的云服务器可不可以搭建 Jenkins ,答案是可以的,Jenkins 占的内存空间在 1-1.1 G左右,而一般 2G 的云服务器一般可用内存在 1.7G左右,所以部署Jenkins + 一个小型的项目,还是扛的住的。至于学习如何使用 Jenkins ,那是完全没问题的(单体项目及Demo项目)一、前言在构建部署完全自动化后,说真的,我们很少会专门去打开 Jenkins 看看他有没有构建成功或失败。因此一些必要的通知相对来说是一个必需品。让我们能感知到它的构建是否成功,测试的小伙伴是否可以开始测试等等等~如果你对之前的知识稍有不熟悉:1、Docker 安装 Nginx 部署 Vue 项目2、Docker + Jenkins + Github 实现自动化部署 Maven 项目(包含如何使用Docker安装Jenkins、下载插件、系统配置、环境配置等等,可以实操部署成功)3、Docker + Jenkins + Nginx + Github 实现自动化部署前端项目可以点过去复习一下~二、Jenkins 安装插件1、安装插件Jenkins 本身含有邮件相关的配置,但是相对于不能满足一些大佬的需求,就有了相关的插件的出现~然后的话,我使用的是这款插件勾选完直接点击 install 即可~(图片说明:出现 success or 完成即表示安装成功)2、系统配置(图片说明:点击进去进行系统配置)2.1、配置 Jenkins Location在这个页面,找到 Jenkins Location 的配置项,这里的填写的邮箱,就是你刚刚拿到授权码的那个邮箱2.2、配置 Extended E-mail Notification然后找到 Extended E-mail Notification 配置项,我这里使用的是 qq 邮箱,第三方终端登录,都是授权码登录的方式,这一步大家百度一下下就行,很简单的,就是要花一毛钱的短信费用~(图片说明:点击添加一个凭据)(图片说明:大伙们记得点保存噢)(图片说明:配置完这里,直接往下划,找到Default Subject配置项,下一步从这里开始)说明:1、Default Subject我们修改为【构建通知】$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS!其中 PROJECT_NAME、BUILD_NUMBER、BUILD_STATUS都是 Jenkins 内置的环境变量更多的环境变量的大家可参考官网给出的信息:链接2、Maximum Attachment Size的意思设置限制发送邮件时所带附件的大小,不写或是-1就是不限制大小3、Default Content 就是发送邮件时的内容,这里的内容我是从网上粘贴而来:<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title> </head> <body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0"> <table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif"> <tr> 本邮件由系统自动发出,无需回复!<br/> 各位同事,大家好,以下为${PROJECT_NAME }项目构建信息</br> <td><font color="#CC0000">构建结果 - ${BUILD_STATUS}</font></td> </tr> <tr> <td><br /> <b><font color="#0B610B">构建信息</font></b> <hr size="2" width="100%" align="center" /></td> </tr> <tr> <td> <ul> <li>项目名称 : ${PROJECT_NAME}</li> <li>构建编号 : 第${BUILD_NUMBER}次构建</li> <li>触发原因: ${CAUSE}</li> <li>构建状态: ${BUILD_STATUS}</li> <li>构建日志: <a href="${BUILD_URL}console">${BUILD_URL}console</a></li> <li>构建 Url : <a href="${BUILD_URL}">${BUILD_URL}</a></li> <li>工作目录 : <a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li> <li>项目 Url : <a href="${PROJECT_URL}">${PROJECT_URL}</a></li> </ul> <h4><font color="#0B610B">失败用例</font></h4> <hr size="2" width="100%" /> $FAILED_TESTS<br/> <h4><font color="#0B610B">最近提交(#$SVN_REVISION)</font></h4> <hr size="2" width="100%" /> <ul> ${CHANGES_SINCE_LAST_SUCCESS, reverse=true, format="%c", changesFormat="<li>%d [%a] %m</li>"} </ul> 详细提交: <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a><br/> </td> </tr> </table> </body> </html>配置完展示(图片说明:展示效果)(图片说明:Default Triggers 即设置默认的触发器)不过为了一些细节问题,我把上面勾选的 Enable Debug Mode 也说一说吧,这里的意思是发送邮件的时候,将向构建日志添加额外的日志输出,方便查看错误。(请注意:这对于调试很有用,但不应该在完全生产模式下使用,这会影响性能)下面几个的意思分别是:1、需要管理员进行模板测试 2、启用监视工作 3、允许发送给未注册用户下面再来看看触发器。勾上的 Always 意思就是总是,触发事件就算出现任何错误或构建成功都将发送邮件。(图片说明:此配置项,配置到这里就算是结束了)2.3、配置邮件通知在此页面找到邮件通知的配置项打开高级选项后配置到这里,配置就算是结束了,我们先来进行测试一番~成功是这样的:收到的邮件是这样的:三、修改任务配置点进你已经拥有的任务,我们来修改配置3.1、增加构建后配置直接划到配置的最下方,构建后操作Project Recipient List:项目收件人列表Project Reply-To List:项目回复列表,这里我就一个账号,默认的就是我们配置的那个账号~接着往下看Attachments的读取路径是主目录下的workspace文件夹 ,所以想要将报告以附件的形式发送的话,就需要在build文件中将报告的生成路径更改为项目的工作空间~~第一次弄吗,我们整简单的(我懈怠了~)3.2、测试修改本地代码,push到远程仓库上,查看构建结果。后记虽然文章较为基础,但是有收获到的一些朋友的喜欢,这让我感觉到十分的开心。你作为读者收获到知识,我作为创作者收获到你的认可或点赞,相互认可~写于2022 年 8 月 4 日晚,作者:宁在春
上一篇文章其实已经介绍过如何利用 Jenkins + Github + Docker 部署一个 Maven 项目,同时也包含了如何使用Docker 安装 Jenkins ,以及一些基本概念 📌文章链接有了后端,那么必然也少不了前端,所以就诞生了本文。前言看起来好像 Jenkins 非常复杂,但其实只要自己多实操几次,一次又一次的去想如何偷懒,你就可以一步一步发现更多的知识点,要相信好奇永远是你的第一老师。关于如何使用 Docker 安装 Jenkins,Jenkins 插件安装配置、系统配置等,都已在 关于 Docker + Jenkins +Github 实现自动化 中全部陈述。先说说本文最后做出来的效果:本地开发,push 到 github 仓库后触发 Github 的钩子函数,通知 Jenkins ,进行重新构建Jenkins 构建完成后,将前端打包出来的 dist 目录,发送到部署的服务器上的 Nginx 容器挂载的部署目录下进行访问测试除了第一步是需要自己动手外,其余部分实现自动化。前一篇文章主要介绍了 Jenkins 如何构建一个 Maven 项目,但其实大家可以看到 Jenkisn 还有其他几钟不同的构建项目的方式。本篇文章用到的是自由风格式(Freestyle project)部署前端项目。也很简单的哈。一、初始化项目如果需要跟着文章实操,大致需要以下环境:一台云服务器或本地虚拟机服务器或虚拟机上需要联网、Docker 环境一个 Github 账号开发机器上需要有 Git 和 开发环境1、初始化 Vue 项目其实我也不知道这一步该不该写......重点就是大家准备一个可以运行和打包的 Vue 项目。如果有小伙伴,没的话,我有~,给你指路: jenkins-vue-demo拉下来之后,把 .git 文件删除掉,然后重新关联你的github 仓库就好~2、推送至 Github 仓库在 github 建立一个仓库 (默认大家都会哈~ 不会可以留言的,摸鱼的时候会回复的,别慌)然后在本地项目目录下执行下面的命令,其实不写,在你创建仓库的时候也会给出这些提示命令git init git add . git commit -m "init" git branch -M main git remote add 远程仓库地址 git push origin main二、设置 Github1、设置通知 WebHook在github 上点击仓库,按下图顺序之后点击创建即可2、创建一个 Personal access tokens三 、Jenkins 部署 Vue 项目1、安装 Nodejs 插件等待完成即可2、配置 Nodejs本地机器查看 node 版本 命令为 node -v3、系统配置之前我们在第四小节,只是在Jenkins中进行了打包,并未发布在服务器上。如果要发布在服务器上,我们还需要配置一下 远程服务器信息。此处还需要下载两个插件SSH : SSH 连接工具Publish Over SSH :SSH 发布工具稍详细的描述在我上一篇文章:Docker + Jenkins + GitHub 自动化部署 Maven 项目找到两个配置:1、SSH remote hosts2、SSH Servers对了记得点击保存哈,不然又得重现填写。4、创建一个自由风格式任务(图片说明:指定分支应为 main,图中有误)(图片说明:变量无需填写)(图片说明:选择 secret text)(图片说明:描述就是取一个名称)(图片说明:选择自己添加的那个 凭据)npm cache clear --force # 清理 npm 缓存,之前我一直报错,第一次之后大家可以修改修改 ~ npm --registry https://registry.npm.taobao.org install cluster # 配置淘宝镜像 npm install --force npm run build echo "打包完成"执行到这一步时,我们已经可以测试我们当前的这个自由风格的任务了。点击立即构建,看看git有没有成功拉取,有没有打包成功。第一次构建的时间也会稍长,需要拉取项目,下载Nodejs,下载依赖等,这些信息都会在控制台上可查看:成功的输出应该如下:5、浅提一下Nginx谈到部署前端项目时,大部分情况下我们不可避免的会谈到Nginx服务器。Nginx 这个中间件,不管是对于后端还是前端,都是需要了解的一个服务器。想要深入的了解它,可能还需要你好好的花费一些时间。关于Docker 安装 Nginx 部署 前端项目 ,我之前已经写好,链接:Docker 安装 Nginx后面的小节,都是默认大家已经安装好了Nginx~我的Nginx 的 server配置如下:location /hello { alias /usr/share/nginx/html/www/hello/dist; try_files $uri $uri/ /hello/index.html; index index.html index.htm; }我们部署成功访问的路径是:IP : Port/hello/ ,例如:192.168.1.100/hello/ 就是访问此项目的的地址。详细的还是得大家去了解一下。6、修改Jenkins 任务配置打开任务配置,直接划到最下面然后选中之前配置的服务器#/bin/bash # 其实在这里执行的命令,就是在你选择的那台服务器上执行的命令 echo ">>>>>>>>>>>>>开始执行 此处的 /home/nginx/html/web 是我 nginx 容器 挂载的目录 >>>>>>>>>>>>>" cd /home/nginx/html/www/hello/dist/ rm -rf dist echo ">>>>>>>>>>>>>cd到Jenkins工作挂载目录下>>>>>>>>>>>>>" cd /home/jenkins/workspace/jenkins-vue-demo echo ">>>>>>>>>>>> 将dist文件夹复制到 nginx 的挂载目录下 >>>>>>>>>>>>>" cp -r dist /home/nginx/html/www/hello/ echo ">>>>>>>>>>>>复制成功 启动成功>>>>>>>>>>>>>"注意:此处我是直接采取将 dist 目录直接放在了 Nginx 部署的目录下的,请注意:我这里并非是一个合格的方式,只能说是用来写Demo倒也无妨。请大家不要照抄~.7、最佳实践看过上一篇文章的读者可能知道,真正的应用场景中这样的部署并不安全,一旦出现bug,甚至都没法立马回退版本,或者出现意外情况没法快速横向扩容.所以大概率下,最佳实践应当是在 vue 项目中增加 Dockerfile 文件 和 nginx.conf 配置文件部署时,首先将 dist + Dockerfile + nginx.conf 打成镜像 (docker build 相关明令)将打包出来的镜像上传至存储应用的服务器或DockerHub(私服仓库)最后在部署服务器上从存储镜像的那台服务器上拉取镜像,执行 docker run 相关命令进行发布.测试原本是没有这一小节的,但是读了一遍,感觉有点遗漏,就新增了这一段。当时在写的时候,前期的Nginx环境已经搭好,一定程度上少了一些思考,所以就补了这一小节。关于 Vue 项目利用 Dockerfile 打包成镜像部署,以往也写过一篇不成熟的博客,如果有想改造成镜像发布的小伙伴,可以参考一下。链接:Docker 部署 vue项目8、测试自动构建我们在本地修改文件,然后推送到远程仓库中,你刷新jenkins页面,就会发现它已经开始在构建啦。(图片说明:此处我放上的是刚写文的测试)请注意:Github 因为是公用资源,有一定程度延迟,这一点在自己搭建的GitLab的私有仓库上是没有的。看看我滴测试结果~后记看完这两篇文章,可能会有小伙伴产生一种感觉自己会用 Jenkins 了,哈哈,我也有这种感觉,但其实还差得很常远的,不过自己简单使用是肯定没啥问题了。希望让你有所收获~,很开心你读到这里,听我讲完废话写于 2022 年 8 月 3 日晚,作者:宁在春
之前编写了 Docker 安装 Nginx 镜像部署前端Vue项目,也算是我写 Docker 集成 Jenkins 自动化部署专栏开篇文。趁着周末写出了这篇文章,原本只打算写一点点,写上头,就直接一篇解决了~本文更加偏向于实操,阅读完的收获清楚怎么使用Docker安装 Jenkins明白如何利用Jenkins部署一个Maven项目知晓Jenkins如何结合Github实现自动化部署一、Jenkins 介绍看到这篇文章的你,或多或少都已经对 Jenkins 有过一定了解,就算没有也一定已经听过它的相关话题。在我们学习阶段,常会听到持续集成和持续部署这样的词语,有些小伙伴们已经亲手实践过,还有些没有过,今天就让我们一起对 Jenkins 做一个了解吧。1、持续集成和持续部署是什么持续集成:CI 是一种开发实践,其中开发人员一天几次将代码集成到共享存储库中。当有人将新代码推送到共享存储库中时,测试会在非开发人员(测试人员)的计算机上自动运行。这种纯手动的构建测试,效率非常的低,开发人员必须等待测试人员的反馈后才知道结果,如果错了,还要修改bug , 这个过程一方面需要沟通成本,另外一方面效率是非常低的.持续部署:我们都知道,项目最终是会部署到服务器上去,在没用Jenkins之前,大都是我们或专业的运维将项目进行部署。如果项目非常多或者部署完后出现bug,需要人手动的一个个部署或者能力强些的大佬,就是用脚本文件部署,但是看起来还是非常麻烦.2、关于 JenkinsJenkins 是一个用 Java 编写的开源自动化工具,带有用于持续集成的插件。Jenkins 用于持续构建和测试您的软件项目,从而使开发人员更容易将更改集成到项目中,并使用户更容易获得新的构建。它还允许您通过与大量测试和部署技术集成来持续交付软件。Jenkins 集成了各种开发生命周期过程,包括构建、文档、测试、打包、模拟、部署、静态分析等等。Jenkins 借助插件实现了持续集成。插件允许集成各种 DevOps 阶段。如果要集成特定工具,则需要安装该工具的插件。例如 Git、Maven、Node 项目等。3、Jenkins 的工作流程我画了一张简易的Jenkins 的工作流程图,希望能带给你一些帮助。(图片说明:Jenkins 一项配置简单的工作流程图)流程说明:开发者在本地开发,然后提交到 Source Respository 中,触发GitHub或者 GitLab 配置的钩子函数程序,继而通知 JenkinsJenkins 收到通知,会通过 Git/SVN 插件,重新从项目配置中的代码仓库中拉取最新代码,放置于 Workspace (Jenkins 存放任务的目录)之后重新触发构建任务,Jenkins 有很多的构建的插件,Java常用的 Maven 、Gradle,前端的 Node 等如果有安装发送邮件的插件并且进行了配置,那么可以在项目中进行配置,构建失败或者成功都可以选择是否给开发者发送邮件构建成功后,Jenkins 会通过一个 SSH 插件,来远程执行 Shell 命令,发布项目,在项目中可以配置多台服务器,也就可以一次性部署到多台服务器上去。补充:当然很多时候,构建成功后,并不会直接部署到服务器上,而是打包到另外一台服务器上存储(应用服务器)或者存储为软件仓库中的一个新版本。原因是一方面为了更好的回退版本,出现错误可以及时恢复,因为一个大型项目,它的构建过程时间说不上短;另外一方面也是为了更好的扩展,如果出现紧急情况,需要横向扩展,可以在备用机器上,直接进行拉取部署即可。一个简易的自动部署化的过程,大致是如此的。但其实中间还有不少东西的,例如代码审查和 Jenkins 自动化测试等等,对一门技术了解的越多,不知道的也就越多了。二、Docker 安装 JenkinsJenkins 其实支持各个系统安装,Windows 、Liunx 、Mac 都可以的。选择 Docker 是方便哈,因为我其他的环境都是用 Docker 搭建的~~ 所以我这里介绍的也是 Docker 安装 Jenkins,后续的文章也都是基于此。我目前的环境:Jenkins 2.346.2、阿里云服务器centos7、Docker version 20.10.72.1、搜索Jenkins 镜像docker search jenkinsdeprecated 是弃用的意思,第一条搜索记录就是告诉我们 jenkins 镜像已经弃用,让我们使用 jenkins/jenkins:lts 镜像名进行拉取。2.2、拉取 Jenkins 镜像docker pull jenkins/jenkins:lts docker images #查看镜像既然是学习,就得上手最新的啦,错了再降。2.3、启动Jenkins 容器在宿主机创建挂载目录mkdir -p /home/jenkins/workspace启动 Jenkins 容器docker run -uroot -d --restart=always -p 9001:8080 \ -v /home/jenkins/workspace/:/var/jenkins_home/workspace \ -v /var/run/docker.sock:/var/run/docker.sock \ --name jenkins jenkins/jenkins:lts2.4、使用 Jenkins这个时候就可以直接访问了。会看到这样的一个界面,我们就要进入容器,去拿到这个密码。docker exec -it -uroot jenkins bash # -uroot 是以管理员身份登入容器然后复制粘贴上去后,会看到这样的一个界面。如果和我一样是个小白的话,直接安装推荐的插件吧,稍微省事点,不然很多插件都需要自己一个个的查。耐心等待它下载完吧大家根据自己的需求进行操作,后续也是根据自己的想法一直点击下去就好了,反正咱们还在学习,无妨的。主界面一些简单页面,大家点进去都能看的明白,我就不再多嘴了。着重说一下 Manger Jenkins 界面一些东西2.5、配置 Jenkins 密钥其实在很多时候你可以把 Jenkins 容器看成一台独立的服务器,因为你运行项目的那些环境,都可以安装在它的内部。还是先说说配置密钥,配置密钥主要作用就是为了去 Github、Gitee、GitLab上拉取代码,这点相信大家都是能够理解的吧。生成密钥:之前我们已经进入Jenkins的终端,直接输入下面的命令就好了。ssh-keygen -t rsa -C "root" #输入完一直回车就结束了 cat /root/.ssh/id_rsa.pub #查看公钥如果没有的话,就先输入 下面命令进入 Jenkins 容器终端docker exec -it -uroot jenkins bash # jenkins 是我启动的容器名 换成容器id 也可以的拿到 Jenkins 公钥后,就放到 Github、Gitee或者是 GitLab 上去,我放的 Github,如下:这样就算是添加完成了。到这一步,可以进行测试一下,是否已经可以从Github上拉取项目三、 Jenkins 插件安装、添加凭据、系统配置、全局工具配置实际上 Jenkins 的功能基本上都是依靠插件来完成,所以不同项目也会要安装不同的插件。3.1、插件安装我演示的是一个 SpringBoot 后端项目的部署,中间也没有穿插复杂的操作,所以装的插件也不多哈。安装的插件的名称:Maven Integration :Maven 项目打包工具SSH : SSH 连接工具Publish Over SSH :SSH 发布工具如果要运行前端 Vue 项目,记得下载一个 NodeJS 的插件(我会的前端就只有Vue哈)等待下载完即可3.2、添加凭据凭据其实就是账号密码,你访问Github、远程服务器都需要账号密码才行,这里的凭据就是相应的账号密码。添加 github 的账号密码添加服务器的登录账号和密码补充:这些都是可以填加多个的。最后就是这样的:稍后在项目中都是需要用到的。3.3、系统配置找到两个配置:1、SSH remote hosts2、SSH Servers对了记得点击保存哈,不然又得重现填写。3.4、全局工具配置我上文有提到很多时候我们可以把 Jenkins 看成一台单独的电脑,尤其是在工具设置的时候。在这里其实就是配置一些项目中需要用到的环境,如JDK、Maven、NodeJS等等1、Maven 配置这里可以用默认的,也可以用宿主机文件系统中的。我这里用的是默认的,因为我Liunx服务器上的环境全部都是基于 Docker 搭建的。选择默认的话,就需要在 Jenkins 中新增一个,然后Jenkins在你构建项目的时候,如果是选择默认的话,没有的Maven情况下,它会主动给你下载一个Maven2、JDK 配置JDK 也是可以选择自动安装和使用宿主机原有的 JDK 两种方案。点击 Please enter your username/password蓝色小字后,会跳转至下面的界面(图片说明:在这里输入 Oracle 官网的账号密码即可)没有账号需要去注册 Oracle 账号补充:如果这里选择 Oracle 官网下载JDK,最高支持JDK 版本为 9,如果要选择更高的稳定的 JDK 版本,一个是使用宿主机的 JDK,另外就是使用压缩包方式,然后解压等。只要想用,互联网应该还是能够满足的。关于 Git ,我们直接用 Jenkins 默认的即可,在安装 Jenkins 推荐的插件的时候,其中就有Git,当然Git,这里Jenkins也允许你使用 宿主机的Git。最后记得点击保存四、Jenkins 部署 Maven 项目相关要求:本地需要Java、Git 环境需要有一个 Github/Gitee 账号不管在那里安装的 Jenkins 都要确保它能够访问网络4.1、本地创建一个Maven项目创建一个SpringBoot 项目,controller,自己看着写就好了pom.xml<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.5.1</version> </dependency> </dependencies> <!-- 这个插件,可以将应用打包成一个可执行的jar包 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>Dockerfile文件FROM adoptopenjdk/openjdk8 WORKDIR app #切换到镜像中的指定路径,设置工作目录 COPY target/*.jar app.jar #会将宿主机的target/*.jar文件复制到 镜像的工作目录 /app/ 下 CMD ["java", "-jar", "app.jar"] #执行java -jar 命令因为不是本文关注的重点,更多详情可能还需朋友们去查询。4.2、推送到远程仓库 github 上首先在 Github 上创建一个仓库然后点击 idea 中下方菜单栏 Terminal 命令行终端中输入git init git add . #这里 . 默认提交所有修改文件 git commit -m "init" git branch -m main # 因为gitgub 仓库默认主分支为main ,而我们本地初始化时 主分支为 master git remote add 远程仓库地址 # 添加远程仓库 git push origin main # push 上去然后刷新 github 界面,就可以看到已经push成功了4.3、Jenkins 项目配置新建任务选择构建一个 Maven 项目项目配置源码管理构建环境没啥说的Build 那不用改啥,用默认的就可以。然后来到 Post Steps选择这个 send files or execute commands over SSH 就是通过SSH发送文件或执行命令我们要将构建好的 jar/war 发送至相应的服务器,然后执行相关的命令,进行部署。(图片说明:那个映射的端口应为8080,我写漏了)#/bin/bash # 注意 其实在这里输入的命令,就是在服务器上的命令,我们所处于的位置就是当前登录用户的根目录下 echo ">>>>>>>>>>>>>cd 到宿主机映射 Jenkins 的项目路径下>>>>>>>>>>>>>" cd /home/jenkins/workspace/hello-springboot echo ">>>>>>>>>>>>>停止容器>>>>>>>>>>>>>" docker stop hellospringboot echo ">>>>>>>>>>>>>删除容器>>>>>>>?>>>2>22" docker rm hellospringboot echo ">>>>>>>>>>>>>删除镜像>>>>>>>>>>>> >" docker rmi nzc/hellospringboot:1.0 echo ">>>>>>>>>>>>>制作镜像>>>>>>>>>>>>>" docker build -f Dockerfile -t nzc/hellospringboot:1.0 . echo ">>>>>>>>>>>>>启动容器>>>>>>>>>>>>>" docker run -p 8080:8080 --name hellospringboot -d nzc/hellospringboot:1.0 echo ">>>>>>>>>>>>自动部署结束>>>>>>>>>>>>>"记得点击保存4.4、部署和测试然后点击立即构建就好了,但因为是第一次构建,要从 github 拉取代码,下载 jdk、maven等,还有相应 jar 包等,所以时间会相对久一些。点击这个,可以看到控制台输出控制台输出,日志比较多,就挑了一点末尾是Finished: SUCCESS 就证明是构建成功啦。我们来看看Jenkins 的工作空间这里已经是存在项目啦。我们再去看看 Docker 镜像有没有构建成功再去看一眼 有没有在运行最后就是看看能不能访问到了已经是可以访问到了。当然现在的话,还没有做到自动化部署,就是我提交完,jenkins 就能自己知道,然后进行构建,我们现在要做的就是把手动构建修改成 github 更新或合并就构建。五、GitHub提交代码时触发 Jenkins 自动构建其实主要就三步,因为前面我们已经搭建好了,所以就只要修改一下即可。5.1、GitHub 上配置 Jenkins 的 webhook像我jenkins是部署在服务器上的 我的 地址就是服务器 IP:port/github-webhook/5.2、GitHub上创建一个access tokenJenkins做一些需要权限的操作的时候就用这个access token去鉴权就是命名,然后勾选你需要的权限就可以了最后完成的时候,记得复制5.3、修改 Jenkins 配置首先先要修改一下系统配置 Configure System点添加的时候,会弹出一个框描述自己写就行~~第一步完成,记得点击保存,接下来去修改一下项目配置找到项目,点击配置把构建触发器中的 GitHub hook trigger for GITScm polling 勾上记得点击保存。然后就可以进行测试啦。5.4、Push 测试这是我本地还没更新的代码,现在push上去哈。push 成功后,刷新一下就可以看到构建任务正在执行了。在构建日志中也可以看到 最新 的提交记录六、总结文章的脉络大致:首先是介绍了 Jenkins,以及 Jenkins 的简单工作流程;而后是教大家如何用 Docker 安装 Jenkins ;然后再是对 Jenkins 进行一些插件安装和配置;再以 Jenkins 部署 Maven 项目为案例,讲解如何使用 Jenkins;最后是对上一步操作的改早,Jenkins + Github + Docker 实现自动化部署。如果可以的话,我觉得还是实操一遍比较好~后记写到这里,这篇文章也算是结束了,我尽可能的将里面牵扯到的知识点都写的通俗易懂,但是阅读起来究竟是如何的,我也还不知晓,希望能够得到你们的反馈和支持。自我感觉这篇文章的质量以及实用程度,应该算是初级文章中,比较合格了的吧。其实在写文的过程中,能够自我反省,很多时候,一些东西用了就忘了,但是当静下心来将知识慢慢输出时,能够发现诸多奥妙,以及当时未注意到的知识点,挺有收获的,同时希望你也是如此。
在上一篇中,我留下了几个疑问,我们使用lombok的注解时,为什么加了个注解就可以帮我们自动生成代码呢?是谁给我们做了这件事情呢?它的原理是什么样的呢?本篇就是以我们最常用的 lombok作为主线来引出 javac 注解处理器,Lombok 插件注解功能很多,出了有自动 set、get 方法外,还有链式调用、建造者模式等等,但是我们就讨论最简单的 set、get 方法的生成。一、用Lombok引出问题1.1、引入1、idea 中打开 settings (快捷键:ctrl+alt+s) ,搜索 plugin ,在 plugins 里面搜索 lombok ,安装2、在项目中引入 lombok 的依赖<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> </dependency>1.2、优缺Lombok 是一个 Java 库,能自动插入编辑器并构建工具,简化 Java 开发。通过添加注解的方式,不需要为类编写 getter或 eques 方法,同时可以自动化日志变量。官网链接优点:简化 Java 开发,减少了许多重复代码。缺点:降低了源码的可读性和完整性;有可能会破坏封装性,因为有些属性并不需要向外暴露;降低了可调试性;Lombok 会帮我们自动生成很多代码,但这些代码是在编译期生成的,因此在开发和调试阶段这些代码可能是“丢失的”,这就给调试代码带来了很大的不便。如果不考虑的那么严谨,我觉得还是要用的,因为我懒。1.3、使用写一个类来分析一下:我们自己手写的一个JavaBean/** * @description: * @author: Yihui Wang * @date: 2022年07月06日 20:23 */ public class Student { private String name; private String age; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAge() { return age; } public void setAge(String age) { this.age = age; } }用了 lombok 注解的 JavaBean/** * @description: * @author: Yihui Wang * @date: 2022年07月06日 20:23 */ @Data public class StudentLombok { private String name; private String age; }我们编译一下,Idea 中点击顶部菜单 Build ,下拉选择 Recompile 看看他们生成的 class文件是什么样的。可以明显看出,使用了 @Setter、@Getter 注解后,和我们手动编写的 Java 代码,编译完的结果是一样的。它直接帮我们生成了这些方法,这些步骤究竟是谁做的勒?我们是否也可以自己编写这样的注解呢?二、Lombok 原理分析其实这里面用到了 AOP 编程的编译时织入技术,就是在编译的时候修改最终 class 文件。大部分的程序代码从开始编译到最终转化成物理机的目标代码或虚拟机能执行的指令集之前,都会按照如下图所示的各个步骤进行:Javac 的编译过程归纳起来主要是由以下三个过程组成:分析和输入到符号表注解处理语义分析和生成 class 文件而Lombok 正是利用注解处理这一步来进行实现的。Lombok 使用的是 JDK 6 实现的 JSR 269: Pluggable Annotation Processing API (编译期的注解处理器) ,它允许在编译期处理注解,读取、修改、添加抽象语法树中的内容。其实说到这里,我们还只是知道它是在这一步处理的,但如何处理的,我们还是一无所知。稍后我们会手动实现 Lombok 中的 @Getter、@Setter 注解,这里先事先说明可能会牵扯到的知识。主要使用到的都是 jdk 源码的 tools.ja 包使用的 api 主要是com.sun.tools.javac包下的抽象语法 JCTree 使用不懂也没关系,我也不是很懂,哈哈,我也只是因为好奇,才来探寻的其中最主要的就是牵扯到的AbstractProcessor抽象注解处理类,还有就是 JCTree 相关的api,这些的话,我也用的不多,不敢胡乱发言。要实现注解处理器首先要做的就是继承抽象类 javax.annotation.processing.AbstractProcessor,然后重写它的 process() 方法,process() 方法是 javac 编译器在执行注解处理器代码时要执行的过程。/** 一个抽象注释处理器,旨在成为大多数具体注释处理器的方便超类。 */ public abstract class AbstractProcessor implements Processor { /** * Processing environment providing by the tool framework. */ protected ProcessingEnvironment processingEnv; private boolean initialized = false; /** 如果处理器类使用SupportedOptions进行注释,则返回一个不可修改的集合,该集合与注释的字符串集相同。 如果类没有这样注释,则返回一个空集 */ public Set<String> getSupportedOptions() { SupportedOptions so = this.getClass().getAnnotation(SupportedOptions.class); if (so == null) return Collections.emptySet(); else return arrayToSet(so.value()); } /** 如果处理器类使用SupportedAnnotationTypes进行注释,则返回一个不可修改的集合, 该集合具有与注释相同的字符串集。如果类没有这样注释,则返回一个空集。 return: 此处理器支持的注释类型的名称,如果没有则为空集 */ public Set<String> getSupportedAnnotationTypes() { SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class); if (sat == null) { if (isInitialized()) processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "No SupportedAnnotationTypes annotation " + "found on " + this.getClass().getName() + ", returning an empty set."); return Collections.emptySet(); } else return arrayToSet(sat.value()); } /** 如果处理器类使用SupportedSourceVersion进行注解,则在注解中返回源版本。 如果类没有这样注释,则返回SourceVersion.RELEASE_6 */ public SourceVersion getSupportedSourceVersion() { SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class); SourceVersion sv = null; if (ssv == null) { sv = SourceVersion.RELEASE_6; if (isInitialized()) processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "No SupportedSourceVersion annotation " + "found on " + this.getClass().getName() + ", returning " + sv + "."); } else sv = ssv.value(); return sv; } /** 该方法有两个参数,“annotations” 表示此处理器所要处理的注解集合; “roundEnv” 表示当前这个 Round 中的语法树节点, 每个语法树节点都表示一个 Element(javax.lang.model.element.ElementKind 可以查看到相关 Element)。 */ public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv); }另外还有这两个用来配合的 注解:@SupportedAnnotationTypes("*") @SupportedSourceVersion(SourceVersion.RELEASE_8)@SupportedAnnotationTypes 表示注解处理器对哪些注解感兴趣,“*” 表示对所有的注解都感兴趣;@SupportedSourceVersion 指出这个注解处理器可以处理最高哪个版本的 Java 代码。三、简易版 Lombok 实现简要说明先说说我们要实现的东西,为了简单的去理解,我这里只讨论get、set 方法,其实里面的实现都差不多,如果偏要说不同的话,就是调用的javac的api不同吧。写了两个注解 :@MyGetter 和 @MySetter 和他们的处理器 MyAnnotationProcessor注解处理器,顾名思义就是用来处理注解的啦。项目结构:由于是maven项目,这里面引用了com.sun.tools的东西,所以,需要在maven的pom文件里面加上,这样,在使用maven打包的时候,才不会报错。<dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.8</version> <scope>system</scope> <systemPath>jdk路径/lib/tools.jar</systemPath> </dependency>我们这里利用 Java SPI 加载自定义注解器的方式,生成一个 jar 包,类似于 Lombok ,这样之后其它应用一旦引用了这个 jar 包,自定义注解器就能自动生效了。SPI是java提供的一种服务发现的标准,具体请看SPI介绍,但每次我们都需要自己创建services目录,以及配置文件,google的autoservice就可以帮我们省去这一步。<dependency> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.0-rc5</version> </dependency>如果你使用Processor(javax.annotation.processing.Processor),并且你的元数据文件被包含在了一个jar包中,同时这个jar包是在javac(java编译)的classpath路径下时,javac会自动的执行通过该方式注入进去的Processor的实现类,以实现对于该项目内的相关数据的扩展。使用 AutoService 会自动的生成META-INF./services/javax.annotation.processing.Processor 文件,并且文件中内容就是我们动态注入进去的类。然后在编译的时候就会执行对应的扩展方法,同时写入文件。项目代码代码都很简单,所以除了注解处理器,其他的都没有带啥注释啦哈。@Target({ElementType.TYPE}) @Retention(RetentionPolicy.SOURCE) public @interface MyGetter { }@Target({ElementType.TYPE}) @Retention(RetentionPolicy.SOURCE) public @interface MySetter { }package com.nzc.my_annotation; import com.google.auto.service.AutoService; import com.sun.source.tree.Tree; import com.sun.tools.javac.api.JavacTrees; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.tree.TreeTranslator; import com.sun.tools.javac.util.*; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import java.util.Set; /** * @author nzc */ @SupportedAnnotationTypes({"com.nzc.my_annotation.MyGetter","com.nzc.my_annotation.MySetter"}) @SupportedSourceVersion(SourceVersion.RELEASE_8) @AutoService(Processor.class) public class MyAnnotationProcessor extends AbstractProcessor { private JavacTrees javacTrees; // 提供了待处理的抽象语法树 private TreeMaker treeMaker; // 封装了创建AST节点的一些方法 private Names names; // 提供了创建标识符的方法 /** * 从Context中初始化JavacTrees,TreeMaker,Names */ @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); Context context = ((JavacProcessingEnvironment) processingEnv).getContext(); javacTrees = JavacTrees.instance(processingEnv); treeMaker = TreeMaker.instance(context); names = Names.instance(context); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // 返回使用给定注释类型注释的元素的集合。 Set<? extends Element> get = roundEnv.getElementsAnnotatedWith(MyGetter.class); for (Element element : get) { // 获取当前类的抽象语法树 JCTree tree = javacTrees.getTree(element); // 获取抽象语法树的所有节点 // Visitor 抽象内部类,内部定义了访问各种语法节点的方法 tree.accept(new TreeTranslator() { @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) { // 在抽象树中找出所有的变量 // 过滤,只处理变量类型 jcClassDecl.defs.stream() .filter(it -> it.getKind().equals(Tree.Kind.VARIABLE)) // 类型强转 .map(it -> (JCTree.JCVariableDecl) it) .forEach(it -> { // 对于变量进行生成方法的操作 jcClassDecl.defs = jcClassDecl.defs.prepend(genGetterMethod(it)); }); super.visitClassDef(jcClassDecl); } }); } Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(MySetter.class); for (Element element : set) { JCTree tree = javacTrees.getTree(element); tree.accept(new TreeTranslator() { @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) { jcClassDecl.defs.stream() .filter(it -> it.getKind().equals(Tree.Kind.VARIABLE)) .map(it -> (JCTree.JCVariableDecl) it) .forEach(it -> { jcClassDecl.defs = jcClassDecl.defs.prepend(genSetterMethod(it)); }); super.visitClassDef(jcClassDecl); } }); } return true; } private JCTree.JCMethodDecl genGetterMethod(JCTree.JCVariableDecl jcVariableDecl) { // 生成return语句,return this.xxx JCTree.JCReturn returnStatement = treeMaker.Return( treeMaker.Select( treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName() ) ); ListBuffer<JCTree.JCStatement> statements = new ListBuffer<JCTree.JCStatement>().append(returnStatement); // public 方法访问级别修饰 JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC); // 方法名 getXXX ,根据字段名生成首字母大写的get方法 Name getMethodName = createGetMethodName(jcVariableDecl.getName()); // 返回值类型,get类型的返回值类型与字段类型一致 JCTree.JCExpression returnMethodType = jcVariableDecl.vartype; // 生成方法体 JCTree.JCBlock body = treeMaker.Block(0, statements.toList()); // 泛型参数列表 List<JCTree.JCTypeParameter> methodGenericParamList = List.nil(); // 参数值列表 List<JCTree.JCVariableDecl> parameterList = List.nil(); // 异常抛出列表 List<JCTree.JCExpression> throwCauseList = List.nil(); // 生成方法定义树节点 return treeMaker.MethodDef( // 方法访问级别修饰符 modifiers, // get 方法名 getMethodName, // 返回值类型 returnMethodType, // 泛型参数列表 methodGenericParamList, //参数值列表 parameterList, // 异常抛出列表 throwCauseList, // 方法默认体 body, // 默认值 null ); } private JCTree.JCMethodDecl genSetterMethod(JCTree.JCVariableDecl jcVariableDecl) { // this.xxx=xxx JCTree.JCExpressionStatement statement = treeMaker.Exec( treeMaker.Assign( treeMaker.Select( treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName() ), treeMaker.Ident(jcVariableDecl.getName()) ) ); ListBuffer<JCTree.JCStatement> statements = new ListBuffer<JCTree.JCStatement>().append(statement); // set方法参数 JCTree.JCVariableDecl param = treeMaker.VarDef( // 访问修饰符 treeMaker.Modifiers(Flags.PARAMETER, List.nil()), // 变量名 jcVariableDecl.name, //变量类型 jcVariableDecl.vartype, // 变量初始值 null ); // 方法访问修饰符 public JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC); // 方法名(setXxx),根据字段名生成首选字母大写的set方法 Name setMethodName = createSetMethodName(jcVariableDecl.getName()); // 返回值类型void JCTree.JCExpression returnMethodType = treeMaker.Type(new Type.JCVoidType()); // 生成方法体 JCTree.JCBlock body = treeMaker.Block(0, statements.toList()); // 泛型参数列表 List<JCTree.JCTypeParameter> methodGenericParamList = List.nil(); // 参数值列表 List<JCTree.JCVariableDecl> parameterList = List.of(param); // 异常抛出列表 List<JCTree.JCExpression> throwCauseList = List.nil(); // 生成方法定义语法树节点 return treeMaker.MethodDef( // 方法级别访问修饰符 modifiers, // set 方法名 setMethodName, // 返回值类型 returnMethodType, // 泛型参数列表 methodGenericParamList, // 参数值列表 parameterList, // 异常抛出列表 throwCauseList, // 方法体 body, // 默认值 null ); } private Name createGetMethodName(Name variableName) { String fieldName = variableName.toString(); return names.fromString("get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1)); } private Name createSetMethodName(Name variableName) { String fieldName = variableName.toString(); return names.fromString("set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1)); } }@MyGetter @MySetter public class School { private String name; private String address; }测试结果:编译:编译的话,直接编译根项目就好,我原本想着先编译子项目,再编译另外一个项目,但是会报错,不想纠结了,放出来的,是经过测试的,可以正常编译出来的。四、思考相信大家都有使用Lombok的过程,但是不知道大家有没有注意到我上面的demo是存在一些问题的呢?我们之前分析了 lombok 它是在编译时为我们添加了诸如 set、get 方法的,但实际上我们在开发的时候就已经可以调用对象上的 set、get 方法啦,这又是如何实现的呢?我个人的测试及思考这里一定是会牵扯到 idea 中的 lombok 插件的问题,如果你只是引入了 lombok 的依赖,没有安装 lombok 插件的话,那么在 idea 是不会有方法提示的。我新建了一个空白项目进行测试,如果你引入依赖,没安装插件,idea 只会爆红,但是是可以通过编译的,也不会报错。写一个main 方法也是可以运行的,只不过安装了插件才会提供提示。按照这个思路,我又返回去测试了上面的那个 Demo,答案是失败的。测试如下:创建了 Demo 类,里面写了 main 方法创建了 School 对象,调用了 set、get 方法使用 maven 编译,成功过,也失败过(问我也是白问,我都测麻了)这个可能跟我的机器环境有关,理论上应该是可以成功的,后来新建的一个项目又是可以的。大家也可以去玩一玩。启动 main 方法,是直接报错,起不来,原因不知道,如果我还有时间,我再去找找。java: java.lang.ClassCastException: class com.sun.proxy.$Proxy26 cannot be cast to class com.sun.tools.javac.processing.JavacProcessingEnvironment (com.sun.proxy.$Proxy26 is in unnamed module of loader java.net.URLClassLoader @4fccd51b; com.sun.tools.javac.processing.JavacProcessingEnvironment is in module jdk.compiler of loader 'app')lombok 的插件在其中肯定是做了一些事情的,但是我在各大搜索引擎上搜索这方面的知识,也没有找到相关的一些资料,倒是看到有几个小伙伴问出了和我相似的问题。如下:大家使用过 lombok 的 @Slf4j 注解吧,为什么有了这个注解,我们就可以直接在类里面使用 log 对象,这个对象又是在哪里创建出来的呢?说到这,其实我还是没说出什么道理,因为我也不明白,所以最后这一小节,我的命名才是直接明了的为思考。如果有明白的大佬,请不啬赐教,非常感谢!此外的补充::其实自定义注解处理器,给我的感觉就像 SpringMVC 中拦截器一样,SpringMVC是拦截请求,自定义注解是拦截在编译前,而且给我的感觉的话,自定义注解编译器应该更好玩,并竟可以改 class 文件,感觉之后还有空的话,会继续整一整这个注解处理器。参考如果想要了解 JCTree和编译相关的信息的话,可以看下面的这篇文章,写的真的非常详细。我不是推销啥啥的,只是单纯觉得作者写的非常优秀,值得看。我帮大家确认过,无广告,无推销,阅读体验很好。Java-JSR-269-插入式注解处理器Lombok经常用,但是你知道它的原理是什么吗?Javac 编译过程JVM系列六(自定义插入式注解器). 代码参考:《深入理解JVM字节码》
关于我为啥突然想要深入的了解 Java 注解和反射好奇心来啦打算看源码巩固 Java 基础知识(基础不牢,地动山摇)不要说我内卷,每个人有每个人选择的路,坚持初心。一、逻辑思维图🧐第 1-5 小节均偏向于理论知识,若只是想要了解如何自定义注解和如何应用注解,请跳转至第 6 小节开始阅读。在本篇中,主要是针对注解的概念及运行时注解进行解释说明,附带有三个实战的案例,尽可能的让大家能够理解透彻并且能够加以应用。二、什么是注解👨🏫Java 注解(Annotation)用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。--官方文档2.1、注解Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。我们常常使用的注解,@Data、@Controller等等,这些都是注解,创建一个注解,也很简单,创建一个类,然后将class改为 @interface就是一个注解啦。2.2、注解出现的位置Java代码中的包、类型、构造方法、方法、成员变量、参数、本地变量的声明都可以用注解来修饰。注解本质上可以看作是一种特殊的标记,程序在编译或者运行时可以检测到这些标记而进行一些特殊的处理。2.3、关于注解的处理我们一般将利用反射来处理注解的方式称之为运行时注解。另外一种则是编译时注解,如我们常常使用的 lombok 里的注解,@Data,它能够帮我们省略set/get方法,我们在Class上加上这个注解后,在编译的时候,lombok其实是修改了.class文件的,将set/get方法放进去了,不然的话,你可以看看编译完后的.class文件。诸如这种,我们常称为编译时注解,也就是使用javac处理注解。--图:来自于极客学院这幅图就是从.java文件到class文件的,再到class文件被 JVM 加载的过程。而其中的注解抽象语法树这一阶段,就是去解析注解,然后根据定义的注解处理器进行相关的逻辑处理。这一块不是我的关注点,略过略过啦,朋友们,好奇可以去研究研究噢3、注解的目的或作用💞生成文档。这是最常见的,也是 Java 最早提供的注解。如@param、@return等等**跟踪代码依赖性,实现替代配置文件功能。**作用就是减少配置,如 Spring中Bean的装载注入,而且现在的框架基本上都是使用注解来减少配置文件的数量,同时这样也使得编程更加简洁,代码更加清晰。在编译时进行格式检查。如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;标识作用。当Java编译时或运行时,检测到这里的注解,做什么的处理,自定义注解一般如此。携带信息。 注解的成员提供了程序元素的关联信息,Annotation 的成员在 Annotation类型中以无参数的方法的形式被声明。其方法名和返回值定义了该成员的名字和类型。在此有一个特定的默认 语法:允许声明任何Annotation成员的默认值。一个Annotation可以将name=value对作为没有定义默认值的Annotation 成员的值,当然也可以使用name=value对来覆盖其它成员默认值。这一点有些近似类的继承特性,父类的构造函数可以作为子类的默认构造函数,但是也 可以被子类覆盖。这么一大段话,其实就是关于注解中成员的解释。说了这么多,其实一句话也能表达完。注解就是一张便利贴,它贴在那里,你看到的那一刻,就明白该做什么事啦。如出门前,门上贴着一张便利贴📌,上面写着"出门记得带钥匙",当你看到的那一刻,你就会去检查一下自己是否带钥匙啦。在 Java 中也是一样的,你定义了一个注解,注解上可以写一些东西,然后你再将它贴在某个上面,说明白执行规则,当编译到这里的时候需要干嘛干嘛,又或者是当运行到这里的时候需要干嘛干嘛。因为注解写的东西的不同,或者是处理注解的规则不同,而产生了不同的注解及作用。4、JDK 内置注解💫Java 中 内置的注解有 5 类,具体包括:@Deprecated:过时注解,用于标记已过时 & 被抛弃的元素(类、方法等)@Override:复写注解,用于标记该方法需要被子类复写@SuppressWarnings:阻止警告注解,用于标记的元素会阻止编译器发出警告提醒@SafeVarargs:参数安全类型注解,用于提醒开发者不要用参数做不安全的操作 & 阻止编译器产生 unchecked 警告,Java 1.7 后引入5、元注解 🎯何为元注解?就是注解的注解,就是给你自己定义的注解添加注解,你自己定义了一个注解,但你想要你的注解有什么样的功能,此时就需要用元注解对你的注解进行说明了。接着上一个比喻注解有很多很多吗,门上贴一个,冰箱上贴一个,书桌上贴一个等等元注解勒就是把他们整合起来称呼的,像上面这些可以统称为生活类注解啊。所以也就是注解的注解。5.1、@Target在 @Target 注解中指定的每一个 ElementType 就是一个约束,它告诉编译器,这 个自定义的注解只能用于指定的类型。说明了注解所修饰的对象范围:注解可被用于 packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数)。5.2、@Retention定义了该注解的生命周期:某些注解仅出现在源代码中,而被编译器丢弃; (源码级)而另一些却被编译在 class 文件中; (字节码级)编译在 class 文件中的注解可能会被虚拟机忽略,而另一些在 class 被装载时将被读取(请注意并不影响 class 的执行,因为注解与 class 在使用上是被分离的)。绝大多数开发者都是使用 RUNTIME,因为我们期望在程序运行时,能够获取到这些注解,并干点有意思的事儿,而只有 RetentionPolicy.RUNTIME,能确保自定义的注解在运行时依然可见。(运行级)使用这个元注解可以对自定义注解的“生命周期”进行限制。RetentionPolicy.SOURCE 一般开发者很少用到,大都是 Java 内置的注解。如@Override @Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {}复制代码@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})@Retention(RetentionPolicy.SOURCE)public @interface SuppressWarnings {复制代码这些注解只是在编译的时候用到,一旦编译完成后,运行时没有任何意义,所以他们被称作源码级别注解。如果有了解过 lombok 一些简单原理的开发者, 都知道它是通过注解在编译时自动生成一部分代码,让源码看起来更简洁,字节码却很强大。当然,这种方式有它自身的缺陷,譬如不一致性,问题排解时的困扰,以及依赖问题,不是本篇重点,扯回来。提供信息给编译器: 编译器可以利用注解来检测出错误或者警告信息,打印出日志。编译阶段时的处理: 软件工具可以用来利用注解信息来自动生成代码、文档或者做其它相应的自动处理。运行时处理: 某些注解可以在程序运行的时候接受代码的提取,自动做相应的操作。5.3、@Documented用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc此类的工具文档化。是一个标记注解,没有成员。5.4、@Inherited是一个标记注解阐述了某个被标注的类型是被继承的。使用了@Inherited修饰的注解类型被用于一个 class 时该 class 的子类也有了该注解。5.5、@Repeatable允许一个注解可以被使用一次或者多次(Java 8)。6、自定义注解📸自定义注解实际上就是一种类型而已,也就是引用类型(Java 中除了 8 种基本类型之外,我们见到的任何类型都是引用类型)6.1、定义注解自定义注解过程:声明一个类 MyAnnotation把 class 关键字改为 @interface这样我们就声明了一个自定义的注解,当我们用@interface声明一个注解的时候,实际上是声明了一个接口,这个接口自动的继承了java.lang.annotation.Annotation,但是我们只需要@interface这个关键字来声明注解,编译器会自动的完成相关的操作,不需要我们手动的指明继承Annotation接口另外在定义注解时,不能再继承其他的注解或接口。我举了四个例子,这四个注解分别是放在 类(接口、枚举类上)、构造函数、方法级别、成员属性上的。@Documented //定义可以被文档工具文档化@Retention(RetentionPolicy.RUNTIME)//声明周期为runtime,运行时可以通过反射拿到@Target(ElementType.TYPE)//注解修饰范围为类、接口、枚举public @interface ClassAnnotation { public String name() default "defaultService"; public String version() default "1.1.0";}复制代码@Documented@Target(ElementType.CONSTRUCTOR)@Retention(RetentionPolicy.RUNTIME)public @interface ConstructorAnnotatin { String constructorName() default ""; String remark() default "构造器";}复制代码@Documented@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface FieldAnnotation { public String name() default "defaultName"; public String value() default "defaultValue";}复制代码@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MethodAnnotation { public String name() default "defaultName"; public MethodTypeEnum type() default MethodTypeEnum.TYPE1; }复制代码public enum MethodTypeEnum { TYPE1,TYPE2}复制代码6.2、注解的成员变量成员以无参数无异常的方式声明 String constructorName() default "";可以使用 default 为成员指定一个默认值 public String name() default "defaultName";成员类型是受限的,合法的类型包括原始类型以及 String、Class、Annotation、Enumeration (JAVA 的基本数据类型有 8 种:byte(字节)、short(短整型)、int(整数型)、long(长整型)、float(单精度浮点数类型)、double(双精度浮点数类型)、char(字符类型)、boolean(布尔类型) public MethodTypeEnum type() default MethodTypeEnum.TYPE1;注解类可以没有成员,没有成员的注解称为标识注解,例如 JDK 注解中的 @Override、@Deprecation如果注解只有一个成员,并且把成员取名为 value(),则在使用时可以忽略成员名和赋值号“=”例如 JDK 注解的 @SuppviseWarnings ;如果成员名 不为 value,则使用时需指明成员名和赋值号"="6.3、使用注解因为我们在注解中声明了属性,所以在使用注解的时候必须要指明属性值 ,多个属性之间没有顺序,多个属性之间通过逗号分隔@ClassAnnotation(name = "personBean", version = "1.2.1")public class Person { // 告诉大家是可以用的,但是影响我测试,我就又注释掉了.// @ConstructorAnnotatin(constructorName="Person()")// public Person(String description) {// this.description = description;// } @FieldAnnotation(name = "description", value = "This is my personal annotation") private String description; public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @MethodAnnotation(name = "sayHello", type = MethodTypeEnum.TYPE2) public void sayHello() { System.out.println("Hello Annotation!"); }}复制代码6.4、浅提一下反射想要去获取注解就不得不提到反射啦,但 Java 反射会带来一定的耗时,因此使用运行注解需要考虑对性能的影响。我们声明一个Student类用来描述学生对象的信息的class Student{ String name; String school; //...set/get}复制代码当我们创建一个学生对象时,学生对象的信息是保存在 Student 类中,所以 Student 类会提供获取这些信息的方法。在 Java 类中,每个类都会有对应的 Class,要想执行反射操作,必须先要获取指定类名的 Class了解 Class 对象:类是程序的一部分,每个类都有一个 Class 对象。换言之,每当我们编写并且编译 了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用 “类加载器” 子系统把 这个类加载到内存中。Class类:简单说就是用来描述类对象的信息的类对象的信息包括:类的基本信息:包名、修饰符、类名、基类,实现的接口属性的信息:修饰符、属性类型、属性名称、属性值,方法的信息:修饰符、返回类型、方法名称、参数列表、抛出的异常构造方法的信息:修饰符、类名、参数列表、抛出的异常注解的相关信息:因为:类对象的相关信息全部保存在 Class 类所以:Class 类会提供获取这些信息的方法一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象。通过 Class 获取类的相关信息,通过 Class 创建对象,通过 Class 调用对象上面的属性,调用对象上面的方法,这种操作就称为反射,要想执行反射操作,必须先要获取到指定的类名的 Class获取 Class 的不同方式获取基本类型的 Class1)基本类型 Class:如 int.Class 获取的就是 int 类型的 Class获取引用类型的 Class:1)引用类型的 Class:如 String.Class 获取的就是 String 类对应的 Class2)通过对象来获取:如:String obj="hello",Class calz = obj.getClass(),获取的就是 String 类对应的 Class3)Class.forName("java.lang.String"),获取的就是对应的 Class6.5、获取注解这里没有再对 Class 的 api 深度挖掘了,大家感兴趣的话,可以再去了解,跟着这篇文章后面,还有注解下半场和 Java 反射方面的知识,所以这里没有深聊了。public class TestClassAnnotation { private static Person person = new Person(); public static void main(String[] args) { Class<?> clazz = person.getClass(); //因为注解是作用于类上面的,所以可以通过isAnnotationPresent来判断是否是一个具有指定注解的类 if (clazz.isAnnotationPresent(ClassAnnotation.class)) { System.out.println("This is a class with annotation ClassAnnotation!"); //通过getAnnotation可以获取注解对象 ClassAnnotation annotation = clazz.getAnnotation(ClassAnnotation.class); if (null != annotation) { System.out.println("BeanName = " + annotation.name()); System.out.println("BeanVersion = " + annotation.version()); } else { System.out.println("the annotation that we get is null"); } } else { System.out.println("This is not the class that with ClassAnnotation"); } }}复制代码This is a class with annotation ClassAnnotation!BeanName = personBeanBeanVersion = 1.2.1复制代码public class AnnotationTest { public static void main(String[] args) throws ClassNotFoundException { Class<?> clazz = Class.forName("com.nzc.my_annotation.shang.Person"); System.out.println("==============类注解解析=============="); printClassAnno(clazz); System.out.println("==============成员变量注解解析=============="); printFieldAnno(clazz); System.out.println("==============成员方法注解解析=============="); printMethodAnno(clazz); System.out.println("==============构造器注解解析=============="); printConstructorAnno(clazz); } /** * 打印类的注解 */ private static void printClassAnno(Class<?> clazz) throws ClassNotFoundException { //判断是否有AuthorAnnotatin注解 if(clazz.isAnnotationPresent(ClassAnnotation.class)) { //获取AuthorAnnotatin类型的注解 ClassAnnotation annotation = clazz.getAnnotation(ClassAnnotation.class); System.out.println(annotation.name()+"\t"+annotation.version()); } } /** * 打印成员变量的注解 */ private static void printFieldAnno(Class<?> clazz) throws ClassNotFoundException { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if(field.isAnnotationPresent(FieldAnnotation.class)) { FieldAnnotation annotation = field.getAnnotation(FieldAnnotation.class); System.out.println(annotation.name()+"\t"+annotation.value()); } } } /** * 打印成员变量的注解 */ private static void printMethodAnno(Class<?> clazz) throws ClassNotFoundException { Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { if(method.isAnnotationPresent(MethodAnnotation.class)) { MethodAnnotation annotation = method.getAnnotation(MethodAnnotation.class); System.out.println(annotation.name()+"\t"+annotation.type()); } } } /** * 打印成员变量的注解 */ private static void printConstructorAnno(Class<?> clazz) throws ClassNotFoundException { Constructor<?>[] constructors = clazz.getDeclaredConstructors(); for (Constructor<?> constructor : constructors) { if(constructor.isAnnotationPresent(ConstructorAnnotatin.class)) { ConstructorAnnotatin annotation = constructor.getAnnotation(ConstructorAnnotatin.class); System.out.println(annotation.constructorName()+"\t"+annotation.remark()); } } System.out.println("无"); } }复制代码==============类注解解析==============personBean 1.2.1==============成员变量注解解析==============description This is my personal annotation==============成员方法注解解析==============sayHello TYPE2==============构造器注解解析==============无复制代码7、自定义注解实战🐱🏍注解大多时候与反射或者 AOP 切面结合使用,它的作用有很多,比如标记和检查,最重要的一点就是简化代码,降低耦合性,提高执行效率。7.1、自定义注解 + SpringMVC 拦截器实现权限控制功能还有一种应用场景,权限判断或者说是登录校验。这个是我当时还没有学习市面上的权限框架,就是使用了这种自定义注解+拦截器的方式来实现简单的权限控制。注意:此案例不可 CV 直接运行,代码很容易实现,大家理解思路即可。定义注解:@Target({ElementType.METHOD,ElementType.TYPE}) // 这个注解可以放在也可以放在方法上的。@Retention(RetentionPolicy.RUNTIME)public @interface Authority { Role[] roles() ;}复制代码public enum Role { SADMIN, ADMIN, TEACHER, STUDENT}复制代码使用注解:@Authority(roles = {Role.ADMIN, Role.SADMIN}) // 放在类上 说明这个类下所有的方法都需要有这个权限才可以进行访问@RestController@RequestMapping("/admin")public class AdminController { @GetMapping("/hello") public String Hello(){ return "hello 你最近还好吗"; }}复制代码@Controller@RequestMapping("/student")public class StudentController { @Authority(roles = {Role.STUDENT}) // 放在方法上则说明此方法需要注解上的权限才能进行访问 @GetMapping("/test") public String test(){ return "你好,我已经不是一名学生啦"; } }复制代码编写 SpringMVC 拦截器及处理注解的Handler在其中进行 Token 的判断,和访问方法的权限判断,看方法上是否有注解,有的话,就和当前用户对比,成功就可以访问,失败就直接拒绝。当时用的是SSM框架,所以才会看到有 response.sendRedirect(contextPath + "/login");这样的。public class LoginInterceptor extends HandlerInterceptorAdapter { private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String url = request.getRequestURI();// log.info(request.getMethod()+" 请求URL:"+url); //从Token中解析User信息 User user = TokenUtil.verifyToken(request); String contextPath = request.getContextPath(); //user 为空则 表示 Token 不存在 if (user != null) { if (user.getRole().equals("sadmin")) { //检查方法上 是否有注解的 Role.SADMIN 或者 Role.ADMIN 权限 , 没有则检查类上有没有 如果符合要求则放行 if (HandlerUitl.checkAuthority(handler, new Role[]{Role.SADMIN, Role.ADMIN})) { request.setAttribute("user", user); return true; } } if (user.getRole().equals("admin")) { if (HandlerUitl.checkAuthority(handler, new Role[]{Role.ADMIN})) { request.setAttribute("user", user); return true; }else { response.sendRedirect(contextPath + "/login"); } } if (user.getRole().equals("teacher")) { if (HandlerUitl.checkAuthority(handler, new Role[]{Role.TEACHER})) { return true; } else { response.sendRedirect(contextPath + "/login"); } } if (user.getRole().equals("student")) { if (HandlerUitl.checkAuthority(handler, new Role[]{Role.STUDENT})) { return true; } else { response.sendRedirect(contextPath + "/student/login"); } } }else { response.sendRedirect(contextPath + "/login"); } return false; }}复制代码用于检查 方法 或者 类 是否需要权限并和 拥有的权限做对比如果方法上有 ,则以方法的 优先public class HandlerUitl { public static boolean checkAuthority(Object handler, Role[] roles1){ if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; // 获取方法上的注解 Authority authority = handlerMethod.getMethod().getAnnotation(Authority.class); // 如果方法上的注解为空 则获取类的注解 if (authority == null) { authority = handlerMethod.getMethod().getDeclaringClass().getAnnotation(Authority.class); } // 如果标记了注解,则判断权限 if (authority != null) { Role[] roles = authority.roles(); //如果 方法权限为 0 则通过 if(roles.length==0){ return true; } //判断 拥有的权限 是否 符合 方法所需权限 for(int i = 0; i < roles.length; i++){ for(int j = 0; j < roles1.length; j++){ if(roles[i]==roles1[j]){// System.out.println("可以访问"); return true; } } } } return false; } return true; } }复制代码7.2、自定义注解+AOP+Redis 防止重复提交先简单说一下防止重复提交注解的逻辑:在需要防止重复提交的接口的方法,加上注解。发送请求写接口携带 Token请求的路径+ Token 拼接程 key,value 值为生成的 UUID 码然后 set Redis 分布式锁,能获取到就顺利提交(分布式锁默认 5 秒过期),不能获取就是重复提交了,报错。定义注解import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target; @Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface NoRepeatSubmit { /** * 设置请求锁定时间 * @return */ int lockTime() default 5;}复制代码定义处理注解的切面类import com.eshop.api.ApiResult;import com.eshop.common.aop.NoRepeatSubmit;import com.eshop.common.util.RedisLock;import com.eshop.common.util.RequestUtils;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import org.springframework.util.Assert; import javax.servlet.http.HttpServletRequest;import java.util.UUID; /** * 重复提交aop */@Aspect@Component@Slf4jpublic class RepeatSubmitAspect { @Autowired private RedisLock redisLock; @Pointcut("@annotation(noRepeatSubmit)") public void pointCut(NoRepeatSubmit noRepeatSubmit) { } @Around("pointCut(noRepeatSubmit)") public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable { int lockSeconds = noRepeatSubmit.lockTime(); HttpServletRequest request = RequestUtils.getRequest(); Assert.notNull(request, "request can not null"); String bearerToken = request.getHeader("Authorization"); String[] tokens = bearerToken.split(" "); String token = tokens[1]; String path = request.getServletPath(); String key = getKey(token, path); String clientId = getClientId(); boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds); log.info("tryLock key = [{}], clientId = [{}]", key, clientId); if (isSuccess) { log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId); // 获取锁成功 Object result; try { // 执行进程 result = pjp.proceed(); } finally { // 解锁 redisLock.releaseLock(key, clientId); log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId); } return result; } else { // 获取锁失败,认为是重复提交的请求 log.info("tryLock fail, key = [{}]", key); return ApiResult.fail("重复请求,请稍后再试"); } } private String getKey(String token, String path) { return token + path; } private String getClientId() { return UUID.randomUUID().toString(); } }复制代码import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisCallback;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import redis.clients.jedis.Jedis;import redis.clients.jedis.params.SetParams; import java.util.Collections; /** * Redis 分布式锁实现 */@Servicepublic class RedisLock { private static final Long RELEASE_SUCCESS = 1L; private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; // 当前设置 过期时间单位, EX = seconds; PX = milliseconds private static final String SET_WITH_EXPIRE_TIME = "EX"; // if get(key) == value return del(key) private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; @Autowired private StringRedisTemplate redisTemplate; /** * 该加锁方法仅针对单实例 Redis 可实现分布式加锁 * 对于 Redis 集群则无法使用 * * 支持重复,线程安全 * * @param lockKey 加锁键 * @param clientId 加锁客户端唯一标识(采用UUID) * @param seconds 锁过期时间 * @return */ public boolean tryLock(String lockKey, String clientId, long seconds) { return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> { Jedis jedis = (Jedis) redisConnection.getNativeConnection(); SetParams setParams = new SetParams(); String result = jedis.set(lockKey, clientId, setParams.nx().px(seconds)); if (LOCK_SUCCESS.equals(result)) { return true; } return false; }); } /** * 与 tryLock 相对应,用作释放锁 * * @param lockKey * @param clientId * @return */ public boolean releaseLock(String lockKey, String clientId) { return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> { Jedis jedis = (Jedis) redisConnection.getNativeConnection(); Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey), Collections.singletonList(clientId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }); }}复制代码使用注解/** * 添加收藏 */@NoRepeatSubmit@PostMapping("/collect/add")@ApiOperation(value = "添加收藏",notes = "添加收藏")public ApiResult<Boolean> collectAdd(@Validated @RequestBody StoreProductRelationQueryParam param){ // 处理业务逻辑 return ApiResult.ok();}复制代码7.3、自定义注解 + Aop 实现日志收集有关于这个,我之前有写过一篇文章,就不再此处特意贴出来增加篇幅啦。自定义注解 + Aop 实现日志收集8、自言自语💌原本还想找点面试题的,但是到处找了找,面试大部分也就是面试上面这些知识点,所以就删掉啦。本篇主要是针对Java运行时的注解的讲解及应用,但是你想一想,我们使用lombok的注解时,它的实现原理又是什么样的呢?为什么可以帮我们自动生成代码呢?是谁给我们做了这件事情呢?下篇主要是针对上述的几个疑问来展开的,文章的大纲和构思倒是有点想法,但是不知道能不能写好下篇。另外 Java 注解的下半场,主要是围绕着 Lombok 相关来讲的,其中牵扯到的 AbstractProcessor ,其实也算是冷门知识了,随着好奇心继续去挖掘吧。也非常感谢大家的阅读,觉得有所收获的话,可以点点赞,或者留下评论,让我收到你的反馈吧下篇文章见。参考Java元注解 - 生命周期 @RetentionJAVA注解开发(精讲)Java 注解完全解析面试官:什么是 Java 注解?java自定义注解解析及相关场景实现【对线面试官】Java注解
今日的清晨,乌云散去前言上一篇我写了关于Vue中全局事件总线的相关原理及小案例。在之前文章有简单的说过关于我个人理解的 Vue 核心思想(刚学不久,如有不足,请各位大佬及时斧正)数据的双向绑定,不用再手动操作DOM元素组件化开发,将一个页面划分成多个小组件,然后再一步一步拼凑而成组件化开发,最大的痛点可能就是要做到任意间组件通信,组件间通信其本质就是数据的共享。对于组件间的通信,我在之前也是一步一步写过来的组件间利用props实现组件间通信 (适用于父子组件通信,祖孙组件也行,对兄弟组件不太友好)组件间利用自定义事件实现组件间通信 (同上)全局事件总线实现任意组件间通信 (任意间组件都能够通信)案例以及通过第三方库发布/订阅方式实现组件间通信(大家私下了解就好,我个人觉得Vue中事件总线比发布订阅更符合vue的生态,所以没有写这篇文章)正文...为什么引入Vuex呢?思考 🧐不知道大家会不会产生这样的一个疑惑, 全局事件总线 明明已经可以实现任意间组件通信啦,为什么还要额外将 Vuex引入Vue的生态呢?这样的操作不会显得有些重复吗?组件间通信其实就是实现数据的共享和增删改查。在全局事件总线中,通过在vm中beforeCreate生命周期中为 Vue 的原型上添加一个 $bus 属性,在所有组件都可以利用 $on和$emit在$bus属性上绑定方法,通过方法参数可以在不同组件传递数据。这个方法解决了兄弟组件或爷爷孙子组件这种层级比较多的组件间的数据传递。但是这些数据本身存在的地方是在某一个组件的内部,然后其他的组件通过触发回调,来进行数据的修改。也就意味着,如果我们要实现组件通信,就必须在子组件中写一个方法来触发父组件中的事先绑定好的回调函数。如果有更多更多的组件要操作这个数据呢??会怎么样??仔细思考思考🤔,我们修改的是一个共享数据,为什么还要两端都写相似且重复的代码呢?难道我们不能在子组件中写了,然后父组件中就立马检测到数据的变更,然后再更新到视图层吗??VuexVuex官方文档在官方文档中,是这么介绍Vuex的:它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension (opens new window),提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能vuex就是将所有要共享的数据,全部拉进了同一个群聊,集中式的管理,增删改查的方法也是同样如此,你要操作什么数据,直接调用方法即可。并且vue官方还给出了调试工具,像我们使用全局事件总线时,操作数据是不会有历史记录的,但是用vuex,打开调试工具,是可以看到你的操作数据的历史记录的,这一点是其他方式无可比拟的。说重点说重点:为什么用Vuex哈...我们的应用非常容易遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:多个视图依赖于同一状态。来自不同视图的行为需要变更同一状态。对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!就相当于定义了这样的一个中央仓库,所有组件都能够获取到存在里面的数据,也能够对数据进行操作,一旦数据改变,也会更新使用了相关数据的组件视图。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。我们通过vuex将所有数据及操作数据的方式都提取出来,不管从代码层面,亦或者数据管理方面,都变得更加方面,无需像全局事件总线那样,都要事先绑定,子组件触发,再执行回调函数,才能更新视图。建议:vuex虽然方便了,但是如果你的项目应用并不庞大,其实可以使用简单的 store 模式。而无需使用vuex,因为这可能让你的代码显得冗余。平常写个小demo啥的,咱们用用props、全局事件总线就完事啦。后语大家一起加油!!!如若文章中有不足之处,请大家及时指出,在此郑重感谢。纸上得来终觉浅,绝知此事要躬行。大家好,我是博主宁在春:主页一名喜欢文艺却踏上编程这条道路的小青年。希望:我们,待别日相见时,都已有所成。摆摊了摆摊了୧⍤⃝🥖长棍面包 ୧⍤⃝🍔汉堡 ୧⍤⃝🍟薯条 ୧⍤⃝🍗炸鸡腿 ୧⍤⃝🍕披萨 ୧⍤⃝🌭热狗 ୧⍤⃝🥪三明治 ୧⍤⃝🌮可乐饼 ୧⍤⃝🥙夹馍 ୧⍤⃝🥘海鲜披萨 ୧⍤⃝🌯鸡肉卷 ୧⍤⃝🍡三色小丸子 ୧⍤⃝🍲炖土豆 ୧⍤⃝🍱便当 ୧⍤⃝🍘仙贝 ୧⍤⃝🍙饭团 ୧⍤⃝🍛咖喱饭 ୧⍤⃝🍜拉面 ୧⍤⃝🍝意大利面 ୧⍤⃝🍣寿司 ୧⍤⃝🍤炸虾 ୧⍤⃝🎂大蛋糕 ୧⍤⃝🧁纸杯蛋糕 ୧⍤⃝🍰小块蛋榚 ୧⍤⃝🍮布丁以上通通一个赞,一个赞你买不了吃亏,一个赞你买不了上当,真正的物有所值
前言插槽可以说是 Vue 中非常重要的一部分吧,在我学习和练习的过程中,当组件搭配着插槽一起使用的时候,会发挥的更好一些。更多时候也会更加方便。今天介绍Vue中三种插槽吧:默认插槽、具名插槽、作用域插槽。环境准备先搭个初始环境给大家看看哈。一步一步讲完这个插槽。就是写了一个类别组件,分别渲染这三种数据。Category组件<template> <div class="category"> <h1>{{title}}</h1> <ul> <li v-for="(item,index) in listData" :key="index">{{item}}</li> </ul> </div> </template> <script> export default { props: { listData:Array, title: String } } </script> <style scoped> .category{ width: 200px; height: 300px; background-color:pink; } </style>App组件<template> <div id="app"> <Category :listData="games" :title="'Games'" /> <Category :listData="movies" :title="'Movies'" /> <Category :listData="foods" :title="'Foods'" /> </div> </template> <script> import Category from './components/Category.vue' export default { name: 'App', components: { Category }, data () { return { games:['穿越火线','qq飞车','洛克王国'], movies:['你好,李焕英','青春派','匆匆那年'], foods:['邵阳米粉','长沙茶颜','重庆火锅'] } } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; display: flex; justify-content: space-between; } </style>最开始就是如上图一样的需求,但是现在业务需求更改了,电影变成了只宣传其中一个,其他的不进行宣传,吃的也变成只宣传一个拉。如下图:我们怎么改合适呢?是在Category组件中加if一个个进行判断吗?还是有更好的方法勒???一个个判断是不行的,那样子代码会变得十分繁杂,不易阅读,万一以后又要更改业务需求,代码都不好动。接下来就到默认插槽的出现拉。一、默认插槽我们在子组件中不用再用props 接收数据,也不做渲染,而是定义一个插槽。<template> <div class="category"> <!-- 定义插槽,插槽默认内容 --> <slot>如果当父组件不传值过来,即显示此默认</slot> </div> </template> <script> export default { props: { } } </script>App组件也作出更改<template> <div id="app"> <Category> <h1>Games</h1> <!-- <ul> <li v-for="(item, index) in games" :key="index">{{ item }}</li> </ul> --> <img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2Fb352264fa7bfdb6d211f2e71e87cc2c48d85b805.jpg&refer=http%3A%2F%2Fi0.hdslb.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1639931135&t=0b2c6c622c84a1e387196cce8f50455e"> </Category> <Category> <h1>Movies</h1> <img class="movies" src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Finews.gtimg.com%2Fnewsapp_bt%2F0%2F13236694597%2F641.jpg&refer=http%3A%2F%2Finews.gtimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1639931502&t=f89c2197bda9bb129d9404d3c4b30f2f"> <!-- <ul> --> <!-- <li v-for="(item, index) in movies" :key="index">{{ item }}</li> --> <!-- </ul> --> </Category> <Category> <h1>Foods</h1> <ul> <li v-for="(item, index) in foods" :key="index">{{ item }}</li> </ul> </Category> <!-- 当我们什么都没有写的时候,看展示什么 --> <Category> </Category> </div> </template> <script> import Category from './components/Category.vue' export default { name: 'App', components: { Category }, data () { return { games:['穿越火线','qq飞车','洛克王国'], movies:['你好,李焕英','青春派','匆匆那年'], foods:['邵阳米粉','长沙茶颜','重庆火锅'] } } } </script>显示效果:解释:我们在子组件写了一个如果当父组件不传值过来,即显示此默认 标签,此处就相当于占了一个位置。我们在父组件中,也不再像之前一样写自闭和标签,而是写了非自闭和标签 内容 。这样做,Vue就会默认的将写在组件标签中的内容渲染完,然后再放回子组件中的 占好位置的地方去。注意:CSS样式写在父组件或者子组件中都是可以的,因为它是渲染完后才放回子组件中的。写在子组件中,就是在放回子组件中时渲染。写完这里,客户突然觉得你们这么厉害,不满足啦,又开始给你们整幺蛾子。接下来就又到具名插槽登场啦哈。二、具名插槽竟然我们能够想到用一个插槽啦,那么为什么不能想着用两个插槽来试一试勒?改造子组件<template> <div class="category"> <!-- 必须加上名称 在父组件中才能指定要放入那个插槽 这也是为什么叫做具名插槽的原因---> <slot name="slot1">如果当父组件不传值过来,即显示此默认</slot> <slot name="slot2"></slot> </div> </template> <script> export default { props: { } } </script>父组件<template> <div id="app"> <Category> <template slot="slot1"> <h1>Games</h1> <img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2Fb352264fa7bfdb6d211f2e71e87cc2c48d85b805.jpg&refer=http%3A%2F%2Fi0.hdslb.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1639931135&t=0b2c6c622c84a1e387196cce8f50455e" /> </template> <template slot="slot2"> <button > qq登录</button> <button > 微信登录</button> </template> </Category> <Category> <template slot="slot1"> <h1>Movies</h1> <img class="movies" src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Finews.gtimg.com%2Fnewsapp_bt%2F0%2F13236694597%2F641.jpg&refer=http%3A%2F%2Finews.gtimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1639931502&t=f89c2197bda9bb129d9404d3c4b30f2f" /> </template> <template slot="slot2"> <button > 点击购票</button> </template> </Category> <Category> <template slot="slot1"> <h1>Foods</h1> <ul> <li v-for="(item, index) in foods" :key="index">{{ item }}</li> </ul> </template> </Category> <!-- 当我们什么都没有写的时候,看展示什么 --> <Category> </Category> </div> </template> <script> import Category from './components/Category.vue' export default { name: 'App', components: { Category }, data () { return { games:['穿越火线','qq飞车','洛克王国'], movies:['你好,李焕英','青春派','匆匆那年'], foods:['邵阳米粉','长沙茶颜','重庆火锅'] } } } </script>效果展示解释:我们可以在组件中放多个slot,但是多个的时候必须要给他们命名,另外父组件中也要进行指定,这样才不会放不进去。三、作用域插槽作用域插槽和前面稍稍有点不同,之前都是数据在父组件中,而作用域插槽是数据在子组件中,反过来传递给父组件,让父组件定义结构进行渲染。改造的子组件<template> <div class="category"> <slot name="slot1">如果当父组件不传值过来,即显示此默认</slot> <slot name="slot2" :foods="foods">如果当父组件不传值过来,即显示此默认</slot> </div> </template> <script> export default { data () { return{ foods:['邵阳米粉','长沙茶颜','重庆火锅'] } } } </script>父组件<template> <div id="app"> <Category> <template slot="slot1"> <h1>Foods</h1> </template> <template slot="slot2" scope="listData"> <!--如果不知道的 咱们可以输出看看这是什么· {{listData}} --> <ul> <li v-for="(item, index) in listData.foods" :key="index"> {{ item }} </li> </ul> </template> </Category> <Category> <template slot="slot1"> <h1>Foods</h1> </template> <template slot="slot2" scope="listData"> <ol> <li v-for="(item, index) in listData.foods" :key="index"> {{ item }} </li> </ol> </template> </Category> <Category> <template slot="slot1"> <h1>Foods</h1> </template> <template slot="slot2" scope="listData"> <h4 v-for="(item, index) in listData.foods" :key="index"> {{ item }} </h4> </template> </Category> <Category> <template slot="slot1" scope="listData"> {{listData}} </template> </Category> </div> </template> <script> import Category from './components/Category.vue' export default { name: 'App', components: { Category } } </script>效果图这种我在学习及练习过程中,并没有想到哪些使用场景,但是在官网上有案例,我想它必定是有存在的理由,只是我的见识太少,而未能利用到而已。解释:子组件中通过:变量名="定义的数据" 向父组件传值,父组件用
前言上一篇文章写了 Vue 中的自定义事件,自定义事件是全局事件总线基础。我在上一篇文章中埋下了一个小小的伏笔。如下图:我说过,在Vue中如果我们用(@orv-on )给组件绑定上一个自定义事件,其本质就是给子组件VueComponent即vc绑定一个事件,然后子组件通过this.$emit()触发,父组件监听到再执行回调方法。这种也只适合于父子组件之间通信,对于兄弟组件来说,仍然无法非常方便的通信。那全局事件总线是什么样的呢?一、全局事件总线前述提供一个思考方向:其他组件同样如此。那么到这一步,我们要明白一件事情哈,全局事件总线,全局两个字,意思是在全局都能够访问到。并且能够绑定方法呢?即xxxx中保证要能够有$on、$off、$emit这些方法,才能够实现组件间通信。那么只有哪里有??我们之前给子组件绑定自定义事件的时候,其本质是不是给子组件的实例对象new VueComponent绑定上一个自定义事件。在这个全局事件总线中,我们就不能再给每个组件的实例对象来绑定自定义事件了,而是要将自定义事件绑定到一个全部组件都能够访问的对象上。那么那个对象大家都能够访问?看下图吧。---图:来自于尚硅谷-张天宇老师我们将它放在vue原型上,那么全局事件总线就能够做到任意间组件通信拉。二、安装全局事件总线我们弄明白要去找谁了,就要将它定义出来,不然怎么用啊。标准定义如下:import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false // 关于全局总线的使用说明 // 使用全局总线的时候,更好的应用是在兄弟组件、祖孙组件之间,这些组件他们并不能做到直接通信,这个使用全局事件总线会方便很多 new Vue({ render: h => h(App), // beforeCreate 位于数据挂载之前的生命周期函数 beforeCreate () { // 安装全局事件总线 $bus就是当前应用的vm 这里的this就是当前的new Vue() Vue.prototype.$bus = this } }).$mount('#app')beforCreate()方法是众多生命周期中最前面的一个。在此时,它的this就是当前的vue.三、使用全局事件总线1、接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。即是图示中的第一步。// 回车增加一个todo addTodo (todo) { this.todos.unshift(todo) }, // 判断勾选不勾选 checkTodo (id) { this.todos.forEach((todo) => { if (todo.id === id) todo.done = !todo.done }) }, // 删除一个todo deleteTodo (id) { this.todos = this.todos.filter(todo => todo.id !== id) }, // 全选全不选 checkAllTodos (done) { this.todos.forEach((todo) => { todo.done = done }) }, // 清除所有已完成的任务 clearDoneTodos () { this.todos = this.todos.filter(todo => !todo.done) } }, // 在加载完成后就进行全局总线的绑定 mounted () { this.$bus.$on('addTodo', this.addTodo) this.$bus.$on('checkTodo', this.checkTodo) this.$bus.$on('deleteTodo', this.deleteTodo) },2、提供数据:this.$bus.$emit('xxxx',数据)methods: { add () { // 1. 检查输入合法性 const title = this.title.trim() if (!title) { alert('请输入内容') return } const id = uuidv4() // 2. 根据输入生成一个todo对象 const todo = { id, title, done: false } // 3. 添加到todos this.$bus.$emit('addTodo', todo) // 4. 清除输入 this.title = '' } } }注意:最后在beforeDestory钩子中,用$off去解绑当前组件所用到的事件。如下图:解绑有多种方式,$off() 不写参数,是直接解绑全部一个参数$off('xxx')是解绑一个,解绑多个可以写成$off(['xx','xxx'])后语大家一起加油!!!如若文章中有不足之处,请大家及时指出,在此郑重感谢。纸上得来终觉浅,绝知此事要躬行。大家好,我是博主宁在春:主页一名喜欢文艺却踏上编程这条道路的小青年。希望:我们,待别日相见时,都已有所成。
前言原本这篇打算写Vue中的那个全局事件总线的原理,但是发现自己少写了这个自定义事件,不讲明白这个自定义事件的操作,不好写全局事件原理,于是就有了这篇文章拉。一、v-on指令要讲自定义事件,就得先说说v-on指令。因为v-on就是实现自定义事件的基础。v-on官网文档基本介绍v-on指令可以缩写为@,并且我们使用v-on指令时,其实它有一个默认参数event.可以和它一起搭配的修饰符大致有以下几种:.stop - 调用 event.stopPropagation()。 停止冒泡.prevent - 调用 event.preventDefault()。 阻止默认行为.capture - 添加事件侦听器时使用 capture 模式。.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。.{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。 键修饰符,键别名.native - 监听组件根元素的原生事件。.once - 只触发一次回调。.left - (2.2.0) 只当点击鼠标左键时触发。.right - (2.2.0) 只当点击鼠标右键时触发。.middle - (2.2.0) 只当点击鼠标中键时触发。.passive - (2.3.0) 以 { passive: true } 模式添加侦听器这些修饰符部分是可以串联起来使用的。作用:绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略。用在普通元素上时,只能监听原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件。今天第二点才是我们滴重点哈。示例:<!-- 方法处理器 --> <button v-on:click="doThis"></button> <!-- 动态事件 (2.6.0+) --> <button v-on:[event]="doThis"></button> <!-- 内联语句 --> <button v-on:click="doThat('hello', $event)"></button> <!-- 缩写 --> <button @click="doThis"></button> <!-- 动态事件缩写 (2.6.0+) --> <button @[event]="doThis"></button> <!-- 停止冒泡 --> <button @click.stop="doThis"></button> <!-- 阻止默认行为 --> <button @click.prevent="doThis"></button> <!-- 阻止默认行为,没有表达式 --> <form @submit.prevent></form> <!-- 串联修饰符 --> <button @click.stop.prevent="doThis"></button> <!-- 键修饰符,键别名 --> <input @keyup.enter="onEnter"> <!-- 键修饰符,键代码 --> <input @keyup.13="onEnter"> <!-- 点击回调只会触发一次 --> <button v-on:click.once="doThis"></button> <!-- 对象语法 (2.4.0+) --> <button v-on="{ mousedown: doThis, mouseup: doThat }"></button>在子组件上监听自定义事件 (当子组件触发“my-event”时将调用事件处理器):<my-component @my-event="handleThis"></my-component> <!-- 内联语句 --> <my-component @my-event="handleThis(123, $event)"></my-component> <!-- 组件中的原生事件 --> <my-component @click.native="onClick"></my-component>看了这个v-on之后,不知道大家有没有想起VueComponent实例上的$on,即vm.$on(event,callback)。vm.$on(event,callback)用法:监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。在此处,建议大家思考一下他们的区别,因为vm.$on其实就是实现全局事件总线的原理。二、自定义事件简单图示:我们给在App组件中,通过v-on或者@给A组件绑定一个自定义事件,它的触发时机是等到A组件在内部调用this.$emit(’myevent‘),之后就会触发App组件中的回调。实际上我们给A组件通过v-on绑定一个自定义事件,其本质就是我们在A组件实例对象VC上绑定了一个事件,事件名字叫我们自定义的名称。因为我们写了一个组件标签,Vue底层也是要帮我们 new VueComponent()对象。关于自定义事件名自定义事件名它不同于组件和prop,事件名不存在任何自动化的大小写转换。只有事件名称完全匹配时才能监听这个事件。v-on事件监听器在 DOM 模板中会被自动转换为全小写,所以 v-on:myEvent 将会变成 v-on:my-event 从而导致 myEvent不可能被监听到。vue 始终推荐你始终使用kebab-case的事件名。三、入门案例实现效果App组件<template> <div id="app"> <!-- props方法传递 --> <Demo :showMsg="showMsg"></Demo> <!--绑定自定义事件 send-message:是我们自定义事件名, 后面的sendMessage自定义事件被触发执行的回调函数 --> <Demo1 v-on:send-message="sendMessage"></Demo1> </div> </template> <script> import Demo from "./components/Demo.vue"; import Demo1 from "./components/Demo1.vue"; export default { name: "App", components: { Demo, Demo1, }, methods: { showMsg() { alert("收到来自demo的信息"); }, sendMessage(value) { alert("sendMessage自定义事件被触发拉!!!收到来自demo2的信息:" + value); }, }, }; </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>Demo组件<template> <div class="demo"> <h1>Demo</h1> <p>{{ msg }}</p> <button type="button" @click="sendMsg">点击发送消息给App组件</button> </div> </template> <script> export default { props: { showMsg: Function, }, data() { return { msg: "hello,大家好,我是宁在春", }; }, methods: { sendMsg() { this.showMsg(); }, }, }; </script> <style> .demo { width: 200px; height: 200px; background-color: #1488f5; } </style>demo1<template> <div class="demo1"> <h1>Demo2</h1> <span>{{ msg }}</span> <button @click="sendAppMsg(msg)" type="button"> 点击发送消息给App组件 </button> </div> </template> <script> export default { data() { return { msg: "大家好,我是来自Demo2的宁在春", }; }, methods: { sendAppMsg(message) { this.$emit("send-message", message); }, }, }; </script> <style> .demo1 { width: 200px; height: 200px; background-color: pink; } </style>今天是先写个头,下一节再说全局事件总线的原理。后语大家一起加油!!!如若文章中有不足之处,请大家及时指出,在此郑重感谢。纸上得来终觉浅,绝知此事要躬行。大家好,我是博主宁在春:主页一名喜欢文艺却踏上编程这条道路的小青年。希望:我们,待别日相见时,都已有所成。
月亮啊月亮你能照见南边,也能照见北边照见她,你跟她说一声,就说我想她了。前言前一篇文章写了 vue 中利用 Props 实现组件之间的通信,那种方式是最简单也是最基础的组件之间的通信方式。父组件通过 props 向下传数据给子组件,当子组件有事情告诉父组件时会通过$emit事件告诉父组件。对于父子组件,这种传递方式,是较为方便且实用的,但是对于祖孙组件或者兄弟组件,就显得不那么友善了。在Vue本身的生态中,也有一个独立的Vuex库用来处理组件之间的通讯,但很多时候,咱们并不需要动用类似Vuex这种大杀招,而可以考虑更简单的 Vue 中的事件总线,即EventBus。在这提出一个简单的思考:一旦当你看到项目中,某段代码或者是要点很多下才能出来的变量,再或者获取到的方式都相同,这个时候你就一定要考虑能不能让代码达到复用,咱们要学会偷懒哈,偷懒才能前进的更快哈.下面开始今天的正文哈...一、什么叫全局事件总线1.1、概念的引入我们先认清一件事情,所谓的组件之间的交互,就是我们将数据能够做到组件之间能够共享数据。无论是props、EventBus、Vuex、发布订阅等实现组件交互,本质就是做到数据共享。弄清这一点,对于使用全局事件总线,就简单多了哈。不过今天的文章,主要是先带着大家使用,原理等周末拉。EventBus 又称为事件总线。在Vue中可以使用 EventBus 来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的灾难,因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。1.2、安装全局事件总线//想要成为事件总线的条件: //1、所有的组件对象必须都能看得到这个总线对象,因此我们把这个对象放在了Vue原型 //2、这个事件总线对象必须能调用$on和$emit方法(总线对象必须是Vue的实例化对象或者是组件对象)确定全局事件总线: 将vm对象作为事件总线挂载到vue的原型对象上import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false // 关于全局总线的使用说明 // 使用全局总线的时候,更好的应用是在兄弟组件、祖孙组件之间,这些组件他们并不能做到直接通信,这个使用全局事件总线会方便很多 new Vue({ render: h => h(App), // beforeCreate 位于数据挂载之前的生命周期函数 beforeCreate () { // 安装全局事件总线 Vue.prototype.$bus = this } }).$mount('#app')1.3、基本使用小案例:在App组件内引入一个demo组件,demo组件中有一个按钮,点击可以修改app组件传给子组件的值,并更新视图。App组件<template> <div class="todo-container"> //数据的传递,还是用props快哈 <Demo :msg="msg"></Demo> </div> </template> <script> import Demo from './components/Demo' export default { components: { Demo }, data () { return { msg: 'hello,你好' } }, methods: { updateMsg () { this.msg = 'hello,你好丫,我是博主宁在春' }, updateMsg2 (value) { this.msg = value } }, // 在加载完成后就进行全局总线的绑定 mounted () { // 绑定方法,'updateMsg'是全局事件总线方法名,而后面是回调时需要执行的方法 this.$bus.$on('updateMsg', this.updateMsg) this.$bus.$on('updateMsg2', this.updateMsg2) }, // 养成习惯 在组件销毁的时候,将事件进行解绑 beforeDestroy () { //就是解绑事件,有多种方式,参数为空,直接是让所有方法解绑 //多个的时候,可以直接放一个数组进去。 // this.$bus.$off(['updateMsg',....]) this.$bus.$off('updateMsg') this.$bus.$off('updateMsg2') // 原理就让我留到下次吧,兄弟们 } } </script>demo组件<template> <div> <h1>{{msg}}</h1> <button @click="updateMessage()">点击修改</button> <button @click="updateMessage2('你好丫,宁在春')">点击修改</button> </div> </template> <script> export default { props: { msg: String }, methods: { updateMessage () { // 通过全局事件总线来进行交互, //第一个参数是要 回调父组件中的方法名,后面可以跟参数,多个或者对象都可以 this.$bus.$emit('updateMsg') }, updateMessage2 (value) { this.$bus.$emit('updateMsg2', value) } } } </script>二、全局事件总线和Props实现组件通信的区别个人使用总结的哈:props用来实现组件之间通信,更多的方便于父子组件通信。如果是祖孙或者兄弟组件,将会多一层中间层,而且没有任何用处,而且给人的感觉比较繁琐。全局事件总线的话,将它挂在vm原型上,对于祖孙组件、或者兄弟组件之间通信,非常的方便,不需要中间层,非常的方便。后语大家一起加油!!!如若文章中有不足之处,请大家及时指出,在此郑重感谢。纸上得来终觉浅,绝知此事要躬行。大家好,我是博主宁在春:主页一名喜欢文艺却踏上编程这条道路的小青年。希望:我们,待别日相见时,都已有所成。
前言上一篇文章写了:「后端小伙伴来学前端了」分析Vue脚手架结构简单说明了Vue的脚手架结构,但是上篇文章还欠了个小点没有说完,就在这篇文章中补齐。就是所谓的render函数。一、main.js中引入的原来是残缺版vue.js我们来接着看看main.js这个入口文件。// 引入vue import Vue from 'vue' // 引入app组件 import App from './App.vue' // 关闭生产提示 Vue.config.productionTip = false // 创建 Vue 实例对象 Vm new Vue({ render: h => h(App) // 这里不是一下就能说完的,这里简单说下: // App 组件渲染,这里的 h 即是 vm.$createElement ,便是在vm.render这个阶段 // 最粗略的理解,执行完这里,就是将app 放入了 容器中去了。 }).$mount('#app') // Vue 的$mount()为手动挂载 这个也不是一下能说清,我也学艺不精,以后再补上 哈哈这个代码,我想咱们只要创建过vue项目,大家肯定都写过了哈。但是不知道大家有没有纠结过或者思考过new Vue() 中的 render:h=>h(App)是干什么。(我是纯属刚学,好奇宝宝😂)按照我们最开始的学习:下面这种写法也是可行的吧,组件我们引入了,也注册了。import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false new Vue({ template:'<App></App>' components:{App} }).$mount('#app')看页面:页面上是空白的,然后有以下报错信息://报错 vue.runtime.esm.js?2b0e:619 [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. //提示的解决方式 Either pre-compile the templates into render functions, or use the compiler-included build. (found in <Root>)这里的报错意思:您正在使用仅运行时版本的Vue解决方式提示有两种:可以将模板预编译为呈现函数, 就是我们之前写的 render 函数 也可以使用编译器附带的构建。(换成人话就是使用完整版的vue,包含模板编译器版本的vue)注意:我们在 main.js 中所引用的import Vue from 'vue',其实是一个运行时版本的vue,并非是完整的。证明方式:我们按照ctrl,用鼠标点击我们引入的vue,再点到vue的文件夹下的dist文件下的vue在这源码上加上一句话,看看会不会运行。虽然还是报错的,但是我们打印的那句话是已经出来了。(虽然有那么多vue...js,但是咋知道是这个呢?测出来的,亲测)报错提示中,告诉我们说,如果引入完整版vue也能解决问题,那么我们就引用完整路径,来测试一下,看可以吗?当我引入完整版的vue之后,我项目中的内容就已经展示出来了,控制台也不再报错。到这个时候,大家也会想,我们既然可以通过引入完整的 vue.js 来进行模板的解析,为什么还要写那个 render函数呢?原因大致如下:这个模板引擎只是在我们生产的时候能够用到,当我们用 webpack 进行打包的时候,就用不上这个vue这个自带的模板引擎了, webpack已经帮我们把vue文件解析成了浏览器认识的.js、.css、.html文件了,那么vue的引擎也没必要继续存在那啦丫。(你可以把它当做个工具人,用就要,不用就扔掉哈哈)但是如果我们一定残缺版的vue呢?这个render函数在这里是做什么呢?二、render函数我们先看看效果哈,当我们改成残缺版vue.js,写上render函数,是成功可以运行的接下来我们一步一步把这个函数解析出来哈:我们先拆成个普通函数,看看它是什么样的哈render (h) { console.log(h) return null }我把拆开,输出来是下面这样的:这个传进来的参数原来就是个 函数。它的返回值也是函数createElement()首先说明一下我的demo项目的结构,然后你再思考思考我是有一个App组件和四个组件小组件,并且在App中进行了引入,而这上面也正好有四个参数,而createElement()中正好是一个vm,后面跟着这四个参数。我们简单想一下就是一个App带着四个小弟哈哈.所以换而言之,如果我们写成普通函数,就是如下状态render (h) { console.log(h) return h(App) }因为我们的组件全部都在 App 内,所以我们实际只需要渲染 app 即可啦。但是这里的其实不叫h,它真正的术语叫createElmentrender (createElment) { return createElment(App) }然后再简化成箭头函数就成了脚手架中的 render: h => h(App)这里 render 其实就是App组件渲染,脚手架方便确实方便了很多,但是真的封装了很多我们看不到的东西.虽然有手就能用,但是就因为简单,我想我们对于它的理解,在很长很长的一段时间内都会处于表面上吧.后语大家一起加油!!!如若文章中有不足之处,请大家及时指出,在此郑重感谢。纸上得来终觉浅,绝知此事要躬行。大家好,我是博主宁在春:主页一名喜欢文艺却踏上编程这条道路的小青年。希望:我们,待别日相见时,都已有所成。
前言每日匆匆忙忙的写老师布置的 Vue 项目,对于 Vue 始终没有一个系统的认知,每天都是遇到什么问题就去查什么样的问题。看起来好像也没啥问题,但是所有的知识都是混入的,导致没有一个像样的体系。也就导致有了以下问题的存在:难以一起讨论。和他们聊天,讲的很多东西我都插不上嘴(学习的路上一定要明白,交流才是让人进步的最快方式,也是发现自己的缺陷和长处的最快方式)解决问题的方式的不同。同样的问题,他们解决问题的代码远远比我写的优雅。看待问题的角度、深度不一样。我想的更多的是如何立马解决当前的问题,而不会、也不知道自己的解决方式会不会带来其他问题。所以我就下定决心,不再把这件事情,当做一个任务来完成了,而是认认真真的来和大家一起学习前端。【放心,我的主业是Java开发,虽然落魄,但是好玩,还有好多好多源码没看勒👨💻】希望能够趁着这段时间持续更新「后端小伙伴来学前端了」系列,对了。一、分析Vue脚手架结构创建vue脚手架咱就默认都会了哈。👨🏫我创建的是一个带路由的vue脚手架哈,文件目录结构大致如下。1.1、babel.config.jsbabel编译js(可以把高级的es语法转化成低级的),平时使用默认的即可这点我没有详细去了解过,如果感兴趣可以查看 babel官网,看看可以配置一些什么。官方文档1.2、package.json简单说就是包的说明书。1.3、package-lock.json这个东西,在我眼里其实就和我们后端用 maven 管理依赖包版本一样的意思。但是东西更多一点点的感觉。二、main.js每个程序都会有个程序入口。那么在vue中其实也一样。我们想想,当我们在命令行敲入npm run serve 之后,程序就开始运行了,运行入口又在哪里呢?程序入口其实就是main.js。我们做个简单测试就知道了。我们把main.js中全部注释掉,就在控制台上打印个输出看看。程序是启动了,没有报错。浏览器从结果上来看,main.js是程序入口是没错,接下来,我们再对main.js这个文件做给详细的认识。下半截大致的解释如下:// 引入vue import Vue from 'vue' // 引入app组件 import App from './App.vue' // 引入 router组件 import router from './router' // 关闭生产提示 Vue.config.productionTip = false // 创建 Vue 实例对象 Vm new Vue({ router, // 路由 render: h => h(App) // 这里不是一下就能说完的,这里简单说下: // App 组件渲染,这里的 h 即是 vm.$createElement ,便是在vm.render这个阶段 // 最粗略的理解,执行完这里,就是将app 放入了 容器中去了。 }).$mount('#app') // Vue 的$mount()为手动挂载 这个也不是一下能说清,我也学艺不精,以后再补上 哈哈但是看到这里其实还是没懂的,因为浏览器它是解析不了vue,我们必须要把我们写的vue代码转换为html、js、css才能在浏览器上正常显示,那么html在哪里呢?在我们之前说的public文件夹中哈。2.1、public我们页面上常常看到的那个小图标,就是这里的,如果我们要修改,直接整一个ico图片进来替换掉即可。(名字得一样哈,不然就去改改index.html文件哈)index.html<!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <!-- 配置网页标题 htmlWebpackPlugin.options.title 这行代码其实是webpack悄咪咪给你做的, 我没有深究原理哈 我们写了这行代码,它就会去 packege.json 文件中去找 我们项目的名称 --> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <!-- 这个noscript 标签就是当 浏览器不支持 js 时,会自动触发,当然我们都知道哈,不能解析js的浏览器,怕早就凉在了历史长河中啦 --> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <!-- 这里就是我们的容器啦 --> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>2.2、流程说明当我们在cmd中输入 npm run serve就去 src 中 找到 main.js引入相关的依赖 如import Vue from 'vue';import App from './App.vue'执行 render: h => h(App),简单理解,就是执行完这个,就是将app放进容器中去了。为什么这么做呢?(脚手架给我们配置的)很多细节没法一一说明,我也还在继续学习中。一起加油。自言自语大家一起加油!!!如若文章中有不足之处,请大家及时指出,在此郑重感谢。纸上得来终觉浅,绝知此事要躬行。大家好,我是博主宁在春:主页一名喜欢文艺却踏上编程这条道路的小青年。希望:我们,待别日相见时,都已有所成
夜晚有明月,梦里有佳人前言最近在写老师布置的vue项目,真的说实话,每天真就是在百度、google、bing等各个搜索引擎之间反复横跳,不然就是掘金搜一搜、思否搜一搜,还有CSDN看一看。我的前端是吃百家饭长大的,每天不知道要遇到多少问题,然后基本上周围所有的前端同学都被我问到了,基本上就是谁有空就拉谁来教我。前端太多细节问题了,一旦遇到没有接触过问题,就会非常麻烦,如果有学习前端的后端小伙伴,我觉得最快熟悉前端的方式,就是整个项目写。这可能是最快上手前端框架的方式了吧。一、vue中修改数组对象下的数组里的某一个对象我的对象结构如下:sections: [ { id: 0, addInputBool: true, generallnformationBool: false, generallnformation: '', updateGenerlInfoImgBool: false, pullUpQusetionBool: true, upQusetionBool: true, downQuestionBool: false, questions: [ { id:'', name:'', isCheckbox:'', answer:'', conditions:[], dropdownMultiSelections:[] } ] } ]要实现的需求是通过数组下标,修改数组里某一个对象。最开始我的想法就是将数值一个一个的赋值进数组,和写Java代码一样的思维。this.sections[index].question[id]=this.addQuestion这里的index和id是我们点击页面修改传过来的值,最后发现这样一直报错,不能够实现修改。后来查百度说:问题:根据数组的索引直接赋值没法修改数组的中对象。原因:Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上然后就查到了要使用this.$set来进行操作解决:// 数组:第一个参数是要修改的数组, 第二个值是修改的下标或字段,第三个是要修改成什么值 this.$set(sections[index].question,id,{ id:'123', name:'宁在春', isCheckbox:true, answer:'测试集', conditions:[1,2,3], dropdownMultiSelections:[a,b,c] }); 或者 // 对象:第一个参数是要修改的对象, 第二个值是修改属性字段,第三个是要修改成什么值 Vue.set(sections[index].question,id,{ id:'123', name:'宁在春', isCheckbox:true, answer:'测试集', conditions:[1,2,3], dropdownMultiSelections:[a,b,c]})看到有这个this.$set方法,就想去了解了解,看看它还有什么应用场景,方便下次有需要的时候,能够直接用上。二、this.$set2.1、this.$set能够实现什么功能官方解释:向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hello,ningzaichun')简单说即是:当你发现你给对象加了一个属性,在控制台能打印出来,但是却没有更新到视图上时,也许这个时候就需要用到this.$set()这个方法了2.2、如何使用 this.$setVue中this.$set的用法 // 数组:第一个参数是要修改的数组, 第二个值是修改的下标或字段,第三个是要修改成什么值 // 对象:第一个参数是要修改的对象, 第二个值是修改属性字段,第三个是要修改成什么值 Vue.set( target, propertyName/index, value ) 参数 {Object | Array} target {string | number} propertyName/index {any} value小小案例:<template> <div class="page" id="app"> <button @click="add">设置</button> <ol> <li v-for="(item, index) in arr" :key="index">{{ item.name }}</li> </ol> </div> </template> <script> export default { data () { return { arr: [ { name: '宁在春', age: 21 }, { name: '北桑夏', age: 21 } ] } }, mounted () { this.arr[2] = { name: '青冬栗', age: 23 } // 数组的值发生了变化 但视图没有更改 console.log(this.arr) }, methods: { add () { this.$set(this.arr, 2, { name: '青冬栗', age: 23 }) // $set 触发视图更改 } } } </script>target: 要更改的数据源(可以是一个对象或者数组) key 要更改的具体数据 (索引) value 重新赋的值在vue的生命周期钩子函数mounted中,我们手动的在数组加入了一个值,但是并不会直接在页面视图进行更新。初始化的页面是这样的。但是在控制台其实是已经打印出来的拉但是如果我们点击按钮的设置,视图就会立马发生改变这就是this.$set一个妙用之处。2.3、this.$set 应用场景1、在我们使用vue进行开发中,可能会碰到一种情况,当已经生成vue实例后,再次去给数据赋值或者添加数据,并不能同步更新到数据上面去。2、另外就是像我这种,利用this.$set进行数据的更新自言自语纸上得来终觉浅,绝知此事要躬行。大家好,我是博主宁在春:主页一名喜欢文艺却踏上编程这条道路的小青年。希望:我们,待别日相见时,都已有所成。
前言今天在写前端的时候,就是遇到一个问题。一开始我以为用textarea去掉角标,实现自动增长,然后就可以了。谁知道它还得加样式,加粗、斜体,老师在最开始给的设计稿上根本没有。直接麻掉。后来就去搞这个富文本编辑器。感觉前端也不容易,要学习的东西真的蛮多。功能需求是这样的就是选中文章,给它加粗,加斜体,加样式,并且选中的时候能够在上面弹出一个小菜单。为了这个气泡菜单,真的找了很多富文本编辑器,最后翻到个element-tiptap,看到样式上有这个。一、Element-tiptap富文本编辑器介绍它易于使用,对开发人员友好,完全可扩展,设计简洁。用它的话,主要是和element适配度高,然后我就想用他了,使用element-ui组件。github官网:github.com/Leecason/el…二、开始使用npm 安装:npm install --save element-tiptap直接安装完就完事啦正常菜单:来贴个最简单的例子:<template> <div> <el-tiptap v-model="content" :extensions="extensions" /> </div> </template> <script> import { // necessary extensions Doc, Text, Paragraph, Heading, Bold, Underline, Italic, Strike, ListItem, BulletList, OrderedList, } from 'element-tiptap'; export default { data () { // editor extensions // they will be added to menubar and bubble menu by the order you declare. return { extensions: [ new Doc(), new Text(), new Paragraph(), new Heading({ level: 5 }), new Bold({ bubble: true }), // render command-button in bubble menu. new Underline({ bubble: true, menubar: false }), // render command-button in bubble menu but not in menubar. new Italic(), new Strike(), new ListItem(), new BulletList(), new OrderedList(), ], // editor's content content: ` <h1>Heading</h1> <p>This Editor is awesome!</p> `, }; }, }, </script>效果图如下:他的样式是直接和element-ui结合的。功能这个方面的话,确实有点点少,但是就正好是符合我的需求。😂另外还自带划词选中弹出气泡菜单。气泡菜单:直接是上面那个例子就已经实现了。另外他的参数就是和element一样,是直接绑定在标签上的。例如:我们不需要字符计数直接在标签上绑定这个属性即可charCounterCount如下:<div> <el-tiptap v-model="content" :extensions="extensions" :charCounterCount="false" /> </div>三、自言自语现在每天的文章,正的就是每天卡着晚上11.50发布,要做到日更真的好难啊。纸上得来终觉浅,绝知此事要躬行。大家好,我是博主宁在春:主页一名喜欢文艺却踏上编程这条道路的小青年。希望:我们,待别日相见时,都已有所成。不过,说真的,富文本编辑器坑真的好多,目前我都还只是写出demo,自定义样式都无法做到。
本文主要讲述Netty相关概念及为什么会出现Netty,Netty的作用有哪些等?以及学习Netty需要什么。前言:我其实更好奇的是:你是因为什么点进了这篇博客,是想要了解Netty;或者是因为自己本心中的好奇心;亦或者是业务场景中需要用Java网络编程,然后百度搜索,搜到了Netty。如果你想要了解,这篇文章我想是适合你的。如果你是想要满足自己的好奇心,想要深究一番的,那么这篇就是Netty的开山篇。如果是业务中需要用到Netty框架,并且已经有了好的基础,我想可以直接跳过这一篇,直接进入实战加设计理论篇可能会更好。我想要学习Netty框架一方面是因为好奇框架中服务与服务之间的通信,一直是处于懂得使用,而不懂其然的状态;另一方面是我想掌握这个技术,想自己琢磨出点东西,写一点属于自己的东西,扩展自己的知识面,Netty正好与我不谋而合,所以我来了。建议:学习Netty框架最好是已经对Java IO编程、网络编程、多线程编程有一定了解。Netty 官网一、Java网络编程1.1、Java BIO最早期的 Java API(java.net)只支持由本地系统套接字库提供的所谓的阻塞函数。简单示例:/** * @Author: crush * @Date: 2021-08-23 11:51 * version 1.0c */ public class BioServer { public static void main(String[] args) throws Exception { //1、创建ServerSocket ServerSocket serverSocket = new ServerSocket(8888); while (true) { //2、监听 等待客户端连接。 该方法阻塞,直到建立连接。 final Socket socket = serverSocket.accept(); System.out.println("连接到一个客户端"); //3、就创建一个线程,与之通讯(单独写一个方法) new Thread(() -> { //4、可以和客户端通讯 handler(socket); }).start(); } } /** * 编写一个handler方法,和客户端通讯,读取客户端发过来的信息 * @param socket */ public static void handler(Socket socket) { try { byte[] bytes = new byte[1024]; //通过socket获取输入流 InputStream inputStream = socket.getInputStream(); //循环的读取客户端发送的数据 while (true) { //从输入流中读取一定数量的字节并将它们存储到缓冲区数组b 。 //实际读取的字节数作为整数返回。 //此方法会阻塞,直到输入数据可用、检测到文件结尾或抛出异常。 int read = inputStream.read(bytes); if (read != -1) { //处理客户端发送的数据::输出客户端发送的数据 System.out.println(new String(bytes, 0, read)); } else { break; } } } catch (Exception e) { e.printStackTrace(); } finally { try { socket.close(); } catch (Exception e) { e.printStackTrace(); } } } }说几个比较重要的点:ServerSocket 上的 accept()方法将会一直阻塞到一个连接建立 ,随后返回一个 新的 Socket 用于客户端和服务器之间的通信。该 ServerSocket 将继续监听传入的 连接。InputStream流处理方式.read()方式将会阻塞,直到在 处一个由换行符或者回车符结尾的字符串被 读取。处理客户端发送的数据。需要注意到的点:这段代码只能同时处理一个连接,如果要管理多个客户端,需要为每个Socket创建一个Thread。我们可以画出它的模型图了,如下:这有什影响呢?第一、在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,会造成资源极大的浪费。第二、Java需要为每个线程的调用栈都分配内存,其默认值大小区间为 64 KB 到 1 MB,具体取决于操作系统,超过1000的话,可能JVM的内存都会被干掉一半。第三、即使 Java 虚拟机(JVM)在物理上可以支持非常大数量的线程,但是远在到达该极限之前,上下文切换所带来的开销就会带来麻烦。这种模式比较适用于中小型的并发,一旦到了成千上万的话,效果就会非常不理想了,更多的话甚至就是糟糕啦。当然Java已经发展这么多年了,不可能不突破的哈,在jdk1.4的时候就增加了Java NIO。如果还想了解一下Java BIO网络编程👈1.2、Java NIOJava BIO是阻塞式的调用,Java NIO 就是来解决这个问题的。Java NIO 是属于非阻塞调用,对原生的IO 也做了进一步的改写。在Jdk1.4的时候引入的,位于java.nio包下。Java NIO的模型图是这样的:几个重要的点:不再像BIO那样是一个线程对应着一个客户端,一个单一的线程便可以处理多个并发的连接,减少了内存管理和上下文切换所带来开销;它使用了事件通知 API 以确定在一组非阻塞套接字中有哪些已经就绪能够进行 I/O 相关的操作。Selector 会根据不同的事件,在各个通道上切换,但是程序切换到哪个 Channel 是由事件(比如:连接请求、数据到达)决定的。只有当socket真正有读写事件时,才会真正调用实际的IO读写操作。当没有 I/O 操作需要处理的时候,线程也可以被用于其他任务。NIO是解决了BIO所产生的一些问题,但是编程也同时的变得繁琐了。框架是什么?个人简单理解:就是加了一层,一层不行就加两层,总之没有加层解决不了的。(😃狗头保命)另外就是直接使用底层的API暴露了复杂性,并且不好去进行扩展,所以勒,我们就要用较简单的抽象来隐藏底层实现的复杂性啦。😁想要了解更详细的Java NIO 网络编程可以点👈。二、Netty简介2.1、概述Netty是由JBOSS提供的一个java开源框架,现为Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。也就是说,Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。2.2、Netty 特点分类Netty的特性设计1、统一的API,支持多种类型传输,阻塞的和非阻塞的。2、简单而强大的线程模型 3、逻辑组件支持复用,不需要重复造轮子易于使用详细的JavaDoc文档,有许多的示例性能拥有比 Java 的核心 API 更高的吞吐量以及更低的延迟,得益于池化和复用,拥有更低的资源消耗,最少的内存复制健壮性不会因为慢速、快速或者超载的连接而导致 OutOfMemoryError ;消除在高速网络中 NIO 应用程序常见的不公平读/写比率安全性完整的 SSL/TLS 以及 StartTLS 支持;可用于受限环境下,如 Applet 和 OSGI社区社区活跃,更新速快2.3、异步和事件驱动知道异步先看同步是什么吧,同步简单说就是串行。举个例子:你去餐馆,在服务员那点了菜,然后服务员去后厨告诉厨师你点了什么菜,然后等厨师炒好,给你送过来。异步:异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。举个生活中的例子就是:你去餐馆,在服务员那点了菜,服务员去后厨告诉厨师你点的菜,然后就回来告诉你菜等会上。服务员就去做其他事情。菜好了,服务员又给你端过来了。Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。事件驱动:Netty使用了异步的事件驱动模型,来触发网络I/O的各种操作,其在socket层上面封装一层异步事件驱动模型,使得业务代码不需要关心网络底层,就可以编写异步的无网络I/O阻塞的代码。事件发生时主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,其实是设计模式中观察者模式的思路。三、自言自语想了很久,最后还是把这一篇文章拆成两篇了,不然篇幅太长了,让人看着发愁。下半篇是:属于你的第一个Netty应用程序
JSON Web Token(缩写 JWT)是目前最流行,也是最常见的跨域认证解决方案。无论是咱们后端小伙伴,还是前端小伙伴对都是需要了解。 本文介绍它的原理、使用场景、用法。一、跨域认证的问题1.1、常见的前后端认证方式Session-CookieToken 验证(包括JWT,SSO)OAuth2.0(开放授权)1.2、Session-Cookie实现方式流程大致如下:1、用户向服务器发送用户名和密码。2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。3、服务器向用户返回一个 session_id,写入用户的 Cookie。4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。这种模式在单机时不存在什么问题,但是一旦服务器变为集群模式时,或者是跨域的服务器时,这个时候Session就必须实现数据共享。这个时候就要考虑每台服务器如何实现对 Session 的数据共享呢??第一种解决方式就是实现 Session 数据的持久化。各种服务收到请求时,都向数据持久层请求数据,来验证是否是正确的用户。但其实无论我们将 Session 存放在服务器哪里,都会增加服务器的负担。这种方案优点就是简单,缺点就是扩展性不好,安全性较差,容易增加服务器的负担。第二种解决方式其实就是 JWT 的方式实现的,所有的数据不在保存到服务器端,而是保存到客户端,每次请求时都携带上 Token 令牌。二、什么是 JWT ?根据官网介绍:JSON Web Token (JWT) 是一个开放标准,它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。JWT 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。简单来理解就是 JWT 就是一个JSON对象经过加密和签名的,可以在网络中安全的传输信息,并且可以被验证和信任。2.1、什么时候应该使用 JWT ?我目前用的最多的地方就是在授权方面,这也是 JWT 最常见的场景,其次还可以用来交换信息。授权例子:用户登录后,服务器端返回一个JWT,用户保存在本地,之后的每次请求都将包含JWT,服务器验证用户携带的JWT,来判断是否允许访问服务和资源。另外,单点登录(SSO) 也是当今广泛使用JWT的一项功能,就是在A网站登录后,在B网站也能够实现自动登录,而不需要重复登录,如你在淘宝登录了,在身份没有过期前,你去看天猫网站,也会发现你已经登录了。简而言之:用户只需要登录一次就可以访问所有相互信任的应用系统。并且能够轻松跨不同域使用,服务器也不需要存储session相关信息,减轻了负担。2.2、JWT 原理其实 JWT 的原理就是,服务器认证以后,将一个 JSON 对象加密成一个紧凑的字符串(Token),发回给用户,就像下面这样。// JSON 对象 { "姓名": "王五", "角色": "管理员", "到期时间": "2021年9月21日0点0分" } //加密后 eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VybmFtZSIsIm5iZiI6MTYzMjI3NzU1NCwiaXNzIjoiY3J1c2giLCJleHAiOjE2MzIyNzc2NTQsImRlbW8iOiLlj6_lrZjlgqjkv6Hmga8iLCJpYXQiOjE2MzIyNzc1NTQsImRlbW8yIjoi5Y-v5a2Y5YKo5L-h5oGvMiJ9.OuqG5Ha_Ofmh5R9Et1vqLYSAlIO85oW9D9Jq9cKKYODO643ZLiDTyQs8dl3PLsZ-_5t0xv6kfKhCzCkCYznBNA在认证之后,用户和服务器通信时,每次都会携带上这个Token。服务器端不再存储session信息,完全依靠用户携带的Token来判断用户身份。为了安全,服务器在生成Token的时候,都会加上一个数字签名。这样做的优势:服务器不需要再保存 session数据,减轻了服务器负担,并且基于 JWT 认证机制的应用不需要去考虑用户在哪一台服务器登录,为应用的扩展提供了便利。2.3、JWT 数据结构JSON Web Tokens 由用点 ( .)分隔的三个部分组成,它们是:Header(头部)Payload(负载)Signature(签名)因此,JWT 通常如下所示。注意:实际上是未分行的,这里是为了便于展示。xxxxx.yyyyy.zzzzz 如: eyJhbGciOiJIUzUxMiJ9. eyJzdWIiOiJ1c2VybmFtZSIsIm5iZiI6MTYzMjI3NzU1NCwiaXNzIjoiY3J1c2giLCJleHAiOjE2MzIyNzc2NTQsImRlbW8iOiLlj6_lrZjlgqjkv6Hmga8iLCJpYXQiOjE2MzIyNzc1NTQsImRlbW8yIjoi5Y-v5a2Y5YKo5L-h5oGvMiJ9. OuqG5Ha_Ofmh5R9Et1vqLYSAlIO85oW9D9Jq9cKKYODO643ZLiDTyQs8dl3PLsZ-_5t0xv6kfKhCzCkCYznBNA2.3.1、Header (标题)jwt的头部承载两部分信息:声明类型,这里是jwt声明加密的算法 通常直接使用 HMAC SHA256Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。{ "alg": "HS256", "typ": "JWT" }上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。2.3.2、Payload(有效载荷)Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。iss (issuer):签发人exp (expiration time):过期时间sub (subject):主题 jwt所面向的用户aud (audience):受众 接收jwt的一方nbf (Not Before):生效时间iat (Issued At):签发时间jti (JWT ID):编号,jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。{ "sub": "1234567890", "name": "John Doe", "admin": true }注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。2.3.3、Signature(签名)Signature 部分是对前两部分的签名,防止数据篡改。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。HMACSHA256 ( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。注意:签名用于验证消息在此过程中没有更改,并且在使用私钥签名的令牌的情况下,它还可以验证 JWT 的发送者是它所说的人。secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证的关键,所以,它就是我们服务端的私钥,在任何场景都不应该泄露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了,那么安全将不复存在。2.3.4、 Base64URL前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。JWT 作为一个令牌(token),有些场合可能会 放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。2.4、JWT工具类相关依赖:<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>如果是Jdk11使用的话,可能会报这样的一个错误:Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter at io.jsonwebtoken.impl.Base64Codec.decode(Base64Codec.java:26) at io.jsonwebtoken.impl.DefaultJwtBuilder.signWith(DefaultJwtBuilder.java:99) at com.crush.jwt.utils.JwtUtils.createJwt(JwtUtils.java:47) at com.crush.jwt.utils.JwtUtils.main(JwtUtils.java:127) Caused by: java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ... 4 more好像是因为Jdk11中没有这个类了,得加上下面这样的一个依赖:<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency>工具类import io.jsonwebtoken.*; import java.util.Date; import java.util.HashMap; /** * @Author: crush * @Date: 2021-09-21 22:18 * version 1.0 */ public class JwtUtils { /** * 服务器端密钥 */ private static final String SECRET = "jwtsecretdemo"; /** * 颁发者 */ private static final String ISS = "crush"; /** * 这里创建用到的时间、用户名、应该是传入进来的, * 登录时选择是否记住我,过期时间应当是不一致的。 * @return */ public static String createJwt() { HashMap<String, Object> map = new HashMap<>(); map.put("demo", "可存储信息"); map.put("demo2","可存储信息2"); String jwt = Jwts.builder() .setClaims(map) // jwt所面向的用户 .setSubject("username") //设置颁发者 .setIssuer(ISS) // 定义在什么时间之前,该jwt都是不可用的. .setNotBefore(new Date()) //签发时间 .setIssuedAt(new Date()) //设置 JWT 声明exp (到期)值 .setExpiration(new Date(System.currentTimeMillis() + 100000)) .signWith(SignatureAlgorithm.HS512, SECRET) //实际构建 JWT 并根据JWT 紧凑序列化 规则将其序列化为紧凑的、URL 安全的字符串。 .compact(); return jwt; } /** * 获取 Claims 实例 * Claims :一个 JWT声明集 。 * 这最终是一个 JSON 映射,可以向其中添加任何值,但为了方便起见,JWT 标准名称作为类型安全的 getter 和 setter 提供。 * 因为这个接口扩展了Map&lt;String, Object&gt; , 如果您想添加自己的属性,只需使用 map 方法, * 例如: * claims.put("someKey", "someValue"); * * @param jwt * @return */ public static Claims getBody(String jwt) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(jwt) .getBody(); } /** * 判断 JWT 是否已过期 * * @param jwt * @return */ public static boolean isExpiration(String jwt) { return getBody(jwt) //返回 JWT exp (到期)时间戳,如果不存在则返回null 。 .getExpiration() //测试此日期是否在指定日期之前。 .before(new Date()); } /** * Subject:获取 jwt 所面向的用户 * * @param jwt * @return */ public static String getSubject(String jwt) { return getBody(jwt).getSubject(); } /** * Issuer:获取颁发者 * * @param jwt * @return */ public static String getIssuer(String jwt) { return getBody(jwt).getIssuer(); } /** * getClaimsValue * * @param jwt * @return */ public static String getClaimsValue(String jwt) { return (String) getBody(jwt).get("demo"); } /** * getClaimsValue * * @param jwt * @return */ public static String getClaimsValue2(String jwt) { return (String) getBody(jwt).get("demo2"); } public static void main(String[] args) { String jwt = createJwt(); System.out.println(jwt); System.out.println("jwt 是否已经过期:"+isExpiration(jwt)); System.out.println("Claims 中所存储信息:"+getBody(jwt).toString()); System.out.println("jwt 所面向的用户:"+getSubject(jwt)); System.out.println("jwt 颁发者:"+getIssuer(jwt)); System.out.println("通过键值,取出我们自己放进 Jwt 中的信息:"+getClaimsValue(jwt)); System.out.println("通过键值,取出我们自己放进 Jwt 中的信息2:"+getClaimsValue2(jwt)); } }三、如何应用此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。Authorization: Bearer <token>一般是在请求头里加入Authorization,并加上Bearer标注:fetch('api/user/1', { headers: { 'Authorization': 'Bearer ' + token } })服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的:3.1、实际使用实际使用过程中,我们通常是结合着Security安全框架一起使用的,大家感兴趣的话,可以来一起看看我写的这篇文章。SpringBoot整合Security安全框架、控制权限也可以直接看源码:Security-Gitee四、总结4.1、优点:因为json的通用性,JWT支持多语言,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。它不需要在服务端保存会话信息, 所以它易于应用的扩展4.2、安全相关:保护好secret私钥,该私钥非常重要。如果密钥泄露,用户自己即可颁布JWT令牌,安全将不复存在。如果条件允许,JWT 不应该使用 HTTP 协议明码传输,而是要使用 HTTPS 协议传输。Https协议更安全。JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。4.3、缺点:JWT 的最大优点是不需要在服务端保存会话信息,最大的缺点也是如此,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效。五、自言自语本文就是简单介绍了,具体使用具体情况具体分析啦。你好,我是博主宁在春:主页希望本篇文章能让你感到有所收获!!!祝 我们:待别日相见时,都已有所成。参考:jwtJSON Web Token 入门教程
你好,我是博主宁在春关于在映射路径中匹配正则表达式,我是第一次知道(原谅我的无知)。在之前都是手动判断(if、类型判断啥的)或者是添加验证。这次学长给了我一个项目,让我学习学习,就是在里面发现这个的。一、曾经像我以前的使用,都是像下面这种方式使用的,根本就没考虑这个问题😂/** * 在请求中我们并没有对id的输入做限制,什么都可以输入 * 我这里限制了id的类型为Long。 * 如果输入字符进入,则会直接报400错误 * * @param id * @return */ @GetMapping("/{id}") public String demo1(@PathVariable("id") Long id){ return "demo"+id.toString(); }输入字符,直接报400输入数字可以正常访问。这样的结果出现是我限制了id的类型为Long,我们换成String类型试试。@GetMapping("/{id}") public String demo1(@PathVariable("id") String id){ return "demo: "+id; }结果就是都可以访问了。😂思考:假定id一定需要为全数字,但是类型又为String,这种字符输入的是不是应该被抛掉,不应该请求进来勒?二、使用正则表达式正则表达式就是起这样的作用。/** * 在这个请求中,我们就限制了 url中的id必须为数字类型 * 输入非数字类型就会直接转到404 * @param id * @return */ @GetMapping("/{id:\d+}") public String demo(@PathVariable("id") Long id){ return "demo"+id.toString(); }输入数字是可以正常访问的:看看输入字符:报的错误是没有找到,404,不是之前的请求错误。我们接着换成String类型来试的话,结果也是一模一样的。三、小结使用正则表达式在Resulful风格中单参数时非常实用。或者是在下面这样的情况下也可以非常实用,就是请求中既有一个单参数,又携带了一个Java对象。@PostMapping("/{id:\d+}") public String demo2(@PathVariable("id") String id,@RequestBody BookDTO bookDTO){ return "demo: "+id; }四、自言自语本文就是简单介绍了,具体使用具体情况具体分析啦。你好,我是博主宁在春:主页希望本篇文章能让你感到有所收获!!!祝 我们:待别日相见时,都已有所成。
SpringBoot是我们经常使用的框架,那么你能不能针对SpringBoot实现自动配置做一个详细的介绍。如果可以的话,能不能画一下实现自动配置的流程图。牵扯到哪些关键类,以及哪些关键点。下面我们一起来看看吧!!前言:阅读完本文:你能知道 SpringBoot 启动时的自动配置的原理知识你能知道 SpringBoot 启动时的自动配置的流程以及对于 SpringBoot 一些常用注解的了解一步一步 debug 从浅到深。注意:本文的 SpringBoot 版本为 2.5.2一、启动类前言什么的,就不说了,大家都会用的,我们直接从 SpringBoot 启动类说起。@SpringBootApplication public class Hello { public static void main(String[] args) { SpringApplication.run(Hello.class); } }@SpringBootApplication 标注在某个类上说明这个类是 SpringBoot 的主配置类, SpringBoot 就应该运行这个类的main方法来启动 SpringBoot 应用;是我们研究的重点!!!它的本质是一个组合注解,我们点进去,看看javadoc上是怎么写的,分析从浅到深,从粗略到详细。我们点进去看:@Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {}Javadoc上是这么写的表示声明一个或多个@Bean方法并触发 auto-configuration 和 component scanning 的 configuration 类。 这是一个方便的注解,相当于声明了 @Configuration 、 @EnableAutoConfiguration 和@ComponentScan 。---为什么它能集成这么多的注解的功能呢?是在于它上面的 @Inherited 注解, @Inherited 表示自动继承注解类型。这里的最重要的两个注解是 @SpringBootConfiguration 和 @EnableAutoConfiguration。1.1、@SpringBootConfiguration我们先点进去看看 @SpringBootConfiguration注解:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration @Indexed public @interface SpringBootConfiguration {}。1.2、@EnableAutoConfiguration再看看 @EnableAutoConfiguration.@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration {}1.3、@ComponentScan@ComponentScan:配置用于 Configuration 类的组件扫描指令。 提供与 Spring XML 的 元素并行的支持。 可以 basePackageClasses 或basePackages ( 或其别名value )来定义要扫描的特定包。 如果没有定义特定的包,将从声明该注解的类的包开始扫描。作为了解,不是本文重点。1.4、探究方向主要探究图中位于中间部分那条主线,其他只会稍做讲解。二、@SpringBootConfiguration我们刚刚已经简单看了一下 @SpringBootConfiguration 啦。@Configuration @Indexed public @interface SpringBootConfiguration {}它是 springboot 的配置类,标注在某个类上,表示这是一个 springboot的配置类。我们在这看到 @Configuration ,这个注解我们在 Spring 中就已经看到过了,它的意思就是将一个类标注为 Spring 的配置类,相当于之前 Spring 中的 xml 文件,可以向容器中注入组件。不是探究重点。三、@EnableAutoConfiguration我们来看看这玩意,它的字面意思就是:自动导入配置。@Inherited @AutoConfigurationPackage ////自动导包 @Import(AutoConfigurationImportSelector.class) ////自动配置导入选择 public @interface EnableAutoConfiguration {}从这里顾名思义就能猜到这里肯定是跟自动配置有关系的。我们接着来看看这上面的两个注解 @AutoConfigurationPackage 和 @Import(AutoConfigurationImportSelector.class) ,这两个才是我们研究的重点。3.1、@AutoConfigurationPackage点进去一看:@Inherited @Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage {}@Import 为 spring 的注解,导入一个配置文件,在 springboot 中为给容器导入一个组件,而导入的组件由 AutoConfigurationPackages.Registrar.class 执行逻辑来决定的。往下👇看:Registrarstatic class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0])); } @Override public Set<Object> determineImports(AnnotationMetadata metadata) { return Collections.singleton(new PackageImports(metadata)); } }在这个地方我们可以打个断点,看看 new PackageImports(metadata).getPackageNames().toArray(new String[0]) 它是一个什么值。我们用 Evaluate 计算 new PackageImports(metadata).getPackageNames().toArray(new String[0]) 出来可以看到就是 com.crush.hello ,当前启动类所在的包。继续往下看的话就是和 Spring 注册相关了,更深入 xdm 可以继续 debug。在这里我们可以得到一个小小的结论:@AutoConfigurationPackage 这个注解本身的含义就是将主配置类(@SpringBootApplication 标注的类)所在的包下面所有的组件都扫描到 spring 容器中。如果将一个 Controller 放到 com.crush.hello 以外就不会被扫描到了,就会报错。3.2、@Import(AutoConfigurationImportSelector.class)AutoConfigurationImportSelector 开启自动配置类的导包的选择器(导入哪些组件的选择器)我们点进 AutoConfigurationImportSelector 类来看看,有哪些重点知识,这个类中存在方法可以帮我们获取所有的配置public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { /**选择需要导入的组件 ,*/ @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } //根据导入的@Configuration类的AnnotationMetadata返回AutoConfigurationImportSelector.AutoConfigurationEntry 。 protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } AnnotationAttributes attributes = getAttributes(annotationMetadata); // 可以在这打个断点,看看 返回的数据 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); //删除重复项 configurations = removeDuplicates(configurations); // 排除依赖 Set<String> exclusions = getExclusions(annotationMetadata, attributes); //检查 checkExcludedClasses(configurations, exclusions); //删除需要排除的依赖 configurations.removeAll(exclusions); configurations = getConfigurationClassFilter().filter(configurations); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); } }我们看看这个断点,configurations 数组长度为131,并且文件后缀名都为 **AutoConfiguration这里的意思是将所有需要导入的组件以全类名的方式返回,并添加到容器中,最终会给容器中导入非常多的自动配置类(xxxAutoConfiguration),给容器中导入这个场景需要的所有组件,并配置好这些组件。有了自动配置,就不需要我们自己手写了。3.2.1、getCandidateConfigurations()我们还需要思考一下,这些配置都从 getCandidateConfigurations 方法中获取,这个方法可以用来获取所有候选的配置,那么这些候选的配置又是从哪来的呢?一步一步点进去:protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 这里有个 loadFactoryNames 方法 执行的时候还传了两个参数,一个是BeanClassLoader ,另一个是 getSpringFactoriesLoaderFactoryClass() 我们一起看看 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct."); return configurations; }看一下getSpringFactoriesLoaderFactoryClass() 方法,这里传过去的是protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }这个 EnableAutoConfiguration 是不是特别眼熟,(我们探究的起点 @EnableAutoConfiguration ,有没有感觉自己离答案越来越近啦)我们再看看 loadFactoryNames() 方法带着它去做了什么处理:先是将 EnableAutoConfiguration.class 传给了 factoryType ,然后 .getName( ) ,所以factoryTypeName 值为 EnableAutoConfiguration。3.2.2、loadSpringFactories()接下里又开始调用 loadSpringFactories 方法这里的 FACTORIES_RESOURCE_LOCATION 在上面有定义:public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";我们再回到 getCandidateConfigurations 方法处。Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct.");这句断言的意思是:“在 META-INF/spring.factories 中没有找到自动配置类。如果您使用自定义包装,请确保该文件是正确的。“这个 META-INF/spring.factories 在哪里呢?里面的内容:我们日常用到的,基本上都有一个配置类。比如 webmvc,我们点进 WebMvcProperties 类中去看一下:那这里到底是要干什么呢?这里的意思首先是把这个文件的 urls 拿到之后并把这些 urls 每一个遍历,最终把这些文件整成一个properties 对象,loadProperties方法然后再从 properties 对象里边获取一些我们需要的值,把这些获取到的值来加载我们最终要返回的这个结果,结果 result 为 map 集合,然后返回到loadFactoryNames方法中。然后我们再回到 loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList()); 的调用处。这个 factoryTypeName 值为 EnableAutoConfiguration因为 loadFactoryNames 方法携带过来的第一个参数为 EnableAutoConfiguration.class,所以 factoryType 值也为 EnableAutoConfiguration.class,那么 factoryTypeName 值为 EnableAutoConfiguration。那么map集合中 getOrDefault 方法为什么意思呢?意思就是当 Map 集合中有这个 key 时,就使用这个 key值,如果没有就使用默认值 defaultValue (第二个参数),所以是判断是否包含 EnableAutoConfiguration看下图,这不就是嘛?所以就是把 spring-boot-autoconfigure-2.5.2.jar/META-INF/spring.factories 这个文件下的EnableAutoConfiguration 下面所有的组件,每一个 xxxAutoConfiguration 类都是容器中的一个组件,都加入到容器中。加入到容器中之后的作用就是用它们来做自动配置,这就是Springboot自动配置开始的地方。只有这些自动配置类进入到容器中以后,接下来这个自动配置类才开始进行启动那 spring.factories 中存在那么多的配置,每次启动时都是把它们全部加载吗?是全部加载嘛?不可能的哈,这谁都知道哈,全部加载启动一个项目不知道要多久去了。它是有选择的。我们随便点开一个类,都有这个 @ConditionalOnXXX 注解@Conditional 其实是 spring 底层注解,意思就是根据不同的条件,来进行自己不同的条件判断,如果满足指定的条件,那么整个配置类里边的配置才会生效。所以在加载自动配置类的时候,并不是将 spring.factories 的配置全部加载进来,而是通过这个注解的判断,如果注解中的类都存在,才会进行加载。这就是SpringBoot的自动配置啦.四、小结简单总结起来就是:启动类中有一个 @SpringBootApplication 注解,包含了 @SpringBootConfiguration、 @EnableAutoConfiguration , @EnableAutoConfiguration 代表开启自动装配,注解会去 spring-boot-autoconfigure 工程下寻找 META-INF/spring.factories 文件,此文件中列举了所有能够自动装配类的清单,然后自动读取里面的自动装配配置类清单。因为有 @ConditionalOn 条件注解,满足一定条件配置才会生效,否则不生效。 如: @ConditionalOnClass(某类.class) 工程中必须包含一些相关的类时,配置才会生效。所以说当我们的依赖中引入了一些对应的类之后,满足了自动装配的条件后,自动装配才会被触发。五、自言自语纸上得来终觉浅,绝知此事要躬行。如果可以,可以自己 debug 一遍,画一画流程图。🛌 (躺平)你好,我是博主宁在春:主页希望本篇文章能让你感到有所收获!!!祝 我们:待别日相见时,都已有所成。如有疑惑,大家可以留言评论。如有不足之处,请大家指出来,非常感谢 👨💻。
一步一步走来,之前去学习了JUC并发编程知识,现在终于到Java IO网络编程啦,难啊。一、BIO介绍引入: 随着技术的发展,两个或以上的程序必然需要进行交互,于是提供了一种端到端的通信,相当于对传输层的一种封装,对于开发人员而言隐藏了传输的细节,将这些固定的“套路”抽象出来,提供一种端到端的通信,可以使我们更加专注于业务的开发。而BIO只是其中一种。Java BIO (old )就是传统的 Java I/O 编程,其相关的类和接口在 java.io,另外Java BIO是同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。效率较低,资源容易被浪费。阻塞和非阻塞:阻塞和非阻塞指的是执行一个操作是等操作结束再返回,还是马上返回。比如餐馆的服务员为用户点菜,当有用户点完菜后,服务员将菜单给后台厨师,此时有两种方式:第一种:就在出菜窗口等待,直到厨师炒完菜后将菜送到窗口,然后服务员再将菜送到用户手中;(阻塞方式)第二种:等一会再到窗口来问厨师,某个菜好了没?如果没有先处理其他事情,等会再去问一次;(非阻塞的)二、BIO模型流程分析:首先需要启动一个ServerSocket服务端,用来供客户端连接然后再启动ClientSocket客户端,与服务器进行连接通信。(注:默认情况下,每个客户端与服务端都是单独的一个线程通信,不管使用不使用)在客户端发出请求后,会先询问服务器端是否可以有线程响应,有以下两种结果:如若有线程响应,客户端会阻塞等待请求结束后,再继续执行;假如没有线程响应则会等待响应,或者直接被拒绝三、代码案例1)案例:我们使用BIO模型写一个服务器端,监听8888端口,当有客户端连接时,就启动一个线程与它通讯。编程思路:创建一个线程池创建ServerSocket对象 服务器套接字(ServerSocket)等待通过网络传入的请求。如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)获得 Socket对象, 用于连接通信编写一个handler方法,和客户端通讯,读取客户端发过来的信息package com.crush.bio; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.*; /** * @Author: crush * @Date: 2021-08-23 11:51 * version 1.0c */ public class BioServer { public static void main(String[] args) throws Exception { //1. 创建一个线程池 ExecutorService newCachedThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); //2、创建ServerSocket ServerSocket serverSocket = new ServerSocket(8888); System.out.println("服务器启动了"); while (true) { System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName()); //监听,等待客户端连接 System.out.println("等待连接...."); //3.侦听要与此套接字建立的连接并接受它。 该方法阻塞,直到建立连接。 final Socket socket = serverSocket.accept(); System.out.println("连接到一个客户端"); //4、就创建一个线程,与之通讯(单独写一个方法) newCachedThreadPool.execute(() -> { //可以和客户端通讯 handler(socket); }); } } /** * 编写一个handler方法,和客户端通讯,读取客户端发过来的信息 * @param socket */ public static void handler(Socket socket) { try { System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName()); byte[] bytes = new byte[1024]; //通过socket获取输入流 InputStream inputStream = socket.getInputStream(); //循环的读取客户端发送的数据 while (true) { System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName()); System.out.println("read...."); int read = inputStream.read(bytes); if (read != -1) { //输出客户端发送的数据 System.out.println(new String(bytes, 0, read)); } else { break; } } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("关闭和client的连接"); try { socket.close(); } catch (Exception e) { e.printStackTrace(); } } } }这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。2)测试步骤:打开cmd命令输入telnet localhost 8888会进入到telnet页面然后在telnet 命令窗口中按下 CTRL+]发送信息命令 是 send 信息控制台输出客户端:客户端我也有写的哈😁package com.crush.bio; import java.io.PrintStream; import java.net.Socket; import java.util.Scanner; public class BIOEchoClient { public static void main(String[] args) throws Exception{ Socket client = new Socket("localhost",8888); PrintStream out = new PrintStream(client.getOutputStream()); boolean flag = true; while (flag){ Scanner scanner = new Scanner(System.in); String inputData = scanner.nextLine().trim(); out.println(inputData); if ("byebye".equalsIgnoreCase(inputData)){ flag = false; System.out.println("和客户端说再见拉!!!"); } } client.close(); } }这个测试就不说了哈,这个蛮简单的。3)可能会出现的问题我们使用telnet命令来测试,默认Windows这个命令是关闭的,就会出现和我一样的问题。打开方式:打开控制面板,点程序,然后再点这个进行选择。四、BIO存在的缺陷BIO的最大缺陷就是在于每个请求都需要创建独立的线程进行连接通讯,这样会造成以下几点问题:当并发数上升到较大的时候,需要创建大量线程来处理,容易给系统造成极大的压力,其次创建太多线程、销毁太多线程,占用系统资源较大。如果建立连接后,当前线程任务较小,较短,然后之后没有数据可读,则线程会一直阻塞在Read操作上,造成资源的浪费。五、自言自语最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。在学习路上充满好奇心,明白思考的重要性,是支持我一直学习下去的积极推动力吧。希望你也能喜欢上编程!😁热爱生活,享受生活!!!无论在哪里,无论此时的生活多么艰难,都要记得热爱生活!!!相信总会有光来的。你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。希望与君共勉😁我们:待别时相见时,都已有所成。
多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!沉下去,再浮上来,我想我们会变的不一样的。CompletableFuture一、什么是CompletableFuture?在Java中CompletableFuture用于异步编程,异步通常意味着非阻塞,可以使我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。在这种方式中,主线程不会被阻塞,因为子线程是另外一条线程在执行,所以不需要一直等到子线程完成。主线程就可以并行的执行其他任务。这种并行方式,可以极大的提供程序性能。CompletableFuture 实现了 Future, CompletionStage 接口。实现了 Future接口CompletableFuture就可以兼容现在有线程池框架;CompletionStage 接口是异步编程的接口抽象,里面定义多种异步方法,实现了CompletionStage多种抽象方法和Future并与一起使用,从而才打造出了强大的CompletableFuture 类。二、Future 与 CompletableFutureCompletableFuture 是 Future API的扩展。Future接口源码上说明:Future表示异步计算的结果。 提供了检查计算是否完成、等待计算完成以及检索计算结果的方法。 结果只能在计算完成后使用get方法检索,必要时阻塞,直到它准备好。 取消由cancel方法执行。 提供了其他方法来确定任务是正常完成还是被取消。 一旦计算完成,就不能取消计算。 --来自谷歌翻译Future 的主要缺点如下:(1)不能够手动的主动给完成任务(即不能手动的主动结束任务)(2)Future 的结果在非阻塞的情况下,不能执行更进一步的操作Future 不会通知你它已经完成了,它提供了一个阻塞的 get() 方法通知你结果。就是它完成了,你不会被通知,只能主动去询问它。(3)不能够支持链式调用就是不能将上一个Future的计算结果传递给下一个Future使用,即不能构成像Web中的Filter模式一样.(4)不支持多个 Future 合并就是不能将多个Future合并起来。(5)不支持异常处理Future 的 API 没有任何的异常处理的 api,所以运行时,很有可能无法定位到错误。Future API:public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); //尝试取消此任务的执行。 boolean isCancelled();//如果此任务在正常完成之前被取消,则返回true boolean isDone(); //如果此任务完成,则返回true 。 完成可能是由于正常终止、异常或取消——在所有这些情况下,此方法将返回true V get() throws InterruptedException, ExecutionException; //获得任务计算结果 V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;//可等待多少时间去获得任务计算结果 }三、应用3.1、创建CompletableFuture对象CompletableFuture提供了四个静态方法用来创建CompletableFuture对象://runAsync 返回void 函数第二个参数表示是用我们自己创建的线程池,否则采用默认的ForkJoinPool.commonPool() public static CompletableFuture<Void> runAsync(Runnable runnable) public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) //supplyAsync 异步返回一个结果 函数第二个参数表示是用我们自己创建的线程池,否则采用默认的ForkJoinPool.commonPool() //Supplier 是一个函数式接口,代表是一个生成者的意思 public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)3.2、场景一:主动完成任务场景:主线程里面创建一个 CompletableFuture,然后主线程调用 get 方法会 阻塞,最后我们在一个子线程中使其终止。/** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo1 { /** * 主线程里面创建一个 CompletableFuture,然后主线程调用 get 方法会阻塞,最后我们在一个子线程中使其终止 * * @param args */ public static void main(String[] args) throws Exception { CompletableFuture<String> future = new CompletableFuture<>(); new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + "子线程开始干活"); //子线程睡 5 秒 Thread.sleep(5000); // //在子线程中完成主线程 如果注释掉这一行代码将会一直停住 future.complete("success"); } catch (Exception e) { e.printStackTrace(); } }, "A").start(); //主线程调用 get 方法阻塞 System.out.println("主线程调用 get 方法获取结果为: " + future.get()); System.out.println("主线程完成,阻塞结束!!!!!!"); } }3.3、场景二:没有返回值的异步任务runAsync:返回一个新的 CompletableFuture,它在运行给定操作后由在ForkJoinPool.commonPool()运行的任务异步完成。如果你想异步的运行一个后台任务并且不需要任务返回结果,就可以使用runAsync/** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo2 { /** * 没有返回值的异步任务 * * @param args */ public static void main(String[] args) throws Exception { System.out.println("主线程开始"); //运行一个没有返回值的异步任务 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { try { System.out.println("子线程启动干活"); Thread.sleep(5000); System.out.println("子线程完成"); } catch (Exception e) { e.printStackTrace(); } }); //主线程阻塞 future.get(); System.out.println("主线程结束"); } }3.4、场景三:有返回值的异步任务supplyAsync:返回任务结果。CompletableFuture.supplyAsync()它持有supplier 并且返回CompletableFuture,T 是通过调用 传入的supplier取得的值的类型。Supplier 是一个简单的函数式接口,表示supplier的结果。它有一个get()方法,该方法可以写入你的后台任务中,并且返回结果。public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier) { return asyncSupplyStage(ASYNC_POOL, supplier); }/** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo2 { /** * 有返回值的异步任务 * * @param args */ public static void main(String[] args) throws Exception { System.out.println("主线程开始"); //运行一个没有返回值的异步任务 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { System.out.println("子线程启动干活"); Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } return "子线程任务完成"; }); //主线程阻塞 System.out.println(future.get()); System.out.println("主线程结束"); } } /** * 主线程开始 * 子线程启动干活 * 子线程任务完成 * 主线程结束 */3.5、场景四:线程串行化当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。/** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo4 { private static String action=""; /** * 线程依赖 * 1、我到了烧烤店, * 2、开始点烧烤 * 3、和朋友次完烧烤 ,给女朋友带奶茶回去 * @param args */ public static void main(String[] args) throws Exception { System.out.println("主线程开始"); CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { action="和朋友一起去次烧烤!!!! "; return action; }).thenApply(string -> { return action+="到店里——>开始点烧烤!!"; }).thenApply(String->{ return action+="和朋友们次完烧烤,给女朋友带杯奶茶回去!!"; }); String str = future.get(); System.out.println("主线程结束, 子线程的结果为:" + str); } } /** 主线程开始 主线程结束, 子线程的结果为:和朋友一起去次烧烤!!!到店里——>开始点烧烤!!和朋友们次完烧烤,给女朋友带杯奶茶回去!! */3.6、场景五:thenAccept 消费处理结果如果你不想从你的回调函数中返回任何东西,仅仅想在Future完成后运行一些代码片段,你可以使用thenAccept() 和 thenRun()方法,这些方法经常在调用链的最末端的最后一个回调函数中使用。thenAccept 消费处理结果, 接收任务的处理结果,并消费处理,无返回结果。/** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo5 { private static String action = ""; public static void main(String[] args) throws Exception { System.out.println("主线程开始"); CompletableFuture.supplyAsync(() -> { try { action = "逛淘宝,想买双鞋 "; } catch (Exception e) { e.printStackTrace(); } return action; }).thenApply(string -> { return action + "选中了,下单成功!!"; }).thenApply(String -> { return action + "等待快递到来"; }).thenAccept(new Consumer<String>() { @Override public void accept(String s) { System.out.println("子线程全部处理完成,最后调用了 accept,结果为:" + s); } }); } } /** 主线程开始 子线程全部处理完成,最后调用了 accept,结果为:逛淘宝,想买双鞋 等待快递到来 */3.7、场景六:异常处理exceptionally 异常处理,出现异常时触发,可以回调给你一个从原始Future中生成的错误恢复的机会。你可以在这里记录这个异常并返回一个默认值。/** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo6 { public static void main(String[] args) throws Exception{ System.out.println("主线程开始"); CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { int i= 1/0; System.out.println("子线程执行中"); return i; }).exceptionally(ex -> { System.out.println(ex.getMessage()); return -1; }); System.out.println(future.get()); } } /** * 主线程开始 * java.lang.ArithmeticException: / by zero * -1 */使用 handle() 方法处理异常API提供了一个更通用的方法 - handle()从异常恢复,无论一个异常是否发生它都会被调用CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> { System.out.println("任务开始"); int i=0/1; return i; }).handle((i,ex) -> { System.out.println("进入 handle 方法"); if (ex != null) { System.out.println("发生了异常,内容为:" + ex.getMessage()); return -1; } else { System.out.println("正常完成,内容为: " + i); return i; } });3.8、场景七: 结果合并thenCompose 合并两个有依赖关系的 CompletableFutures 的执行结果/** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo7 { private static Integer num = 10; public static void main(String[] args) throws Exception { System.out.println("主线程开始"); //第一步加 10 CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("让num+10;任务开始"); num += 10; return num; }); //合并 CompletableFuture<Integer> future1 = future.thenCompose(i -> //再来一个 CompletableFuture CompletableFuture.supplyAsync(() -> { return i + 1; })); System.out.println(future.get()); System.out.println(future1.get()); } } /** * 主线程开始 * 让num+10;任务开始 * 20 * 21 */thenCombine 合并两个没有依赖关系的 CompletableFutures 任务package com.crush.juc09; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; /** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo8 { private static Integer sum = 0; private static Integer count = 1; public static void main(String[] args) throws Exception{ System.out.println("主线程开始"); CompletableFuture<Integer> job1 = CompletableFuture.supplyAsync(() -> { System.out.println("从1+...+50开始"); for (int i=1;i<=50;i++){ sum+=i; } System.out.println("sum::"+sum); return sum; }); CompletableFuture<Integer> job2 = CompletableFuture.supplyAsync(() -> { System.out.println("从1*...*10开始"); for (int i=1;i<=10;i++){ count=count*i; } System.out.println("count::"+count); return count; }); //合并两个结果 CompletableFuture<Object> future = job1.thenCombine(job2, new BiFunction<Integer, Integer, List<Integer>>() { @Override public List<Integer> apply(Integer a, Integer b) { List<Integer> list = new ArrayList<>(); list.add(a); list.add(b); return list; } }); System.out.println("合并结果为:" + future.get()); } } /** 主线程开始 从1*...*10开始 从1+...+50开始 sum::1275 count::3628800 合并结果为:[1275, 3628800] */3.9、场景八:合并多个任务的结果allOf 与 anyOfallOf: 一系列独立的 future 任务,等其所有的任务执行完后做一些事情/** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo9 { private static Integer num = 10; public static void main(String[] args) throws Exception{ System.out.println("主线程开始"); List<CompletableFuture> list = new ArrayList<>(); CompletableFuture<Integer> job1 = CompletableFuture.supplyAsync(() -> { System.out.println("加 10 任务开始"); num += 10; return num; }); list.add(job1); CompletableFuture<Integer> job2 = CompletableFuture.supplyAsync(() -> { System.out.println("乘以 10 任务开始"); num = num * 10; return num; }); list.add(job2); CompletableFuture<Integer> job3 = CompletableFuture.supplyAsync(() -> { System.out.println("减以 10 任务开始"); num = num - 10; return num; }); list.add(job3); CompletableFuture<Integer> job4 = CompletableFuture.supplyAsync(() -> { System.out.println("除以 10 任务开始"); num = num / 10; return num; }); list.add(job4); //多任务合并 List<Integer> collect = list.stream().map(CompletableFuture<Integer>::join).collect(Collectors.toList()); System.out.println(collect); } } /**主线程开始 乘以 10 任务开始 加 10 任务开始 减以 10 任务开始 除以 10 任务开始 [110, 100, 100, 10] */anyOf: 只要在多个 future 里面有一个返回,整个任务就可以结束,而不需要等到每一个 future 结束package com.crush.juc09; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; /** * @Author: crush * @Date: 2021-08-23 9:08 * version 1.0 */ public class CompletableFutureDemo10 { private static Integer num = 10; /** * 先对一个数加 10,然后取平方 * @param args */ public static void main(String[] args) throws Exception{ System.out.println("主线程开始"); CompletableFuture<Integer>[] futures = new CompletableFuture[4]; CompletableFuture<Integer> job1 = CompletableFuture.supplyAsync(() -> { try{ Thread.sleep(5000); System.out.println("加 10 任务开始"); num += 10; return num; }catch (Exception e){ return 0; } }); futures[0] = job1; CompletableFuture<Integer> job2 = CompletableFuture.supplyAsync(() -> { try{ Thread.sleep(2000); System.out.println("乘以 10 任务开始"); num = num * 10; return num; }catch (Exception e){ return 1; } }); futures[1] = job2; CompletableFuture<Integer> job3 = CompletableFuture.supplyAsync(() -> { try{ Thread.sleep(3000); System.out.println("减以 10 任务开始"); num = num - 10; return num; }catch (Exception e){ return 2; } }); futures[2] = job3; CompletableFuture<Integer> job4 = CompletableFuture.supplyAsync(() -> { try{ Thread.sleep(4000); System.out.println("除以 10 任务开始"); num = num / 10; return num; }catch (Exception e){ return 3; } }); futures[3] = job4; CompletableFuture<Object> future = CompletableFuture.anyOf(futures); System.out.println(future.get()); } } //主线程开始 //乘以 10 任务开始 //100四、小结本文只是做了一点简单介绍,还需要大家更深入的了解。🌈自言自语最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。希望与君共勉😁我们:待别时相见时,都已有所成。
多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!沉下去,再浮上来,我想我们会变的不一样的。🚤Fork&Join框架1)介绍Fork/Join框架是从Java1.7开始提供的一个并行处理任务的框架,它的基本思路是将一个大任务分解成若干个小任务,并行处理多个小任务,最后再汇总合并这些小任务的结果便可得到原来的大任务结果。通俗点说它就是一个事情划分给好几个人做,效率得到显著提升。1、Fork :递归式的将大任务分割成合适大小的小任务。2、Join:执行任务并合并结果。2)相关类我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。 ForkJoin 类提供了在任务中执行 fork 和 join 的机制。通常情况下我们都是直接继承ForkJoinTask 的子类,Fork/Join框架提供了两个子类:RecursiveAction:一个递归无结果的ForkJoinTask(没有返回值)任务RecursiveTask:一个递归有结果的ForkJoinTask(有返回值)任务ForkJoinTask 主要方法:fork() // 在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务。 join() //当任务完成的时候返回计算结果。 invoke() //开始执行任务,如果必要,等待计算完成。ForkJoinPool:另外ForkJoinTask需要通过 ForkJoinPool 来执行RecursiveTask:一个递归有结果的ForkJoinTask(有返回值)任务🚁Fork 方法调用fork方法时,程序会将新创建的子任务放入当前线程的workQueue队列中,Fork/Join框架将根据当前正在并发执行的ForkJoinTask任务的ForkJoinWorkerThread线程状态,来决定是让这个任务在队列中等待,还是创建一个新的ForkJoinWorkerThread线程运行它,又或者是唤起其它正在等待任务的ForkJoinWorkerThread线程运行它。public final ForkJoinTask<V> fork() { Thread t; if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ((ForkJoinWorkerThread)t).workQueue.push(this); else //将给定的任务添加到提交者当前队列的提交队列中,如果为 null 或存在竞争,则创建一个。 ForkJoinPool.common.externalPush(this); return this; }push方法把当前任务存放在 ForkJoinTask 数组队列里。然后再调用 ForkJoinPool 的 signalWork()方法唤醒或创建一个工作线程来执行任务。代码如下://推送任务。 仅由非共享队列中的所有者调用。 final void push(ForkJoinTask<?> task) { ForkJoinTask<?>[] a; int s = top, d, cap, m; ForkJoinPool p = pool; if ((a = array) != null && (cap = a.length) > 0) { QA.setRelease(a, (m = cap - 1) & s, task); top = s + 1; if (((d = s - (int)BASE.getAcquire(this)) & ~1) == 0 && p != null) { // size 0 or 1 VarHandle.fullFence(); // signalWork方法的意义在于,如果运行的工作程序太少,则尝试创建或释放工作程序。 p.signalWork(); } // 如果array的剩余空间不够了,则进行增加 else if (d == m) growArray(false); } }🪂Join 方法Join 方法的主要作用是阻塞当前线程并等待获取结果。代码如下:通过 doJoin()方法得到当前任务的状态来判断返回什么结果,任务状态有 4 种:已完成(NORMAL)、被取消(CANCELLED)、信号(SIGNAL)和出现异常(EXCEPTIONAL)public final V join() { int s; if ((s = doJoin() & DONE_MASK) != NORMAL) //假如任务状态是抛出异常状态,就直接抛出对应的异常 //若是任务状态是被取消状态,则直接抛出CancellationException异常。 reportException(s); //如若任务状态是已经完成,则直接立马返回任务结果。 //即使此任务异常完成,如果不知道此任务已完成,则返回null return getRawResult(); }让我们分析一下 doJoin 方法的实现private int doJoin() { int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w; //1. 首先通过查看任务的状态,看任务是否已经执行完成,如果执行完成,则直接返回任务状态; //2. 如果没有执行完任务,则从任务数组里取出任务并执行。 //3. 如果任务顺利执行完成,则设置任务状态为 NORMAL,假如出现异常,则记录异常,并将任务状态设置为 EXCEPTIONAL。 return (s = status) < 0 ? s : ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ? (w = (wt = (ForkJoinWorkerThread)t).workQueue). tryUnpush(this) && (s = doExec()) < 0 ? s : //帮助和/或阻塞,直到给定的任务完成或超时 wt.pool.awaitJoin(w, this, 0L) : externalAwaitDone(); }🚤Fork/Join 框架的异常处理ForkJoinTask 在执行的时候可能会抛出异常,但因为它并不是在主线程中运行,故此没有办法在主线程中去捕获异常,这种问题既然是历史遗留, ForkJoinTas这个后来者当然也是提供了API来处理的啦,如下://如果此任务引发异常或被取消,则返回true 。 通常用来判断任务情况 public final boolean isCompletedAbnormally() { return (status & ABNORMAL) != 0; }//getException 方法返回 Throwable 对象,如果任务被取消了则返回CancellationException,如果任务没有完成或者没有抛出异常则返回 null。 public final Throwable getException() { int s = status; return ((s & ABNORMAL) == 0 ? null : (s & THROWN) == 0 ? new CancellationException() : getThrowableException()); }🌍入门案例场景: 生成一个计算任务,计算 1+2+3.........+1000,每 100 个数切分一个 子任务import java.util.concurrent.RecursiveTask; /** * 递归累加 */ public class TaskExample extends RecursiveTask<Long> { private int start; private int end; private long sum; /** * 构造函数 * @param start * @param end */ public TaskExample(int start, int end){ this.start = start; this.end = end; } @Override protected Long compute() { System.out.println("任务" + start + "=========" + end + "累加开始"); //大于 100 个数相加切分,小于直接加 if(end - start <= 100){ for (int i = start; i <= end; i++) { //累加 sum += i; } }else { //切分为 2 块 int middle = start + 100; //递归调用,切分为 2 个小任务 TaskExample taskExample1 = new TaskExample(start, middle); TaskExample taskExample2 = new TaskExample(middle + 1, end); //执行:异步 taskExample1.fork(); taskExample2.fork(); //同步阻塞获取执行结果 sum = taskExample1.join() + taskExample2.join(); } //加完返回 return sum; } }import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; /** * 分支合并案例 */ public class ForkJoinPoolDemo { /** * 生成一个计算任务,计算 1+2+3.........+1000 * @param args */ public static void main(String[] args) { //定义任务 TaskExample taskExample = new TaskExample(1, 1000); //定义执行对象 ForkJoinPool forkJoinPool = new ForkJoinPool(); //加入任务执行 ForkJoinTask<Long> result = forkJoinPool.submit(taskExample); //输出结果 try { System.out.println(result.get()); }catch (Exception e){ e.printStackTrace(); }finally { forkJoinPool.shutdown(); } } }🌈自言自语最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。希望与君共勉😁我们:待别时相见时,都已有所成。
多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!沉下去,再浮上来,我想我们会变的不一样的。关于封面:一个非常喜欢的女孩子拍的照片作者:次辣条吗JUC系列JUC系列(一) 什么是JUC?JUC系列(二) 回顾Synchronized关键字JUC系列(三)Lock 锁机制详解 代码理论相结合JUC系列(四)集合的线程安全问题JUC系列(五)| Synchonized关键字的进一步讲解JUC系列(六) | Callable和Future接口详解&使用、FutureTask应用JUC系列(七)| JUC三大常用工具类CountDownLatch、CyclicBarrier、SemaphoreJUC系列(八)| 读写锁-ReadWriteLock正在持续更新中...一、读写锁1)概述:我们开发中在大都数场景中,都是遇到这样的一个场景,对一个资源而言,读的次数往往比比写的次数要多, 在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是当一个写者线程在写这些共享资源时,就不允许其他线程进行访问。针对这种场景,Java的并发包下提供了读写锁 ReadWriteLock(接口) | ReentrantReadWriteLock(实现类)。读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。我们将读操作相关的锁,称为读锁,因为可以共享读,我们也称为“共享锁”,将写操作相关的锁,称为写锁、排他锁、独占锁。每次可以多个线程的读者进行读访问,但是一次只能由一个写者线程进行写操作,即写操作是独占式的。读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁。public interface ReadWriteLock { // 读锁 Lock readLock(); // 写锁 Lock writeLock(); }ReentrantReadWriteLock这个得自己去看哈,这里给出一个整体架构哈😁。public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { /** 读锁 */ private final ReentrantReadWriteLock.ReadLock readerLock; /** 写锁 */ private final ReentrantReadWriteLock.WriteLock writerLock; final Sync sync; /** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */ public ReentrantReadWriteLock() { this(false); } /** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */ public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } /** 返回用于写入操作的锁 */ public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } /** 返回用于读取操作的锁 */ public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } abstract static class Sync extends AbstractQueuedSynchronizer {} static final class NonfairSync extends Sync {} static final class FairSync extends Sync {} public static class ReadLock implements Lock, java.io.Serializable {} public static class WriteLock implements Lock, java.io.Serializable {} }2)使用相关:当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞,因为写锁是独占锁.当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但如果以写模式尝试对此锁进行加锁, 它必须等到所有的线程释放锁.如果线程想要进入读锁的前提条件:不存在其他线程的写锁没有写请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)线程进入写锁的前提条件:没有读者线程正在访问没有其他写者线程正在访问通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求长期阻塞.3)特点:🛫公平选择性:非公平模式(默认)当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。公平模式当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁。🛬可重入读锁和写锁都支持线程重进入。但是写锁可以获得读锁,读锁不能获得写锁。因为读锁是共享的,写锁是独占式的。💺锁降级遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为 读锁。🚤支持中断锁的获取在读锁和写锁的获取过程中支持中断🛸监控提供一些辅助方法,例如hasQueuedThreads方法查询是否有线程正在等待获取读锁或写锁、isWriteLocked方法查询写锁是否被任何线程持有等等二、案例实现一个特别简单的案例哈。🍟代码场景: 使用 ReentrantReadWriteLock 对一个 hashmap 进行读和写操作package com.crush.juc06; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; //资源类 class ReentrantReadWriteLockDemo{ //创建 map 集合 private volatile Map<String, Object> map = new HashMap<>(); //创建读写锁对象 private ReadWriteLock rwLock = new ReentrantReadWriteLock(); //放数据 public void put(String key, Object value) { //添加写锁 rwLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + "正在读数据" + key); //暂停一会 TimeUnit.MICROSECONDS.sleep(300); //放数据 map.put(key, value); System.out.println(Thread.currentThread().getName() + "读完了" + key); } catch (InterruptedException e) { e.printStackTrace(); } finally { //释放写锁 rwLock.writeLock().unlock(); } } //取数据 public Object get(String key) { //添加读锁 rwLock.readLock().lock(); Object result = null; try { System.out.println(Thread.currentThread().getName() + "正在取数据" + key); //暂停一会 TimeUnit.MICROSECONDS.sleep(300); result = map.get(key); System.out.println(Thread.currentThread().getName() + "取完数据了" + key); } catch (InterruptedException e) { e.printStackTrace(); } finally { //释放读锁 rwLock.readLock().unlock(); } return result; } public static void main(String[] args) { ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo(); for (int i = 1; i <= 5; i++) { final int number = i; new Thread(() -> { demo.put(String.valueOf(number), number); }, String.valueOf(i)).start(); } for (int i = 1; i <= 5; i++) { final int number = i; new Thread(() -> { demo.get(String.valueOf(number)); }, String.valueOf(i)).start(); } } } /** 5正在进行写操作5 5写完了5 4正在进行写操作4 4写完了4 3正在进行写操作3 3写完了3 2正在进行写操作2 2写完了2 1正在进行写操作1 1写完了1 1正在取数据1 4正在取数据4 3正在取数据3 5正在取数据5 2正在取数据2 1取完数据了1 4取完数据了4 2取完数据了2 5取完数据了5 3取完数据了3 */写是唯一的,而读的时候是共享的。🍔小总结ReentrantReadWriteLock和Synchonized、ReentrantLock比较起来有哪些区别呢?或者有哪些优势呢?Synchonized、ReentrantLock是属于独占锁,不管是读操作还是写操作,都只能一个人进行访问,这样导致效率极低。而ReentrantReadWriteLock读操作可以共享,而写操作还是每次一个人访问,这样的情况下,性能方面比起独占锁就要好的多。当然ReentrantReadWriteLock优势是有,但是也存在一些缺陷,容易造成锁饥饿,因为如果是读线程先拿到锁的话,并且后续有很多读线程,但只有一个写线程,很有可能这个写线程拿不到锁,它可能要等到所有读线程读完才能进入,就可能会造成一种一直读,没有写的现象。三、锁降级🍜概念:锁降级的意思就是写锁降级为读锁。而读锁是不可以升级为写锁的。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程,最后释放读锁的过程。编程模型:获取写锁--->获取读锁--->释放写锁--->释放读锁简单的代码:/** * @Author: crush * @Date: 2021-08-21 9:04 * version 1.0 */ public class ReadWriteLockDemo2 { public static void main(String[] args) { ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); // 获取读锁 ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); // 获取写锁 ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); //1、 获取到写锁 writeLock.lock(); System.out.println("获取到了写锁"); //2、 继续获取到写锁 readLock.lock(); System.out.println("继续获取到读锁"); //3、释放写锁 writeLock.unlock(); //4、 释放读锁 readLock.unlock(); } } /** * 获取到了写锁 * 继续获取到读锁 */也许大家觉得看不出什么,但是如果将获取读锁那一行代码调到获取写锁上方去,可能结果就完全不一样拉。public class ReadWriteLockDemo2 { public static void main(String[] args) { ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); // 获取读锁 ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); // 获取写锁 ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); //1、 获取到读锁 readLock.lock(); System.out.println("获取到了读锁"); writeLock.lock(); System.out.println("继续获取到写锁"); writeLock.unlock(); readLock.unlock(); // 释放写锁 } }🍿原因:为什么会出现上面这一幕呢?因为在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的前提条件是,当前没有读者线程,也没有其他写者线程,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。但是在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。上面就一普通案例,看完确实会有点迷,这只是做个简单证明,下面才是正文哈。😁🌭使用场景:对于数据比较敏感, 需要在对数据修改以后, 获取到修改后的值, 并进行接下来的其它操作我们来看个比较实在的案例:import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class CacheDemo { /** * 缓存器,这里假设需要存储1000左右个缓存对象,按照默认的负载因子0.75,则容量=750,大概估计每一个节点链表长度为5个 * 那么数组长度大概为:150,又有雨设置map大小一般为2的指数,则最近的数字为:128 */ private Map<String, Object> map = new HashMap<>(128); private ReadWriteLock rwl = new ReentrantReadWriteLock(); private Lock writeLock=rwl.writeLock(); private Lock readLock=rwl.readLock(); public static void main(String[] args) { } public Object get(String id) { Object value = null; readLock.lock();//首先开启读锁,从缓存中去取 try { //如果缓存中没有 释放读锁,上写锁 if (map.get(id) == null) { readLock.unlock(); writeLock.lock(); try { //防止多写线程重复查询赋值 if (value == null) { //此时可以去数据库中查找,这里简单的模拟一下 value = "redis-value"; } //加读锁降级写锁,不明白的可以查看上面锁降级的原理与保持读取数据原子性的讲解 readLock.lock(); } finally { //释放写锁 writeLock.unlock(); } } } finally { //最后释放读锁 readLock.unlock(); } return value; } }如果不使用锁降级功能,如先释放写锁,然后获得读锁,在这个获取读锁的过程中,可能会有其他线程竞争到写锁 或者是更新数据 则获得的数据是其他线程更新的数据,可能会造成数据的污染,即产生脏读的问题。🍖锁降级的必要性:锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁, 假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。四、自言自语最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。希望与君共勉😁我们:待别时相见时,都已有所成。参考:并发库应用之五 & ReadWriteLock场景应用读写锁的使用场景及锁降级深入理解读写锁—ReadWriteLock源码分析
多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!沉下去,再浮上来,我想我们会变的不一样的。 🍟我们:待别日相见时,都已有所成。关于封面:忽然之间,你忽略的,我忽略的所有细节当初的猜疑好奇,爱恨痴嗔全已走远JUC系列JUC系列(一) 什么是JUC?JUC系列(二) 回顾Synchronized关键字JUC系列(三)Lock 锁机制详解 代码理论相结合JUC系列(四)集合的线程安全问题JUC系列(五)| Synchonized关键字的进一步讲解JUC系列(六) | Callable和Future接口详解&使用、FutureTask应用JUC系列(七)| JUC三大常用工具类CountDownLatch、CyclicBarrier、SemaphoreJUC系列(八)| 读写锁-ReadWriteLock正在持续更新中...JUC实际辅助类有五个,标题中三个最为常用。剩下未指明的分别为:Phaser、Exchanger。稍后会做简单讲解。一、🎈CountDownLatch(减计数器)1)概述:CountDownLatch位于 java.util.concurrent包下。CountDownLatch是一个同步辅助类,允许一个或多个线程等待,一直到其他线程执行的操作完成后再执行。CountDownLatch是通过一个计数器来实现的,计数器的初始值是线程的数量。每当有一个线程执行完毕后,然后通过 countDown 方法来让计数器的值-1,当计数器的值为0时,表示所有线程都执行完毕,然后继续执行 await 方法 之后的语句,即在锁上等待的线程就可以恢复工作了。CountDownLatch中主要有两个方法:countDown:递减锁存器的计数,如果计数达到零,则释放所有等待的线程。如果当前计数大于零,则递减。 如果新计数为零,则为线程调度目的重新启用所有等待线程。如果当前计数为零,则什么也不会发生。public void countDown() { sync.releaseShared(1); }await:使当前线程等待直到闩锁倒计时为零,除非线程被中断。如果当前计数为零,则此方法立即返回。即await 方法阻塞的线程会被唤醒,继续执行如果当前计数大于零,则当前线程出于线程调度目的而被禁用并处于休眠状态public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }2)案例:举个生活中的小例子:我们一寝室人去上课,得等到1、2、3、4、5、6、7、8个人都出来,才可以锁上寝室门吧。即当计数器值为0时,就可以执行await的方法啦。编码步骤:CountDownLatch countDownLatch = new CountDownLatch(8);countDownLatch.countDown(); 一个线程出来一个人,计数器就 -1countDownLatch.await(); 阻塞的等待计数器归零执行后续步骤我们用代码来模拟一下这个例子哈:/** * @Author: crush * @Date: 2021-08-19 23:21 * version 1.0 */ public class CountDownLatchDemo1 { public static void main(String[] args) { // 初始值8 有八个人需要出寝室门 CountDownLatch countDownLatch = new CountDownLatch(8); for (int i = 1; i <= 8; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "出去啦"); // 出去一个人计数器就减1 countDownLatch.countDown(); }, String.valueOf(i)).start(); } try { countDownLatch.await(); // 阻塞等待计数器归零 } catch (InterruptedException e) { e.printStackTrace(); } // 阻塞的操作 : 计数器 num++ System.out.println(Thread.currentThread().getName() + "====寝室人都已经出来了,关门向教室冲!!!===="); } }3)小结:CountDownLatch使用给定的计数进行初始化。 由于调用了countDown方法,每次-1, await方法会一直阻塞到当前计数达到零,然后释放所有等待线程,并且任何后续的await调用都会立即返回。 这是一种一次性现象——计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier 。CountDownLatch一个有用属性是它不需要调用countDown线程在继续之前等待计数达到零,它只是阻止任何线程通过await,直到所有线程都可以通过。二、🎀CyclicBarrier(加法计数器)2.1、概述:CyclicBarrier 看英文单词就可以看出大概就是循环阻塞的意思。所以还常称为循环栅栏。CyclicBarrier 主要方法有:public class CyclicBarrier { private int dowait(boolean timed, long nanos); // 供await方法调用 判断是否达到条件 可以往下执行吗 //创建一个新的CyclicBarrier,它将在给定数量的参与方(线程)等待时触发,每执行一次CyclicBarrier就累加1,达到了parties,就会触发barrierAction的执行 public CyclicBarrier(int parties, Runnable barrierAction) ; //创建一个新的CyclicBarrier ,参数就是目标障碍数,它将在给定数量的参与方(线程)等待时触发,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句 public CyclicBarrier(int parties) //返回触发此障碍所需的参与方数量。 public int getParties() //等待,直到所有各方都在此屏障上调用了await 。 // 如果当前线程不是最后一个到达的线程,那么它会出于线程调度目的而被禁用并处于休眠状态.直到所有线程都调用了或者被中断亦或者发生异常中断退出 public int await() // 基本同上 多了个等待时间 等待时间内所有线程没有完成,将会抛出一个超时异常 public int await(long timeout, TimeUnit unit) //将障碍重置为其初始状态。 public void reset() }public CyclicBarrier(int parties):的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后 的语句。可以将 CyclicBarrier 理解为加 1 操作。public CyclicBarrier(int parties, Runnable barrierAction) :的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,就会执行我们传入的Runnable;2.2、案例:我想大家多少玩过王者荣耀吧,里面不是有个钻石夺宝吗,抽201次必得荣耀水晶,这次让我们用代码来模拟一下吧。编程步骤:创建CyclicBarrier对象CyclicBarrier cyclicBarrier = new CyclicBarrier(count, new MyRunnable());编写业务代码cyclicBarrier.await(); //在线程里面等待阻塞,累加1,达到最大值count时,触发我们传入进去MyRunnable执行。/** * @Author: crush * @Date: 2021-08-20 13:18 * version 1.0 */ public class CyclicBarrierDemo1 { public static void main(String[] args) { // 第一个参数:目标障碍数 第二个参数:一个Runnable任务,当达到目标障碍数时,就会执行我们传入的Runnable // 当我们抽了201次的时候,就会执行这个任务。 CyclicBarrier cyclicBarrier = new CyclicBarrier(201,()->{ System.out.println("恭喜你,已经抽奖201次,幸运值已满,下次抽奖必中荣耀水晶!!!"); }); for (int i=1;i<=201;i++){ final int count=i; new Thread(()->{ System.out.println(Thread.currentThread().getName()+"抽奖一次"); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } // 这行代码是重置计数 cyclicBarrier.reset(); // 这里是我又加了 一次循环, 可以看到最后结果中输出了两次 "恭喜你" for (int i=1;i<=201;i++){ final int count=i; new Thread(()->{ System.out.println(Thread.currentThread().getName()+"抽奖一次"); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } }2.3、小结:CyclicBarrier和CountDownLatch其实非常相似,CyclicBarrier表示加法,CountDownLatch表示减法。区别还是有的:CyclicBarrier只能够唤醒一个任务,CountDownLatch可以唤起多个任务。CyclicBarrier可以重置,重新使用,但是CountDownLatch的值等于0时,就不可重复用了。三、🩰Semaphore( 信号灯)📍、概述:Semaphore:信号量通常用于限制可以访问某些(物理或逻辑)资源的线程数。使用场景:限制资源,如抢位置、限流等。🎃、案例:【例子】:不知道大家有没有过在网吧抢电脑打游戏的那种经历,小时候,平常便宜点的网吧都比较小,而且也比较少,特别多的人去,去晚了的人就只有站在那里看,等别人下机才能上网。这次的例子就是:网吧有十台高配置打游戏的电脑,有20个小伙伴想要上网。我们用代码来模拟一下:编程步骤:创建信号灯Semaphore semaphore = new Semaphore(10); // 5个位置等待获取信号灯semaphore.acquire();//等待获取许可证业务代码释放信号semaphore.release();//释放资源,女朋友来找了,下机下机,陪女朋友去了,那么就要释放这台电脑啦/** * @Author: crush * @Date: 2021-08-20 14:03 * version 1.0 */ public class SemaphoreDemo1 { public static void main(String[] args) { // 10台电脑 Semaphore semaphore = new Semaphore(10); // 20 个小伙伴想要上网 for (int i = 1; i <= 20; i++) { new Thread(() -> { try { //等待获取许可证 semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "抢到了电脑"); //抢到的小伙伴,迅速就开打啦 这里就模拟个时间哈, TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } finally { //打完几把游戏的小伙伴 女朋友来找了 溜啦溜啦 希望大家都有人陪伴 System.out.println("女朋友来找,"+Thread.currentThread().getName() + "离开了"); semaphore.release();//释放资源,离开了就要把电脑让给别人啦。 } }, String.valueOf(i)).start(); } } }🎠、小结:在获得一个项目之前,每个线程必须从信号量中获得一个许可,以保证一个项目可供使用。 当线程完成该项目时,它会返回到池中,并且将许可返回给信号量,允许另一个线程获取该项目。 请注意,调用acquire时不会持有同步锁,因为这会阻止项目返回到池中。 信号量封装了限制访问池所需的同步,与维护池本身一致性所需的任何同步分开。初始化为 1 的信号量,并且使用时最多只有一个许可可用,可以用作互斥锁。 这通常被称为二进制信号量,因为它只有两种状态:一种许可可用,或零许可可用。 以这种方式使用时,二进制信号量具有属性(与许多java.util.concurrent.locks.Lock实现不同),即“锁”可以由所有者以外的线程释放(因为信号量没有所有权的概念)。 这在某些特定上下文中很有用,例如死锁恢复。此类的构造函数可以选择接受公平参数。 当设置为 false 时,此类不保证线程获取许可的顺序。 当公平性设置为真时,信号量保证调用任何acquire方法的线程被选择以按照它们对这些方法的调用的处理顺序(先进先出;FIFO)获得许可。通常,用于控制资源访问的信号量应初始化为公平的,以确保没有线程因访问资源而饿死。 当使用信号量进行其他类型的同步控制时,非公平排序的吞吐量优势通常超过公平性考虑。内存一致性影响:在调用“释放”方法(如release()之前线程中的操作发生在另一个线程中成功的“获取”方法(如acquire()之后的操作之前。四、🏏简单讲述 | Phaser & Exchanger4.1、PhaserPhaser一种可重用的同步屏障,功能上类似于CyclicBarrier和CountDownLatch,但使用上更为灵活。非常适用于在多线程环境下同步协调分阶段计算任务(Fork/Join框架中的子任务之间需同步时,优先使用Phaser)//默认的构造方法,初始化注册的线程数量为0,可以动态注册 Phaser(); //指定了线程数量的构造方法 Phaser(int parties); //添加一个注册者 向此移相器添加一个新的未到达方。 如果正在进行对onAdvance调用,则此方法可能会在返回之前等待其完成。 register(); //添加指定数量的注册者 将给定数量的新未到达方添加到此移相器(移相器就是Phaser)。 bulkRegister(int parties); // 到达屏障点直接执行 无需等待其他人到达。 arrive(); //到达屏障点后,也必须等待其他所有注册者到达这个屏障点才能继续下一步 arriveAndAwaitAdvance(); //到达屏障点,把自己注销了,不用等待其他的注册者到达 arriveAndDeregister(); //多个线程达到注册点之后,会回调这个方法,可以做一些逻辑的补充 onAdvance(int phase, int registeredParties);package com.crush.juc05; import java.util.concurrent.Phaser; public class PhaserDemo { private static Phaser phaser = new MyPhaser(); //自定义一个移相器来自定义输出 static class MyPhaser extends Phaser { /** * @deprecated 在即将到来的阶段提前时执行操作并控制终止的可覆盖方法。 此方法在推进此移相器的一方到达时调用(当所有其他等待方处于休眠状态时)。 * 如果此方法返回true ,则此移相器将在提前时设置为最终终止状态,并且对isTerminated后续调用将返回 true。 * @param phase 进入此方法的当前阶段号,在此移相器前进之前 * @param registeredParties 当前注册方的数量 * @return */ @Override protected boolean onAdvance(int phase, int registeredParties) { if (phase == 0) { System.out.println("所有人都到达了网吧,准备开始开黑!!!"); return false; } else if (phase == 1) { System.out.println("大家都同意,一起去次烧烤咯!!!"); return false; } else if (phase == 2) { System.out.println("大家一起回寝室!!!"); return true; } return true; } } //构建一个线程任务 static class DoSomeThing implements Runnable { @Override public void run() { /** * 向此移相器添加一个新的未到达方 */ phaser.register(); System.out.println(Thread.currentThread().getName() + "从家里出发,准备去学校后街上网开黑!!!"); phaser.arriveAndAwaitAdvance(); System.out.println(Thread.currentThread().getName() + "上着上着饿了,说去次烧烤吗?"); phaser.arriveAndAwaitAdvance(); System.out.println(Thread.currentThread().getName() + "烧烤次完了"); phaser.arriveAndAwaitAdvance(); } } public static void main(String[] args) throws Exception { DoSomeThing thing = new DoSomeThing(); new Thread(thing, "小明").start(); new Thread(thing, "小王").start(); new Thread(thing, "小李").start(); } } /** * 小李从家里出发,准备去学校后街上网开黑!!! * 小王从家里出发,准备去学校后街上网开黑!!! * 小明从家里出发,准备去学校后街上网开黑!!! * 所有人都到达了网吧,准备开始开黑!!! * 小李上着上着饿了,说去次烧烤吗? * 小明上着上着饿了,说去次烧烤吗? * 小王上着上着饿了,说去次烧烤吗? * 大家都同意,一起去次烧烤咯!!! * 小明烧烤次完了 * 小李烧烤次完了 * 小王烧烤次完了 * 大家一起回寝室!!! */注意:这里只是做了简单的一个使用,更深入的了解,我暂时也没有,想要研究可以去查一查。4.2、ExchangerExchanger允许两个线程在某个汇合点交换对象,在某些管道设计时比较有用。Exchanger提供了一个同步点,在这个同步点,一对线程可以交换数据。每个线程通过exchange()方法的入口提供数据给他的伙伴线程,并接收他的伙伴线程提供的数据并返回。当两个线程通过Exchanger交换了对象,这个交换对于两个线程来说都是安全的。Exchanger可以认为是 SynchronousQueue 的双向形式,在运用到遗传算法和管道设计的应用中比较有用。这个的使用我在Dubbo中的总体架构图中看到了它的身影。五、🎡自言自语最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。希望与君共勉😁待我们,别时相见时,都已有所成。希望再遇见参考:JUC并发编程(八)-JUC常用辅助类
多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!沉下去,再浮上来,我想我们会变的不一样的。喜欢封面的云,就是不知道你喜不喜欢JUC系列JUC系列(一) 什么是JUC?JUC系列(二) 回顾Synchronized关键字JUC系列(三)Lock 锁机制详解 代码理论相结合JUC系列(四)集合的线程安全问题JUC系列(五)| Synchonized关键字的进一步讲解JUC系列(六) | Callable和Future接口详解&使用、FutureTask应用JUC系列(七)| JUC三大常用工具类CountDownLatch、CyclicBarrier、SemaphoreJUC系列(八)| 读写锁-ReadWriteLock正在持续更新中...一、Callable 接口🚀1)前言:在上上篇文章中,创建线程那个小角落,提到了这个,但是当时只是匆匆忙忙讲了一下。到这里再全面性的讲解一下。我们以前使用实现Runnable接口的方式来创建线程,但是Runnable的run() 存在一个缺陷问题,就是不能将执行完的结果返回。Java就是为了能够实现这个功能,在jdk1.5中提出了Callable接口。2)概述:Callable 接口位于java.util.concurrent包下。@FunctionalInterface public interface Callable<V> { V call() throws Exception; //计算结果,如果无法计算则抛出异常。 }Callable 类似于Runnable 接口,但Runnable 接口中的run()方法不会返回结果,并且也无法抛出经过检查的异常,但是Callable中call()方法能够返回计算结果,并且也能够抛出经过检查的异常。3)实现:通过实现Callable接口创建线程详细步骤:Runnable直接看代码就知道了哈。创建实现Callable接口的类SomeCallable创建一个类对象:Callable oneCallable = new SomeCallable();由Callable创建一个FutureTask对象:FutureTask futureTask= new FutureTask(oneCallable);注释:FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。由FutureTask创建一个Thread对象:Thread oneThread = new Thread(futureTask);启动线程:oneThread.start();/** * @Author: crush * @Date: 2021-08-19 16:14 * version 1.0 */ public class Demo1 { public static void main(String[] args) { new Thread(new RunnableDemo1(),"AA").start(); FutureTask<Integer> futureTask = new FutureTask<>(new CallableDemo<Integer>()); new Thread(futureTask,"BB").start(); // 在线程执行完后,我们可以通过futureTask的get方法来获取到返回的值。 System.out.println(futureTask.get()); } } class RunnableDemo1 implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"::通过实现Runnable来执行任务"); } } class CallableDemo<Integer> implements Callable<java.lang.Integer> { @Override public java.lang.Integer call() throws Exception { System.out.println(Thread.currentThread().getName()+"::通过实现Callable接口来执行任务,并返回结果!"); return 1024; } } /** * AA::通过实现Runnable来执行任务 * BB::通过实现Callable接口来执行任务,并返回结果! * 1024 */这里之所以要转成 FutureTask 放进 Thread中去,是因为Callable 本身与Thread没有关系,通过FutureTask 才能和Thread产生联系。二、Future 接口 🛸2.1、概述:Future 接口同样位于java.util.concurrent包下。Future接口提供方法来检测任务是否被执行完,等待任务执行完获得结果,也可以设置任务执行的超时时间。这个设置超时的方法就是实现Java程序执行超时的关键。public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); //尝试取消此任务的执行。 boolean isCancelled();//如果此任务在正常完成之前被取消,则返回true boolean isDone(); //如果此任务完成,则返回true 。 完成可能是由于正常终止、异常或取消——在所有这些情况下,此方法将返回true V get() throws InterruptedException, ExecutionException; //获得任务计算结果 V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;//可等待多少时间去获得任务计算结果 }2.2、实现:Future模式通俗点来描述就是:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从Future那儿取出结果。/** * @Author: crush * @Date: 2021-08-19 16:14 * version 1.0 */ public class Demo1 { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> futureTask = new FutureTask<>(new CallableDemo<Integer>()); new Thread(futureTask,"BB").start(); System.out.println(futureTask.get()); // 我们来测试一下任务是否已经完成 System.out.println(futureTask.isDone()); } } class CallableDemo<Integer> implements Callable<java.lang.Integer> { @Override public java.lang.Integer call() throws Exception { System.out.println(Thread.currentThread().getName()+"::通过实现Callable接口来执行任务,并返回结果!"); return 1024; } }Future 用于存储从另一个线程获得的结果。如果只是简单创建线程,直接使用Runnable就可以,想要获得任务返回值,就用Future。三、 FutureTask 🚁3.1、FutureTask介绍位于 java.util.concurrent包下。可取消的异步计算。 此类提供Future的基本实现,具有启动和取消计算、查询以查看计算是否完成以及检索计算结果的方法。 计算完成后才能检索结果; 如果计算尚未完成, get方法将阻塞。 一旦计算完成,就不能重新开始或取消计算(除非使用runAndReset调用计算)。结构图:FutureTask实现了 Runnable 和 Future接口,并方便地将两种功能组合在一起。并且通过构造函数提供Callable来创建FutureTask,就可以提供给Thread来创建线程啦。FutureTask有以下三种状态:未启动状态:还未执行run()方法。已启动状态:已经在执行run()方法。完成状态:已经执行完run()方法,或者被取消了,亦或者方法中发生异常而导致中断结束。3.2、FutureTask应用场景及注意事项应用场景:在主线程执行那种比较耗时的操作时,但同时又不能去阻塞主线程时,就可以将这样的任务交给FutureTask对象在后台完成,然后等之后主线程需要的时候,就可以直接get()来获得返回数据或者通过isDone()来获得任务的状态。一般FutureTask多应用于耗时的计算,这样主线程就可以把一个耗时的任务交给FutureTask,然后等到完成自己的任务后,再去获取计算结果注意:仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计 算完成,就不能再重新开始或取消计算。get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。因为只会计算一次,因此通常get方法放到最后。使用放在下一小节啦👇四、使用 Callable 和 Future🚩这里的使用其实在上文已经提到过了,这里就将其更完善一些吧。/** * @Author: crush * @Date: 2021-08-19 18:44 * version 1.0 */ public class CallableDemo2 { public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException { CallableAndFutureTest callableAndFutureTest = new CallableAndFutureTest(); FutureTask<String> task = new FutureTask<>(callableAndFutureTest); new Thread(task).start(); // System.out.println("尝试取消任务,传true表示取消任务,false则不取消任务::"+task.cancel(true)); System.out.println("判断任务是否已经完成::"+task.isDone()); //结果已经计算出来,则立马取出来,如若摸没有计算出来,则一直等待,直到结果出来,或任务取消或发生异常。 System.out.println("阻塞式获取结果::"+task.get()); System.out.println("在获取结果时,给定一个等待时间,如果超过等待时间还未获取到结果,则会主动抛出超时异常::"+task.get(2, TimeUnit.SECONDS)); } } class CallableAndFutureTest implements Callable<String> { @Override public String call() throws Exception { String str=""; for (int i=0;i<10;i++){ str+=String.valueOf(i); Thread.sleep(100); } return str; } }还有很多没玩到位,大家可以继续尝试哈。五、自言自语⛵最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。希望与君共勉😁我们:待别时相见时,都已有所成。
多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!沉下去,再浮上来,我想我们会变的不一样的。JUC系列JUC系列(一) 什么是JUC?JUC系列(二) 回顾Synchronized关键字JUC系列(三)Lock 锁机制详解 代码理论相结合JUC系列(四)集合的线程安全问题JUC系列(五)| Synchonized关键字的进一步讲解JUC系列(六) | Callable和Future接口详解&使用、FutureTask应用JUC系列(七)| JUC三大常用工具类CountDownLatch、CyclicBarrier、Semaphore正在持续更新中...我想我们大家肯定都使用过ArrayList的吧。不知道你之前有没有想过它也会牵扯到线程安全问题勒。一、问题引入:我们一起先看看下面的程序吧,看你能看出什么问题吗?public static void main(String[] args) { List list = new ArrayList(); for (int i = 0; i < 20; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString()); System.out.println(list); }, "线程" + i).start(); } }你觉得能每次都能正常输出吗?答案是否定的,也许好几次运行程序都不会出错,但是偶尔就会遇上一次的。会报一个ConcurrentModificationException的异常,中文名为:并发修改异常。原因:就是我们正在读的时候,正好也遇上了写操作,我们这里又没有同步代码块、锁什么的,那么此时肯定是不可以继续往下执行的。还有ArrayList的add方法并非线程同步的。(jdk11源码)public boolean add(E e) { modCount++; add(e, elementData, size); return true; }我们该如何解决这个问题呢???二、解决方式第一种方式:使用 Vector我们可以使用Vector来代替ArrayList,因为Vector 继承了 AbstractList 类并且实现了List 、RandmoAccess 接口。RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在 Vector 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访 问。public class Vector<E> extends AbstractList<E> implements List<E> ,RandomAccess 我们将上面的程序修改后,程序将不再出现异常。public static void main(String[] args) { List list = new Vector(); for (int i = 0; i < 20; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString()); System.out.println(list); }, "线程" + i).start(); } }原因其实就在 Vector 的代码中。public synchronized boolean add(E e) { modCount++; add(e, elementData, elementCount); return true; }add方法上加了synchronized关键字,让这个方法成为了同步方法块。第二种方式:使用 CollectionsCollections 提供了方法 synchronizedList 保证 list 是同步线程安全的。Collections 仅包含对集合进行操作或返回集合的静态方法,所以我们通常也称Collections 为集合的工具类。public static void main(String[] args) { List list = Collections.synchronizedList(new ArrayList<>()); for (int i = 0; i < 20; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString()); System.out.println(list); }, "线程" + i).start(); } }这样也不会发生异常。源码上也都有体现public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} }大多数方法都提供了同步和不同步两种api。第三种方式: 使用 CopyOnWriteArrayListCopyOnWriteArrayList和ArrayList 一样,它是个可变数组。有以下几个特点:更新操作开销大(add()、set()、remove()等等),因为要复制整个数组是线程安全的。它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多 于可变操作,需要在遍历期间防止线程间的冲突。独占锁效率低:采用读写分离思想写线程获取到锁,其他写线程阻塞复制思想CopyOnWriteArrayList 的思想和原理:当我们要添加一个元素的时候,不直接往当前容器中添加,而是应该先将当前容器复制一份,然后在新的容器中进行添加操作,等到添加完成后,我们再让原容器的引用指向新的容器。当然,这个时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写进内存,那么其他的线程就会读到了脏数据。public static void main(String[] args) { List list = new CopyOnWriteArrayList(); for (int i = 0; i < 20; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString()); System.out.println(list); }, "线程" + i).start(); } }为什么不会产生线程安全问题呢?我们从"动态数组"和“线程安全”两个方面来看待:动态数组机制 :它内部有个volatile 数组(array)来保持数据。它在涉及到更新操作时,都会新建数组,所以CopyOnWriteArrayList效率都会很低;但如果只是单单进行遍历查找的话, 效率是能够达到比较高的。public boolean add(E element) { synchronized (lock) { checkForComodification(); CopyOnWriteArrayList.this.add(offset + size, element); expectedArray = getArray(); size++; } return true; } // CopyOnWriteArrayList.this.add(offset + size, element); public void add(int index, E element) { synchronized (lock) { Object[] es = getArray(); int len = es.length; if (index > len || index < 0) throw new IndexOutOfBoundsException(outOfBounds(index, len)); Object[] newElements; int numMoved = len - index; if (numMoved == 0) newElements = Arrays.copyOf(es, len + 1); else { newElements = new Object[len + 1]; System.arraycopy(es, 0, newElements, 0, index); System.arraycopy(es, index, newElements, index + 1, numMoved); } newElements[index] = element; setArray(newElements); } }线程安全机制:通过 volatile 和互斥锁(synchronized)来实现的。/** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array;通过“volatile 数组”来保存数据的一个线程读取 volatile 数组时,总能看 到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读 取到的数据总是最新的”这个机制的保证。通过互斥锁来保护数据在更新操作时,都会率先去获取互斥锁, 在修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,这样就能够保证数据的安全。另外补充: 除了ArrayList是线程不安全的,还有HashMap、HashSet都是不安全的。 HashMap、HashSet的解决方式可以用Hashtable解决,还有CopyOnWriteArraySet解决,当然不局限于这一种哈,(还没看完😂) HashMap还可以用ConcurrentHashMap解决。三、自言自语最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。希望与君共勉😁待我们,别时相见时,都已有所成。
多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!沉下去,再浮上来,我想我们会变的不一样的。先看张图,舒缓下心情,再继续吧JUC系列JUC系列(一) 什么是JUC?JUC系列(二) 回顾Synchronized关键字JUC系列(三)Lock 锁机制详解 代码理论相结合JUC系列(四)集合的线程安全问题JUC系列(五)| Synchonized关键字的进一步讲解JUC系列(六) | Callable和Future接口详解&使用、FutureTask应用JUC系列(七)| JUC三大常用工具类CountDownLatch、CyclicBarrier、SemaphoreJUC系列(八)| 读写锁-ReadWriteLock正在持续更新中...一、JUC简介JUC实际上就是我们对于jdk中java.util .concurrent 工具包的简称。这个包下都是Java处理线程相关的类,自jdk1.5后出现。二、进程与线程2.1、进程概述:进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的 描述,进程是程序的实体。定义:狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。2.2、线程线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之 中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流, 一个进程中可以并发多个线程,每条线程并行执行不同的任务。1、线程是独立调度和分派的基本单位。2、同一进程中的多条线程将共享该进程中的全部系统资源。3、一个进程可以有很多线程,每条线程并行执行不同的任务。可并发执行。当然,我们Java都知道的线程的同步是Java多线程编程的难点,因为要给哪些地方是共享资源(竞争资源),什么时候要考虑同步等,都是编程中的难点。😭所以才有了那么多滴锁,看到烦人。2.3、创建线程的三种常见方式通过实现Runnable接口来创建Thread线程public class TheardCreateDemo { public static void main(String[] args) { Runnable runnable = new SomeRunnable(); Thread thread1 = new Thread(runnable); thread1.start(); //lamda表达式方式 Thread thread2 = new Thread(() -> { System.out.println("使用lamda表达式方式"); }); thread2.start(); } } class SomeRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()+":: 通过实现Runnable接口来创建Thread线程"); } }2.通过继承Thread类来创建一个线程public class TheardCreateDemo { public static void main(String[] args) { SomeThread thread = new SomeThread(); thread.start(); } } class SomeThread extends Thread{ @Override public void run() { System.out.println("通过继承Thread类来创建一个线程"); } }3.通过实现Callable接口来创建Thread线程public class TheardCreateDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Object> futureTask = new FutureTask<Object>(new SomeCallable<Object>()); Thread oneThread = new Thread(futureTask); oneThread.start(); // 这里可以在运行之后获得 返回值 System.out.println(futureTask.get()); } } class SomeCallable<Object> implements Callable<Object> { @Override public Object call() throws Exception { System.out.println("通过实现Callable接口来创建Thread线程"); // 这个是可以返回数据的 这里就随便返回个 1024 哈 return (Object)" 这个是可以返回数据的 这里就随便返回个哈"; } }这里之后有篇文章还会讲到这个点的。这里稍微讲下步骤:} 步骤1:创建实现Callable接口的类SomeCallable步骤2:创建一个类对象:Callable oneCallable = new SomeCallable();步骤3:由Callable创建一个FutureTask对象:FutureTask futureTask= new FutureTask(oneCallable);注释:FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。 步骤4:由FutureTask创建一个Thread对象:Thread oneThread = new Thread(futureTask);步骤5:启动线程:oneThread.start();这里为什么Thread(FutureTask futureTask)可以勒?Thread的构造函数:public Thread(Runnable target) { this(null, target, "Thread-" + nextThreadNum(), 0); }原因:因为套娃套到一起啦。😁public class FutureTask<V> implements RunnableFuture<V> public interface RunnableFuture<V> extends Runnable, Future<V> 第四种还有线程池的创建方式,之后会讲到,就不再这里增加篇幅啦。三、并发和并行在了解并发和并行之前,让我们先来看一看串行是什么样的吧。1)串行模式:串行模式:即表示所有任务都是按先后顺序进行。串行是一次只能取的一个任务,并执行这个任务。举个生活中的小例子:就是在火车站买票,今天只开放这一个窗口卖票,那么我们只有等到前面的人都买了,才能轮到我们去买。即按先后顺序买到票。2)并行模式:概述:一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。对比地,并发是指:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)。并行模式:并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。我们还是用上面那个例子:还是在买票,以前是只有一个窗口卖票,但是近几年发展起来了,现在有五个窗口卖票啦,大大缩短了人们买票的时间。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。不过并行的效率,一方面受多进程/线程编码的好坏的影响,另一方面也受硬件角度上的CPU的影响。3)并发:并发:并发指的是多个程序可以同时运行的一种现象,并发的重点在于它是一种现象,并发描述的是多进程同时运行的现象。但真正意义上,一个单核心CPU任一时刻都只能运行一个线程。所以此处的"同时运行"表示的不是真的同一时刻有多个线程运行的现象(这是并行的概念),而是提供了一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占 CPU 的,而是根据CPU的调度,执行一会儿停一会儿。4)小小的总结一下:并发:即同一时刻多个线程在访问同一个资源,多个线程对一个点例子:秒杀活动、12306抢回家的票啦、抢演唱会的票...并行:多个任务一起执行,之后再汇总例子:电饭煲煮饭、用锅炒菜,两个事情一起进行,(最后我们一起干饭啦干饭啦😁)四、用户线程和守护线程用户线程:指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。守护线程:是指在程序运行的时候在后台提供一种通用服务的线程,用来服务于用户线程;不需要上层逻辑介入,当然我们也可以手动创建一个守护线程。(用白话来说:就是守护着用户线程,当用户线程死亡,守护线程也会随之死亡)比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。用一个简单代码来模拟一下:未设置为守护线程时:主线程执行完成了,但是我们自己创建的线程仍然未结束。设置为守护线程后:明显可以看到,当主线程执行完成后,我们设置为守护线程的那个线程也被强制结束了。setDaemon就是设置为是否为守护线程。五、自言自语最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。希望与君共勉😁待我们,别时相见时,都已有所成。
Mybatis官方文档说明处一、搭建数据库环境student 表DROP TABLE IF EXISTS `student_2`; CREATE TABLE `student_2` ( `id` int(10) NOT NULL, `name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `tid` int(10) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `fktid`(`tid`) USING BTREE, CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; INSERT INTO `student_2` VALUES (1, '小明', 1); INSERT INTO `student_2` VALUES (2, '邱ss', 2); INSERT INTO `student_2` VALUES (3, '邱大哥', 3); INSERT INTO `student_2` VALUES (4, '杨大哥', 1); INSERT INTO `student_2` VALUES (5, '杨ss', 2);teacherDROP TABLE IF EXISTS `teacher`; CREATE TABLE `teacher` ( `id` int(10) NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of teacher -- ---------------------------- INSERT INTO `teacher` VALUES (1, '小王老师'); INSERT INTO `teacher` VALUES (2, '小李老师'); INSERT INTO `teacher` VALUES (3, '小黑老师');二、idea 搭建maven 项目 (mybatis-demo)2.1、项目结构2.2、导入依赖<dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> </dependencies>2.3、mysql 配置文件jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm-study?useSSL=false jdbc.username=root jdbc.password=1234562.4、创建pojo 类学生/** * @Author: crush * @Date: 2021-06-17 18:23 * version 1.0 */ public class Student { /** * 学生id */ private Integer id; /** * xueshneg xingming */ private String name; /** * 老师id */ private Integer tid; }老师/** * @Author: crush * @Date: 2021-06-17 18:23 * version 1.0 */ public class Teacher { /** * 老师id */ private Integer id; /** * 老师的姓名 */ private String name; /** * 每个老师是不是有很多学生 */ private List<Student> students; }2.5、写一个mybatis 的工具类/** * @author crush */ public class MybatisUtil { private static SqlSessionFactory sqlSessionFactory; static { try { String resource="mybatis-config.xml"; InputStream inputStream= Resources.getResourceAsStream(resource); sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } public static SqlSession getSession(){ return sqlSessionFactory.openSession(true); } }2.7、 写一个TeacherMapperimport com.crush.pojo.Teacher; import org.apache.ibatis.annotations.Param; import java.util.List; public interface TeacherMapper { // 获取老师 List<Teacher> getTeacher(); //获取指定老师下的所有学生及老师的信息 Teacher getTeacher2(@Param("tid") Integer id); //获取指定老师下的所有学生及老师的信息 Teacher getTeacher3(@Param("tid") Integer id); }写一个TeacherMapper.xml文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.crush.dao.TeacherMapper"> <select id="getTeacher" resultType="Teacher"> select * from teacher </select> <select id="getTeacher2" resultMap="TeacherStudent"> select s.id sid,s.name sname,t.name tname, t.id tid from student_2 s,teacher t where s.tid=t.id and t.id=#{tid} </select> <resultMap id="TeacherStudent" type="Teacher"> <result property="id" column="tid"/> <result property="name" column="tname"/> <!-- 复杂的属性,我们需要单独处理 对象 association 集合collection javaType ="" 是指属性的类型 集合中的泛型的信息 我们使用ofType 获取 --> <collection property="students" ofType="Student"> <result property="id" column="sid"/> <result property="name" column="sname"/> <result property="tid" column="tid"/> </collection> </resultMap> <!--========================= 结果集映射=============================--> <select id="getTeacher3" resultMap="TeacherStudent3"> select * from teacher where id=#{tid} </select> <resultMap id="TeacherStudent3" type="Teacher"> <collection property="students" javaType="ArrayList" ofType="Student" select="getStudentByTeacherId" column="id" /> </resultMap> <select id="getStudentByTeacherId" resultType="Student"> select * from student_2 where tid=#{tid} </select> </mapper>2.8、mybatis-config.xml 文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--属性--> <properties resource="dbconfig.properties"/> <!--设置--> <settings> <!-- STDOUT_LOGGING 打印到控制台 --> <setting name="logImpl" value="STDOUT_LOGGING"/> <!-- 开启驼峰 --> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <!--别名--> <typeAliases> <!--这是 自己 取别名--> <typeAlias alias="Student" type="com.crush.pojo.Student"/> <typeAlias alias="Teacher" type="com.crush.pojo.Teacher"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/StudentMapper.xml"/> <mapper resource="mapper/TeacherMapper.xml"/> </mappers> </configuration>测试:/** * @Author: crush * @Date: 2021-06-17 18:22 * version 1.0 */ public class MyTest { @Test public void getTeacher(){ SqlSession session = MybatisUtil.getSession(); TeacherMapper mapper = session.getMapper(TeacherMapper.class); List<Teacher> teacher = mapper.getTeacher(); System.out.println(teacher); session.close(); } @Test public void getTeacher2(){ SqlSession session = MybatisUtil.getSession(); TeacherMapper mapper = session.getMapper(TeacherMapper.class); Teacher teacher2 = mapper.getTeacher2(1); System.out.println(teacher2); session.close(); } @Test public void getTeacher3(){ SqlSession session = MybatisUtil.getSession(); TeacherMapper mapper = session.getMapper(TeacherMapper.class); Teacher teacher2 = mapper.getTeacher3(1); System.out.println(teacher2); session.close(); } }自言自语难啊,加油吧
Java设计模式-观察者模式(订阅发布模式) 一起来看看吧,充实充实自己,为下一阶段做做准备啦。会了就当复习丫,不会来一起来看看吧。很喜欢一句话:“八小时内谋生活,八小时外谋发展”。如果你也喜欢,让我们一起坚持吧!!共勉😁你好,我喜欢你设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言1)引入:在现实世界中,许多对象并不是独立存在的,其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。例如,股票价格与股民、微信公众号与微信用户、气象局的天气预报与听众等。还有上课铃声响了,该进教室啦。在软件世界也是这样,例如,Excel 中的数据与折线图、饼状图、柱状图之间的关系;MVC 模式中的模型与视图的关系;事件模型中的事件源与事件处理者。所有这些,如果用观察者模式来实现就非常方便。2)概述:观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。3)角色结构:Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。4)注意事项:1、避免循环引用。 2、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。二、案例代码2.1、案例:【例】微信公众号在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了XXX这个公众号。类图2.2、实现:定义抽象观察者类,里面定义一个更新的方法public interface Observer { void update(String message); }定义具体观察者类,微信用户是观察者,里面实现了更新的方法public class WeixinUser implements Observer { // 微信用户名 private String name; public WeixinUser(String name) { this.name = name; } @Override public void update(String message) { System.out.println(name + "-" + message); } }定义抽象主题类,提供了attach、detach、notify三个方法public interface Subject { //增加订阅者 public void attach(Observer observer); //删除订阅者 public void detach(Observer observer); //通知订阅者更新消息 public void notify(String message); }微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法public class SubscriptionSubject implements Subject { //储存订阅公众号的微信用户 private List<Observer> weixinUserlist = new ArrayList<Observer>(); @Override public void attach(Observer observer) { weixinUserlist.add(observer); } @Override public void detach(Observer observer) { weixinUserlist.remove(observer); } @Override public void notify(String message) { for (Observer observer : weixinUserlist) { observer.update(message); } } }客户端程序public class Client { public static void main(String[] args) { SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject(); //创建微信用户 WeixinUser user1=new WeixinUser("小明"); WeixinUser user2=new WeixinUser("小王"); WeixinUser user3=new WeixinUser("小李"); //订阅公众号 mSubscriptionSubject.attach(user1); mSubscriptionSubject.attach(user2); mSubscriptionSubject.attach(user3); //公众号更新发出消息给订阅的微信用户 mSubscriptionSubject.notify("宁在春的文章更新啦!!!"); /** * 小明-宁在春的文章更新啦!!! * 小王-宁在春的文章更新啦!!! * 小李-宁在春的文章更新啦!!! */ } }微信公众号一发消息,所有订阅的用户都能接收到。之前写过一篇 SpringBoot整合Redis实现发布/订阅模式 的文章。大家感兴趣可以看一看哈!!!三、总结优点:降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。目标与观察者之间建立了一套触发机制。缺点:1、如果有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。(即没有确认机制)使用场景:对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。实现类似广播机制的功能,不需要知道具体收听者,只需分发广播,系统中感兴趣的对象会自动接收该广播。多层级嵌套使用,形成一种链式触发机制,使得事件具备跨域(跨越两种观察者类型)通知四、自言自语我也不知道文章写出来是有用还是无用,只是想做一个分享。希望大家能够喜欢并且在这里能有收获。当然不可否认,我也想获得那种别人认可的那种快乐,并且能人我继续维之坚持。你好啊,要天天开心哦。下篇文章再见。此系列还在持续更新中.... 我一定还会回来的。😁在一个充满大佬的群中看到了这个图😂,我们是新生代农民工啦!!!不过吗劳动最光荣啦,作为即将成为新生代农民工的我,要继续卷啦。哈😁希望与君共勉,我们:待别日相见时,都已有所成。
Java设计模式-策略模式,一起来看看吧,让我们一起为进阶做一个充足的准备吧!!!!会了就当复习丫,不会来一起来看看吧。很喜欢一句话:“八小时内谋生活,八小时外谋发展”。如果你也喜欢,让我们一起坚持吧!!共勉😁封面:我想这才是夏天吧,心目中的夏天设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言1)引入:在现实生活中常常遇到实现某种目标存在多种策略可供选择的情况,例如,今天的作业该让这个女朋友写还是那个女朋友写勒?好难选啊,算了吧还是自己来吧。(其实就是没有😂)。正文:例如,出行旅游可以乘坐飞机、乘坐火车、骑自行车或自己开私家车等,超市促销可以釆用打折、送商品、送积分等方法。大家编程肯定知道,当实现某一个功能存在多种算法或者策略时,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如我们写个排序算法题,可以选择二叉树排序、快速排序、堆排序等,根据给出的条件不同,从中选出最适合的排序算法。还有咱们作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。2)概述:策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。3)角色结构:策略模式的主要角色如下。抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。环境(Context)类:持有一个策略类的引用,最终给客户端调用。4)使用场景:1、 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。2、 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。3、 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。4、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。5)注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。二、案例代码2.1、案例:【例】促销活动一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图:2.2、实现:定义百货公司所有促销活动的共同接口public interface Strategy { void show(); }定义具体策略角色(Concrete Strategy):每个节日具体的促销活动//为春节准备的促销活动A public class StrategyA implements Strategy { public void show() { System.out.println("春节活动:买一送一"); } } //为中秋准备的促销活动B public class StrategyB implements Strategy { public void show() { System.out.println("中秋活动:满200元减50元"); } } //为圣诞准备的促销活动C public class StrategyC implements Strategy { public void show() { System.out.println("圣诞活动:满1000元加一元换购任意200元以下商品"); } }定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员public class SalesMan { //持有抽象策略角色的引用 private Strategy strategy; public SalesMan(Strategy strategy) { this.strategy = strategy; } //向客户展示促销活动 public void salesManShow(){ strategy.show(); } } 客户端:public class Client { public static void main(String[] args) { SalesMan salesMan = new SalesMan(new StrategyA()); salesMan.salesManShow(); System.out.println("-----------"); SalesMan salesManB = new SalesMan(new StrategyB()); salesManB.salesManShow(); System.out.println("-----------"); /** * 春节活动:买一送一 * ----------- * 中秋活动:满200元减50元 * ----------- */ } }三、总结优点:策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if...else 语句、switch...case 语句。策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码转移到父类里面,从而避免重复的代码。缺点:客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。策略模式造成很多的策略类,每个具体策略类都会产生一个新类。有时候可以通过把依赖于环境的状态保存到客户端里面,而将策略类设计成可共享的,这样策略类实例可以被不同客户端使用。换言之,可以使用享元模式来减少对象的数量。四、自言自语我也不知道文章写出来是有用还是无用,只是想做一个分享。希望大家能够喜欢并且在这里能有收获。当然不可否认,我也想获得那种别人认可的那种快乐,并且能让我继续坚持。你好啊,要天天开心哦。下篇文章再见。此系列还在持续更新中.... 我一定还会回来的。😁
继享元模式后来到了模板方法模式啦。说到模板方法模式,它可能是一个让我们深入骨髓而又不自知的模式了,因为它在我们开发过程中会经常遇到,并且也非常简单。自我认为是Java设计模式中最简单的一种啦。 会了就当复习丫,不会来一起来看看吧。很喜欢一句话:“八小时内谋生活,八小时外谋发展”。如果你也喜欢,让我们一起坚持吧!!共勉😁人生还有许久,请相信会有光的设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言1)引入:说到模板方法模式,它可能是一个让我们深入骨髓而又不自知的模式了,因为它在我们开发过程中会经常遇到,并且也非常简单。只不过,很多时候我们并不知道它就是模板方法模式而已。不负责任的说,当我们用到override关键字重写父类方法的时候,十有八九就跟模板方法模式有关了。当然生活中也不少这种,例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。2)概述:模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤。并竟模板吗,就是整出一个样板出来,让其他人模仿啦。3)角色结构:模板方法(Template Method)模式包含以下主要角色:抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。4)使用场景:一些方法通用,却在每一个子类都重新写了这一方法。(即再向上抽取,提取公共代码)控制子类别必须遵守的一些事项。重构时,模板方法模式是一个经常使用到的模式,把相同的代码抽取到父类中,通过钩子函数约束其行为5)注意事项:为防恶意操作,一般模板方法都加上final关键字二、案例代码2.1、案例:就以我们去银行办理业务为例子。去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现2.2、实现:定义一个抽象类public abstract class AbstractClass { public final void takeNumber(){ System.out.println("取号码"); } public final void queueUp(){ System.out.println("耐心排队"); } /** * 办理具体业务 根据字类的需求来进行实现 */ public abstract void handleBusiness(); public final void score(){ System.out.println("给银行工作人员评分!!!"); } }AUser和BUser来模拟不同的顾客public class AUser extends AbstractClass{ @Override public void handleBusiness() { this.takeNumber(); this.queueUp(); System.out.println("你好,我想贷款2k,交个房租"); this.score(); } } public class BUser extends AbstractClass{ @Override public void handleBusiness() { this.takeNumber(); this.queueUp(); System.out.println("你好,我打算存2k,留着以后取媳妇!!!"); this.score(); } }客户端测试代码:public class Client { public static void main(String[] args) { AbstractClass aUser = new AUser(); aUser.handleBusiness(); /** * 取号码 * 耐心排队 * 你好,我想贷款2k,交个房租 * 给银行工作人员评分!!! */ } }注意:为防止恶意操作,一般模板方法都加上 final 关键词。三、总结优点:提高代码复用性,将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。实现了反向控制通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。缺点:对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。四、自言自语我也不知道文章写出来是有用还是无用,只是想做一个分享。希望大家能够喜欢并且在这里能有收获。当然不可否认,我也想获得那种别人认可的那种快乐,并且能让我继续为之坚持。你好啊,要天天开心哦。下篇文章再见。此系列还在持续更新中.... 我一定还会回来的。😁
现在大多数项目都会输出日志或保存日志,现在这个大数据时代,数据已经是一种非常非常重要的资源了。日志也有很大作用的,不要小瞧它哦。😁很喜欢一句话:“八小时内谋生活,八小时外谋发展”。如果你也喜欢,让我们一起坚持吧!!共勉😁我们:待别日相见时,都已有所成一、前言本文使用的SpringBoot版本为:2.5.21)概述:日志:网络设备、系统及服务程序等,在运作时都会产生一个叫log的事件记录;每一行日志都记载着日期、时间、使用者及动作等相关操作的描述。2)介绍:Windows网络操作系统设计有各种各样的日志文件,如应用程序日志,安全日志、系统日志、Scheduler服务日志、FTP日志、WWW日志、DNS服务器日志等等,这些根据你的系统开启的服务的不同而有所不同。本文介绍的更多的是偏向于行为日志,并非系统日志级别的。我们在系统上进行一些操作时,这些日志文件通常会记录下我们操作的一些相关内容,这些内容也许对我们来说并没有什么用处,但是对系统安全工作人员却相当有用。比如说有人对系统进行了IPC探测,系统就会在安全日志里迅速地记下探测者探测时所用的IP、时间、用户名等,用FTP探测后,就会在FTP日志中记下IP、时间、探测所用的用户名等。3)使用场景:简单介绍几个~~(我还菜很多不晓得,狗头保命😂)~~排查bug,从日志查看错误出现地方异地登录。(登录日志会记录下你的Ip)对了哈,本文更多的是提供一个方法、思路和用一个完整案例来让大家对SpringBoot-注解Aop记录日志有一个认识二、前期准备案例:使用SpringBoot的Aop方式,将访问者的信息写入数据库中。项目结构:说明:因为习惯了用MybatisPlus,拿了之前的完整配置,所以看起来java文件有多,但是关于log的其实并不复杂,代码中也带有注释, 请放心食用。对MybatisPlus感兴趣的可以点👉SpringBoot整合MybatisPlus2.1、数据库tb_user表CREATE TABLE `tb_user` ( `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `passwrod` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `deleted` int(1) NOT NULL DEFAULT 0, `create_time` datetime(0) NOT NULL COMMENT '创建时间', `update_time` datetime(0) NOT NULL COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; INSERT INTO `tb_user` VALUES ('1', '宁在春', '123456', 0, '2021-07-23 14:32:46', '2021-07-29 23:56:10'); INSERT INTO `tb_user` VALUES ('2', '青冬栗', 'qwerasd', 0, '2021-07-23 15:02:02', '2021-07-23 15:49:55');tb_log表DROP TABLE IF EXISTS `tb_log`; CREATE TABLE `tb_log` ( `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `user_id` int(10) NOT NULL, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `login_ip` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `type` int(10) NOT NULL, `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `operation` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `create_time` datetime(0) NULL DEFAULT NULL, `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '', `update_time` datetime(0) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; INSERT INTO `tb_log` VALUES ('e5b49465-b20a-453f-b15c-b284733f2f8e', 1, '宁在春', '0:0:0:0:0:0:0:1', 1, '127.0.0.1', '查询用户信息', '2021-08-15 01:04:31', '', '2021-08-15 01:04:31');2.2、导入依赖<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <!--spring切面aop依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.72</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.6.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies>依赖都是常用的哈,没啥要说的哈。😀2.3、yml配置文件server: port: 8091 spring: application: name: springboot-log # 数据源配置 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver # 阿里的数据库连接池 druid: username: root password: 123456 url: jdbc:mysql://localhost:3306/commons_utils?serverTimezone=UTC&useSSL=false&characterEncoding=utf8&serverTimezone=GMT # 初使化连接数(向数据库要五个连接) initial-size: 5 # 最小连接数(常住10个连接) min-idle: 10 # 最大连接数(最多获得10个连接,多到10个数据库将进入一个阻塞状态,等待其他连接释放) max-active: 20 # 获取连接最长等待时间,单位毫秒 max-wait: 10000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: cache-enabled: true #开启缓存 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志 mapper-locations: classpath:/mapper/*Mapper.xml global-config: db-config: logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)2.3、配置自定义log注解类如果需要收集多种日志的话,可以做扩展,增加注解也可,用编码也可,当然如果项目多的话,那么必然是要抽取出来才是最合适的。(经验不足、如有不妥,请及时提出,蟹蟹各位大佬😁)/** * 配置自定义log注解类 * @author crush */ @Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上 @Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行 @Documented //生成文档 public @interface MyLog { /** 操作事件 */ String operation () default ""; /** 日志类型 */ int type (); }2.4、SysLogAspect:切面处理类import cn.hutool.core.lang.UUID; import com.crush.log.annotation.MyLog; import com.crush.log.entity.LogOperation; import com.crush.log.entity.LogUser; import com.crush.log.mapper.LogOperationMapper; import com.crush.log.utils.IpUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; /** 系统日志:切面处理类 */ @Aspect @Component public class SysLogAspect { /**我这里是使用log4j2把一些信息打印在控制台上面,可以不写 */ private static final Logger log = LogManager.getLogger(SysLogAspect.class); /**操作数据库 */ @Autowired private LogOperationMapper logOperationMapper; /** * 定义切点 @Pointcut * 在注解的位置切入代码 * 这里的意思就是注解写在那个方法上,那个方法就是被切入的。 */ @Pointcut("@annotation(com.crush.log.annotation.MyLog)") public void logPoinCut() { } //切面 配置通知 @Before("logPoinCut()") //AfterReturning public void saveOperation(JoinPoint joinPoint) { log.info("---------------接口日志记录---------------"); //用于保存日志 LogOperation logOperation = new LogOperation(); // 这里是获得当前请求的request ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = servletRequestAttributes.getRequest(); String requestURL = request.getRequestURL().toString(); logOperation.setUrl(requestURL); // 客户端ip 这里还可以与之前做一个比较,如果不同的话,就给他推送消息什么的,说异地登录 什么的。 String ip = IpUtils.getIpAddr(request); logOperation.setLoginIp(ip); //从切面织入点处通过反射机制获取织入点处的方法 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //获取切入点所在的方法 Method method = signature.getMethod(); //获取操作--方法上的Log的值 MyLog myLog = method.getAnnotation(MyLog.class); if (myLog != null) { //保存操作事件 String operation = myLog.operation(); logOperation.setOperation(operation); //保存日志类型 这里也可以做扩展 根据不同的类型,你可以做不同的操作 int type = myLog.type(); logOperation.setType(type); log.info("operation="+operation+",type="+type); } // 操作人账号、姓名(需要提前将用户信息存到session) // 因为这里是模拟 所以偷懒用了个 session // 实际上用了security 获取的应该是当前授权对象的信息 而不是从session 中获取 // 也或者说是从 redis 中获取,这只是提供一个思路,请见谅 LogUser user = (LogUser) request.getSession().getAttribute("user"); if(user != null) { String userId = user.getId(); String userName = user.getUsername(); logOperation.setUserId(userId); logOperation.setUsername(userName); System.out.println(user); } log.info("url="+requestURL,"ip="+ip); //调用service保存Operation实体类到数据库 //我id使用的是UUID,不需要的可以注释掉 String id = UUID.randomUUID().toString().replace("-",""); logOperation.setId(id); logOperationMapper.insert(logOperation); } }2.5、MybatisPlus相关配置类MybatisPlusConfig/** * @EnableTransactionManagement :开启事务 * @Author: crush * @Date: 2021-07-23 14:14 * version 1.0 */ @Configuration @EnableTransactionManagement @MapperScan("com.crush.log.mapper") public class MybatisPlusConfig { /*** 分页*/ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 注册乐观锁 插件 return mybatisPlusInterceptor; } /** 配置数据源 druid*/ @Bean @Primary @ConfigurationProperties("spring.datasource.druid") public DruidDataSource druidDataSource() { return DruidDataSourceBuilder.create().build(); } }MyMetaObjectHandler:自动填充/** * 填充创建和修改时间 * @Author: crush * @Date: 2021-07-23 14:14 */ @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); this.setFieldValByName("createTime", LocalDateTime.now(),metaObject); this.setFieldValByName("updateTime",LocalDateTime.now(),metaObject); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); this.setFieldValByName("updateTime",LocalDateTime.now(),metaObject); } }LocalDateTimeSerializerConfig:配置全局的LocalDateTime格式化@Configuration public class LocalDateTimeSerializerConfig { @Value("${spring.jackson.date-format}") private String DATE_TIME_PATTERN; @Value("${spring.jackson.date-format}") private String DATE_PATTERN ; /*** string转localdate*/ @Bean public Converter<String, LocalDate> localDateConverter() { return new Converter<String, LocalDate>() { @Override public LocalDate convert(String source) { if (source.trim().length() == 0) { return null; } try { return LocalDate.parse(source); } catch (Exception e) { return LocalDate.parse(source, DateTimeFormatter.ofPattern(DATE_PATTERN)); } } }; } /** * string转localdatetime*/ @Bean public Converter<String, LocalDateTime> localDateTimeConverter() { return new Converter<String, LocalDateTime>() { @Override public LocalDateTime convert(String source) { if (source.trim().length() == 0) { return null; } // 先尝试ISO格式: 2019-07-15T16:00:00 try { return LocalDateTime.parse(source); } catch (Exception e) { return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)); } } }; } /** * 统一配置 LocalDateTime 格式化*/ @Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { JavaTimeModule module = new JavaTimeModule(); LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer); return builder -> { builder.simpleDateFormat(DATE_TIME_PATTERN); builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN))); builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN))); builder.modules(module); }; } }2.6、IpUtilsimport javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; /** * 获取IP方法 */ public class IpUtils { public static String getIpAddr(HttpServletRequest request) { if (request == null) { return "unknown"; } String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Forwarded-For"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip; } public static boolean internalIp(String ip) { byte[] addr = textToNumericFormatV4(ip); return internalIp(addr) || "127.0.0.1".equals(ip); } private static boolean internalIp(byte[] addr) { if (addr == null || addr.length < 2) { return true; } final byte b0 = addr[0]; final byte b1 = addr[1]; // 10.x.x.x/8 final byte SECTION_1 = 0x0A; // 172.16.x.x/12 final byte SECTION_2 = (byte) 0xAC; final byte SECTION_3 = (byte) 0x10; final byte SECTION_4 = (byte) 0x1F; // 192.168.x.x/16 final byte SECTION_5 = (byte) 0xC0; final byte SECTION_6 = (byte) 0xA8; switch (b0) { case SECTION_1: return true; case SECTION_2: if (b1 >= SECTION_3 && b1 <= SECTION_4) { return true; } case SECTION_5: if (b1 == SECTION_6) { return true; } default: return false; } } /** * 将IPv4地址转换成字节 * * @param text IPv4地址 * @return byte 字节 */ public static byte[] textToNumericFormatV4(String text) { if (text.length() == 0) { return null; } byte[] bytes = new byte[4]; String[] elements = text.split("\\.", -1); try { long l; int i; switch (elements.length) { case 1: l = Long.parseLong(elements[0]); if ((l < 0L) || (l > 4294967295L)) return null; bytes[0] = (byte) (int) (l >> 24 & 0xFF); bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); bytes[3] = (byte) (int) (l & 0xFF); break; case 2: l = Integer.parseInt(elements[0]); if ((l < 0L) || (l > 255L)) return null; bytes[0] = (byte) (int) (l & 0xFF); l = Integer.parseInt(elements[1]); if ((l < 0L) || (l > 16777215L)) return null; bytes[1] = (byte) (int) (l >> 16 & 0xFF); bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); bytes[3] = (byte) (int) (l & 0xFF); break; case 3: for (i = 0; i < 2; ++i) { l = Integer.parseInt(elements[i]); if ((l < 0L) || (l > 255L)) return null; bytes[i] = (byte) (int) (l & 0xFF); } l = Integer.parseInt(elements[2]); if ((l < 0L) || (l > 65535L)) return null; bytes[2] = (byte) (int) (l >> 8 & 0xFF); bytes[3] = (byte) (int) (l & 0xFF); break; case 4: for (i = 0; i < 4; ++i) { l = Integer.parseInt(elements[i]); if ((l < 0L) || (l > 255L)) return null; bytes[i] = (byte) (int) (l & 0xFF); } break; default: return null; } } catch (NumberFormatException e) { return null; } return bytes; } public static String getHostIp() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException ignored) { } return "127.0.0.1"; } public static String getHostName() { try{ return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException ignored) { } return "未知"; } }三、业务代码我这里没有写查看日志的接口,存数据库,管理员可以随时查看这些信息,也可以使用web页面、会方便许多。1、entityLogUser/** * @Author: crush * @Date: 2021-08-14 8:43 * version 1.0 */ @Data @Accessors(chain = true) @TableName("tb_user") public class LogUser implements Serializable { private static final long serialVersionUID = 1L; private String id; private String username; private String passwrod; /*** 逻辑删除字段 */ @TableLogic private Integer deleted; /*** 创建时间*/ @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; /*** 修改时间*/ @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }package com.crush.log.entity; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Builder; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; import java.time.LocalDateTime; /** * 日志表 * @author crush */ @Data @Accessors(chain = true) @TableName("tb_log") public class LogOperation implements Serializable { private static final long serialVersionUID = 7925874058046995566L; private String id; /*** 用户id 操作人ID */ private String userId; /** * 用户名称 关联admin_user */ private String username; /** * 登录ip */ private String loginIp; /** * 操作类型(0登录、1查询、2修改) 这个根据自己需求定义即可 ,还有很多其他方式,这个并不完善,只是刚刚够用的那种 */ private int type; /** * 操作的url*/ private String url; /** * 操作内容 */ private String operation; /** * 操作时间*/ @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; /*** 备注*/ private String remark; /*** 修改时间*/ @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }2、mapper@Repository @Mapper public interface LogOperationMapper extends BaseMapper<LogOperation> { }@Repository public interface LogUserMapper extends BaseMapper<LogUser> { }3、Servicepublic interface ILogUserService extends IService<LogUser> { }@Service public class LogUserServiceImpl extends ServiceImpl<LogUserMapper, LogUser> implements ILogUserService { }4、Controllerimport com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.crush.log.annotation.MyLog; import com.crush.log.entity.LogUser; import com.crush.log.service.ILogUserService; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.util.List; @RestController @RequestMapping("user") public class UserController { private static final Logger log = LogManager.getLogger(UserController.class); @Autowired private ILogUserService userService; /** * 假装登录,将用户信息存到session(方法是我之前写的懒得改,) * */ @RequestMapping("/login") public String login(@RequestBody LogUser logUser,HttpServletRequest request){ QueryWrapper<LogUser> wrapper = new QueryWrapper<>(); wrapper.eq("username",logUser.getUsername()).eq("passwrod",logUser.getPasswrod()); LogUser user = userService.getOne(wrapper); if(user!=null){ request.getSession().setAttribute("user",user); return "登录成功"; } return "登录失败"; } /**记录日志*/ @MyLog(operation = "查询用户信息",type = 1) @RequestMapping("/log") public List<LogUser> insertLog(HttpServletRequest request){ List<LogUser> users = userService.list(); return users; } }记得写个主启动类,这我就不写啦。5、测试直接启动测试,先登录,再访问/log.再访问/log我们再看一下后台输出:四、自言自语本文只是给大家提供一个小思路,代码写的较为粗糙,请见谅。😁还有很多地方可以扩展和完善,大家感兴趣的话,可以多试一试,这样学习才有乐趣啦。😂日志的话他还会分很多类的,大家可以根据自己的需求扩展。我知道咱们掘金的大佬,讲话又好听,长的又帅,女朋友随便new,给小弟一个赞👍,这肯定的吧。😁
继续啦继续啦,学习不能断哦。继组合模式后开启了享元模式啦。 会了就当复习丫,不会来一起来看看吧。很喜欢一句话:“八小时内谋生活,八小时外谋发展”。如果你也喜欢,让我们一起坚持吧!!共勉😁一张旧图设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、享元模式前言1)引入:在Java中,我们都知道在创建字符串对象时,都需要去字符串常量池中寻找一番,已经有了,就不再重复创建了,只是把它的引用指向那个地址,没有就再创建。因为如果每一次都去创建新的字符串对象的话,内存开销会非常大,所以说享元模式是池技术的重要实现方式。2)概述:享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式.享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。3)结构:享元(Flyweight )模式中存在以下两种状态:内部状态,即不会随着环境的改变而改变的可共享部分。外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。享元模式的主要有以下角色:抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。4)使用场景:1、系统有大量相似对象。2、需要缓冲池的场景。如果一个应用程序使用了大量的对象,而这些对象造成了很大的存储开销的时候就可以考虑是否可以使用享元模式。例如,如果发现某个对象的生成了大量细粒度的实例,并且这些实例除了几个参数外基本是相同的,如果把那些共享参数移到类外面,在方法调用时将他们传递进来,就可以通过共享大幅度单个实例的数目二、案例代码案例:俄罗斯方块下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。我们玩俄罗斯方块大都数是这些方块形状,出现不同也只是颜色的不一样,那么这样我们就可以把他们共有的东西,抽取出来。类图:俄罗斯方块有不同的形状,这里我只描述了其中几个,其他的都相同,就不再写出来了。”I" 、"J"、"L"就拿这三个字符来表示俄罗斯方块了哈😁😁,更详细的在代码中含有解释。我们在AbstractBox 中定义了public abstract String getShape();,这个是将形状再向上抽象一层。IBox、LBox、OBox继承分别实现了getShape()方法,表示不一样的形状。这在享元模式的状态中,属于内部状态。外部状态就是各种形状的颜色。代码:详细的看下面的代码:俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。public abstract class AbstractBox { public abstract String getShape(); public void display(String color) { System.out.println("方块形状:" + this.getShape() + " 颜色:" + color); } }不同的形状了,IBox类、LBox类、OBox类public class IBox extends AbstractBox { @Override public String getShape() { return "I"; } } public class LBox extends AbstractBox { @Override public String getShape() { return "L"; } } public class OBox extends AbstractBox { @Override public String getShape() { return "O"; } }提供了一个工厂类(BoxFactory),用来管理享元对象(也就是AbstractBox子类对象),该工厂类对象只需要一个,所以可以使用单例模式。并给工厂类提供一个获取形状的方法。public class BoxFactory { private static HashMap<String, AbstractBox> map; // 在构造方法中进行初始化操作 private BoxFactory() { map = new HashMap<String, AbstractBox>(); map.put("I", new IBox()); map.put("L", new LBox()); map.put("O", new OBox()); } //提供一个方法获取工厂类对象 public static final BoxFactory getInstance() { return SingletonHolder.INSTANCE; } // 静态内部类的方式来实现单例模式 private static class SingletonHolder { private static final BoxFactory INSTANCE = new BoxFactory(); } //根据名字获取图形对象 public AbstractBox getBox(String key) { return map.get(key); } }客户端:public class Client { public static void main(String[] args) { BoxFactory boxFactory = BoxFactory.getInstance(); AbstractBox box = boxFactory.getBox("I"); box.display("黄色"); AbstractBox box2 = boxFactory.getBox("I"); box2.display("绿色"); System.out.println(box==box2); System.out.println(box.equals(box2)); /** * 方块形状:I 颜色:黄色 * 方块形状:I 颜色:绿色 * true * true */ } }我们可以看到两个对象虽然颜色不一样,但是两个对象却是一样的。实现了公用。三、总结1、优点极大减少内存中相似或相同对象数量,节约系统资源,提高系统性能享元模式中的外部状态相对独立,且不影响内部状态2、缺点:为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。3、使用场景:一个系统有大量相同或者相似的对象,造成内存的大量耗费。对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。4、应用实例:String常量池数据库连接池四、自言自语你好,如果你正巧看到这篇文章,并且觉得对你有益的话,就给个赞吧,让我感受一下分享的喜悦吧,蟹蟹。🤗如若有写的有误的地方,也请大家不啬赐教!!同样如若有存在疑惑的地方,请留言或私信,定会在第一时间回复你。持续更新中
继Java设计模式-外观模式后的组合模式它也来了哦,让我们一起来瞧一瞧吧!!!😁 会了就当复习丫,不会来一起来看看吧。很喜欢一句话:“八小时内谋生活,八小时外谋发展”。如果你也喜欢,让我们一起坚持吧!!共勉😁初入夏时设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言1)引入:在现实生活中,存在很多“部分-整体”的关系,例如,大学中的部门与学院、总公司中的部门与分公司、学习用品中的书与书包、生活用品中的衣服与衣柜、以及厨房中的锅碗瓢盆等。在软件开发中也是如此,如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。2)概述:组合模式(Composite Pattern):将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。。有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。3)角色:1.抽象根节点Component 是组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。2.树枝节点Composite 定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(remove)等。3.叶子节点Leaf 在组合中表示叶子结点对象,叶子结点没有子结点。4)使用场景:1.你想表示对象的部分-整体层次结构2.你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。组合模式正是应树形结构而生,所以组合模式的使用场景就大都是是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。二、代码实现案例:如下图,我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。要实现该案例,我们先画出类图:代码:不管是菜单还是菜单项,都应该继承自统一的接口,这里姑且将这个统一的接口称为菜单组件。//菜单组件 不管是菜单还是菜单项,都应该继承该类 public abstract class MenuComponent { protected String name; protected int level; //添加菜单 public void add(MenuComponent menuComponent){ System.out.println("文件不能添加菜单"); throw new UnsupportedOperationException(); } //移除菜单 public void remove(MenuComponent menuComponent){ System.out.println("文件不能移除菜单"); throw new UnsupportedOperationException(); } //获取指定的子菜单 public MenuComponent getChild(int i){ System.out.println("文件没有子菜单"); throw new UnsupportedOperationException(); } //获取菜单名称 public String getName(){ return name; } public void print(){ throw new UnsupportedOperationException(); } }这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法。举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。我这里就是打印句话,然后抛出异常。MenuMenu类已经实现了除了getName方法的其他所有方法,因为Menu类具有添加菜单,移除菜单和获取子菜单的功能。public class Menu extends MenuComponent { private List<MenuComponent> menuComponentList; public Menu(String name,int level){ this.level = level; this.name = name; menuComponentList = new ArrayList<MenuComponent>(); } @Override public void add(MenuComponent menuComponent) { menuComponentList.add(menuComponent); } @Override public void remove(MenuComponent menuComponent) { menuComponentList.remove(menuComponent); } @Override public MenuComponent getChild(int i) { return menuComponentList.get(i); } @Override public void print() { for (int i = 1; i < level; i++) { System.out.print("--"); } System.out.println(name); for (MenuComponent menuComponent : menuComponentList) { menuComponent.print(); } } }MenuItemMenuItem是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现。public class MenuItem extends MenuComponent { public MenuItem(String name,int level) { this.name = name; this.level = level; } @Override public void print() { for (int i = 1; i < level; i++) { System.out.print("--"); } System.out.println(name); } }测试public class Client { public static void main(String[] args) { MenuComponent component = new Menu("crush",2); MenuComponent c2 = new Menu("wyh1",1); MenuComponent wyh4 = new MenuItem("wyh4", 2); MenuComponent wyh5 = new MenuItem("wyh5", 2); component.add(c2); component.add(wyh4); //wyh4.add(wyh5); component.print(); /** * 输出: * --crush * wyh1 * --wyh4 */ } }上面这个代码案例,是属于组合模式中的透明模式,你没看错,组合模式有两种。我忘记写在前面啦,这里再来给大家介绍一下哈:透明组合模式透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中 MenuComponent 声明了 add、remove 、getChild 方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)安全组合模式在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 Menu 类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。三、总结优点组合模式 屏蔽了对象系统的层次差异性(树节点和叶子节点为不同类型),将客户代码与复杂的容器对象解耦,使得客户端可以忽略层次间的差异,简化了客户端代码,使用一致的行为控制不同层次。高层模块调用简单。在 组合模式可以很方便地增加 树枝节点 和 叶子节点 对象,并对现有类库无侵入,符合开闭原则;缺点如果类系统(树形结构)过于庞大,虽然对不同层次都提供一致性操作,但客户端仍需花费时间理清类之间的层次关系;在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。不容易用继承的方法来增加构件的新功能;四、自言自语你卷我卷,大家卷,什么时候这条路才是个头啊。😇(还是直接上天吧)有时候也想停下来歇一歇,一直做一个事情,感觉挺难坚持的。😁你好,如果你正巧看到这篇文章,并且觉得对你有益的话,就给个赞吧,让我感受一下分享的喜悦吧,蟹蟹。🤗如若有写的有误的地方,也请大家不啬赐教!!同样如若有存在疑惑的地方,请留言或私信,定会在第一时间回复你。持续更新中
我又来了,继Java设计模式之桥接模式后,现在来到了外观模式啦,外观模式又称为门面模式啦😁,下面慢慢来啦。 会了就当复习丫,不会来一起来看看吧。很喜欢一句话:“八小时内谋生活,八小时外谋发展”。如果你也喜欢,让我们一起坚持吧!!共勉😁一张旧图,恍惚间念起旧人设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言1)引入:在以前,手机没有这么方便的时候,我们一旦需要去哪里哪里办个什么证,那真就的从这签个字从那签个字,签一路,才能办下那个证,比如去办房产证:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vbHf1WGj-1628694563882)(C:\Users\ASUS\Desktop\宁在春的学习笔记\JDK8\文档\Java设计模式-外观模式.assets\image-20210811223659159.png)]作为懒人的我们,肯定会想要是能有个一站式的就好了。直接一次解决所有问题。其实,在我们软件设计当中,也是如此。当一个系统的功能越来越强,子系统会越来越多,客户对系统的访问也变得越来越复杂。这时如果系统内部发生改变,客户端也要跟着改变,这违背了“开闭原则”,也违背了“迪米特法则”,所以有必要为多个子系统提供一个统一的接口,从而降低系统的耦合度,这就是外观模式的目标。2)概述:外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。外观(Facade)模式是“迪米特法则”的典型应用。迪米特法则:又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。英文简写为: LOD。3)角色结构:主要包含以下主要角色。外观(Facade)角色:为多个子系统对外提供一个共同的接口。子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。客户(Client)角色:通过一个外观角色访问各个子系统的功能。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KVhuh6Og-1628694563885)(C:\Users\ASUS\Desktop\宁在春的学习笔记\JDK8\文档\Java设计模式-外观模式.assets\image-20210811224537663.png)]4)使用场景(1)设计初期阶段,应该有意识的将不同层分离,层与层之间建立外观模式。(例如:我们平时开发时,controller调用service接口,而不用管serviceImpl的实现是如何的,即三层开发模式。)(2) 开发阶段,子系统越来越复杂,增加外观模式提供一个简单的调用接口。(3) 维护一个大型遗留系统的时候,可能这个系统已经非常难以维护和扩展,但又包含非常重要的功能,为其开发一个外观类,以便新系统与其交互。二、案例【例】智能家电控制父母年纪大了,平时是我在家的话,这开灯开电视开空调都是父母直接喊我来做的,想着自己离开家了,父母的自己动手,就给父母买了个智能音箱来控制这些。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n1F81uxz-1628694563887)(C:\Users\ASUS\Desktop\宁在春的学习笔记\JDK8\文档\Java设计模式-外观模式.assets\外观模式.png)]代码:电视类public class TV { public void on() { System.out.println("打开了电视...."); } public void off() { System.out.println("关闭了电视...."); } }灯类public class Light { public void on() { System.out.println("打开了灯...."); } public void off() { System.out.println("关闭了灯...."); } }空调类public class AirCondition { public void on() { System.out.println("打开了空调...."); } public void off() { System.out.println("关闭了空调...."); } }智能音箱package com.crush.facade; //智能音箱 public class SmartAppliancesFacade { private Light light; private TV tv; private AirCondition airCondition; public SmartAppliancesFacade() { light = new Light(); tv = new TV(); airCondition = new AirCondition(); } public void say(String message) { if (message.contains("开灯")) { onLamp(); } else if (message.contains("关灯")) { offLamp(); } else if (message.contains("开电视")) { onTV(); } else if (message.contains("关电视")) { offTV(); } else if (message.contains("开空调")) { onAirCondition(); } else if (message.contains("关空调")) { offAirCondition(); } else { System.out.println("我还听不懂你说的!!!"); } } private void onLamp() { light.on(); } private void offLamp() { light.off(); } private void onTV() { tv.on(); } private void offTV() { tv.off(); } private void onAirCondition() { airCondition.on(); } private void offAirCondition() { airCondition.off(); } }测试://测试类 public class Client { public static void main(String[] args) { //创建外观对象 SmartAppliancesFacade facade = new SmartAppliancesFacade(); //客户端直接与外观对象进行交互 facade.say("打开家电"); facade.say("关闭家电"); } }这样就做到了通过一个智能音箱控制全部,而不用管其他的具体实现,只要知道这个接口即可以了。当然这只是一个体现的外观模式的小demo,实际中并不全是一样的,设计模式也是根据实际的软件设计需求来进行应用的,多数情况下都是几种设计模式一起用的。三、总结优点:降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。缺点:不能很好地限制客户使用子系统类,很容易带来未知风险。增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。四、自言自语你卷我卷,大家卷,什么时候这条路才是个头啊。😇(还是直接上天吧)有时候也想停下来歇一歇,一直做一个事情,感觉挺难坚持的。😁你好,如果你正巧看到这篇文章,并且觉得对你有益的话,就给个赞吧,让我感受一下分享的喜悦吧,蟹蟹。🤗如若有写的有误的地方,也请大家不啬赐教!!同样如若有存在疑惑的地方,请留言或私信,定会在第一时间回复你。持续更新中
最近在整理知识点的时候,对于SpringSecurity中的那个ROLE_真的感觉很奇怪,今天查了不少,找到一点点东西,可以丰富一些杂识哈。😁喜欢的话,一起坚持啦!!!SpringBoot集成Security一、前言:先讲一下我的好奇点:最近在使用Security做安全权限控制,可以看到下图,这个方法可以通过的角色是USER,但是我的表中的数据是这样的。我就对于这个前缀ROLE_非常的好奇。(因为是许久之前的代码了,就忘记的差不多啦🐕狗头保命)我做过测试如果使用@PreAuthorize("hasAnyRole('USER')")此注解的话:总之得拼出ROLE_USER数据库上的那个权限or角色的字段必须为ROLE_USER,又或者啊数据库写成 USER,但是在使用注解时写成这样也可以@PreAuthorize("hasAnyRole('ROLE_USER')")。我测试访问是可以通过的,这样我就对于为什么一定要这样,就更好奇,所以就开始了属于我的好奇之旅哈。😂二、目前所知看完查到的博客,暂时还没有找到security的设计者这样设计的原因,但是对于为什么要这么去写,在源码中有所提及:/** 投票是否有任何ConfigAttribute.getAttribute()以前缀开头,表明它是一个角色。 默认前缀字符串是ROLE_ , 但这可以覆盖为任何值。 它也可以设置为空,这意味着基本上任何属性都将被投票。 如下文进一步描述的,空前缀的效果可能不是很理想。 如果没有配置属性以角色前缀开头,则弃权。 如果存在与以角色前缀开头的ConfigAttribute完全匹配的GrantedAuthority ,则投票授予访问权限。 如果没有与以角色前缀开头的ConfigAttribute完全匹配的GrantedAuthority ,则投票拒绝访问。 空的角色前缀意味着投票者将为每个 ConfigAttribute 投票。 当使用不同类别的 ConfigAttributes 时,这将不是最佳的,因为投票者将为不代表角色的属性投票。 但是,当使用没有前缀的预先存在的角色名称时,此选项可能会有一些用处,并且无法在读取它们时使用角色前缀作为前缀, 例如在org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl 。 所有比较和前缀都区分大小写。 */ public class RoleVoter implements AccessDecisionVoter<Object> { private String rolePrefix = "ROLE_"; public String getRolePrefix() { return rolePrefix; } public void setRolePrefix(String rolePrefix) {this.rolePrefix = rolePrefix; } // 这里就是判断是否符合 public boolean supports(ConfigAttribute attribute) { if ((attribute.getAttribute() != null) && attribute.getAttribute().startsWith(getRolePrefix())) { return true; } else { return false; } } public boolean supports(Class<?> clazz) { return true; } //判断是否授予访问权限。 public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { if (authentication == null) { return ACCESS_DENIED; } int result = ACCESS_ABSTAIN; Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication); for (ConfigAttribute attribute : attributes) { if (this.supports(attribute)) { result = ACCESS_DENIED; // Attempt to find a matching granted authority for (GrantedAuthority authority : authorities) { if (attribute.getAttribute().equals(authority.getAuthority())) { return ACCESS_GRANTED; } } } } return result; } Collection<? extends GrantedAuthority> extractAuthorities( Authentication authentication) { return authentication.getAuthorities(); } }有以下几个点:ROLE_是默认的一个前缀,可以覆盖,但是不建议为空,这点我在👉官方文档中也查询到了。因为如果为空的话,空的角色前缀意味着投票者将为每个 ConfigAttribute 投票。就是每个都可以通过的意思(自我理解)。但是如果没有配置前缀的话,那么就会直接判定为权限不足,继而不通过。只有以角色前缀开头ConfigAttribute完全匹配的GrantedAuthority(表示授予Authentication对象的权限) 的才能被授权访问。就是权限名是完全一样才能被访问,否则就被拒绝。三、自言自语学习必须的带上兴趣,才能变得不一样,有动力,有冲劲。😁感兴趣的话,大家可以再试着Debug、bug、bug一下下哦。
继Java设计模式-装饰器模式后的桥接模式出来了,感兴趣的话,就来看一看吧。会了就当复习丫,不会来一起来看看吧。很喜欢一句话:“八小时内谋生活,八小时外谋发展”。如果你也喜欢,让我们一起坚持吧!!共勉😁校园一角设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、桥接模式介绍1)引入 在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。 在软件系统中,某些类型由于自身的逻辑,它具有两个或多个维度的变化,那么如何应对这种“多维度的变化”?如何利用面向对象的技术来使得该类型能够轻松的沿着多个方向进行变化,而又不引入额外的复杂度?这就要使用Bridge模式。(当然并不局限于桥接模式)2)概述桥接模式:将抽象部分与实现部分分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。桥接模式将继承关系转化成关联关系,它降低了类与类之间的耦合度,减少了系统中类的数量,也减少了代码量。将抽象部分与他的实现部分分离这句话不是很好理解,其实这并不是将抽象类与他的派生类分离,而是抽象类和它的派生类用来实现自己的对象。这样还是不能理解的话。我们就先来认清什么是抽象化,什么是实现化,什么是脱耦。抽象化:存在于多个实体中的共同的概念性联系,就是抽象化。作为一个过程,抽象化就是忽略一些信息,从而把不同的实体当做同样的实体对待。实现化:抽象化给出的具体实现,就是实现化脱耦:所谓耦合,就是两个实体的行为的某种强关联。而将它们的强关联去掉,就是耦合的解脱,或称脱耦。在这里,脱耦是指将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联。将两个角色之间的继承关系改为聚合关系,就是将它们之间的强关联改换成为弱关联。因此,桥梁模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以相对独立地变化。这就是桥梁模式的用意。3)模式结构由抽象化角色和修正抽象化角色组成的抽象化等级结构。由实现化角色和两个具体实现化角色所组成的实现化等级结构。抽象化 (Abstraction)角色:抽象化给出的定义,并保存一个对实现化对象的引用。扩展抽象化(Refined Abstraction)角色:扩展抽象化角色,改变和修正父类对抽象化的定义。实现化(Implementor)角色:这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。具体实现化(Concrete Implementor)角色:这个角色给出实现化角色接口的具体实现。4)使用场景不希望或不适用使用继承的场景接口或抽象类不稳定的场景重用性要求较高的场景二、桥接模式案例2.1、案例下面我们举一个例子:需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。桥接模式的核心意图就是把这些实现独立出来,让它们各自地变化,这就使得每种实现的变化不会影响其他实现,从而达到应对变化的目的。图解:从上面这个图可以看出,两个维度分别为:OperatingSystem就是抽象化角色,Windows和Mac是扩展化角色,VideoFile就是实现化角色,AVIFile和RMVBFile是具体实现化角色。我都懂的,还是看下面👇代码的实现是咋样的吧😁2.2、代码实现OperatingSystem:public abstract class OperatingSystemVersion { protected VideoFile videoFile; public OperatingSystemVersion(VideoFile videoFile) { this.videoFile = videoFile; } public abstract void play(String fileName); }Windows和Mac是扩展化角色public class Windows extends OperatingSystemVersion { public Windows(VideoFile videoFile) { super(videoFile); } public void play(String fileName) { System.out.println("Windows正在播放:"); videoFile.decode(fileName); } } public class Mac extends OperatingSystemVersion { public Mac(VideoFile videoFile) { super(videoFile); } @Override public void play(String fileName) { System.out.println("Mac正在播放:"); videoFile.decode(fileName); } }VideoFilepublic interface VideoFile { void decode(String fileName); }AVIFile和RMVBFile是具体实现化角色public class REVBBFile implements VideoFile { public void decode(String fileName) { System.out.println("rmvb文件:" + fileName); } } public class AVIFile implements VideoFile { @Override public void decode(String fileName) { System.out.println("avi视频文件:"+ fileName); } }测试:public class Client { public static void main(String[] args) { OperatingSystemVersion os = new Mac(new AVIFile()); os.play("战狼3"); /** * 输出:Mac正在播放:avi视频文件:战狼3 */ } }这么写之后,无论是对于扩展OperatingSystem还是VideoFile方面,都可以独立的扩展,非常的方便。也符合上👆文中对于桥接模式的定义:将抽象部分与实现部分分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。三、总结优缺点:桥接(Bridge)模式的优点是:抽象与实现分离,扩展能力强符合开闭原则符合合成复用原则其实现细节对客户透明缺点是:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。注意事项不要一涉及继承就考虑该模式,尽可能把变化的因素封装到最细、最小的逻辑单元中,避免风险扩散当发现类的继承有n层时,可以考虑使用该模式四、自言自语你卷我卷,大家卷,什么时候这条路才是个头啊。😇(还是直接上天吧)有时候也想停下来歇一歇,一直做一个事情,感觉挺难坚持的。😁你好,如果你正巧看到这篇文章,并且觉得对你有益的话,就给个赞吧,让我感受一下分享的喜悦吧,蟹蟹。🤗如若有写的有误的地方,也请大家不啬赐教!!同样如若有存在疑惑的地方,请留言或私信,定会在第一时间回复你。持续更新中
继Java设计模式适配器模式后的装饰器模式来啦,让我们一起看看吧。会了就当复习丫,不会来一起来看看吧。很喜欢一句话:“八小时内谋生活,八小时外谋发展”。如果你也喜欢,让我们一起坚持吧!!共勉😁一张旧图,恍惚间念起旧人设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、装饰器模式介绍1)引入:上班族大多都有睡懒觉的习惯,每天早上上班时间都很紧张,于是很多人为了多睡一会,就会用方便的方式解决早餐问题。有些人早餐可能会吃煎饼,煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么“加码”,都还是一个煎饼。在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等,都是装饰器模式。在我们自己行业就是这个东西得加需求啦在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰器模式来实现。2)概述装饰器(Decorator)模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。3)角色结构抽象构件(Component):定义一个抽象接口以规范准备接收附加责任的对象。具体构件(ConcreteComponent):实现抽象构件,通过装饰角色为其添加一些职责。抽象装饰(Decorator):继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。具体装饰(ConcreteDecorator):实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。4)使用场景1、扩展一个类的功能。2、动态增加功能,动态撤销。就是主要为了方便扩展。5)举个例子快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。这是一个快餐店的例子,这么咋一看,感觉还可以,但是如果不再局限于炒饭FriedRice和炒面FriedNoodies中,想要做一些扩展,例如加一个炒粉Fried sweet potato powder,就又要额外增加一个整体,再往下重复实现鸡蛋、培根等等的类。增加这么多,就会造成类爆炸,特别多,就非常不合适。会产生过多的子类。欲知后事如何,请看下文👇。二、装饰器模式实现2.1、前言接下来,我们用装饰器的模式来重构一下代码,看看会产生哪些方面的变化哈。也来一起看看装饰器模式的精髓。不过图也要改变一下啦,变成这样子的啦:我们先来讲讲这张图和上一张图的区别。炒饭炒面FriedRice和FriedNoodies还是继承FastFoot之前的配料Egg和Bacon不再位于 炒饭炒面下面,而是继承于抽象的配料类下,而配料类Garnish又继承于FastFoot。这么看好像还是少了点东西,结合代码我们一起来看一看。我们把角色定位一下:抽象构件(Component):FastFoot类 即快餐类具体构件(ConcreteComponent): FriedRice和FriedNoodies 即炒饭炒面抽象装饰(Decorator):Garnish 类具体装饰(ConcreteDecorator) Egg和Bacon 类,即鸡蛋和培根为具体装饰类2.2、代码实现👇下面看代码一步一步实现看一下:FastFoot快餐接口:即抽象构件//快餐接口 public abstract class FastFood { private float price; private String desc; public FastFood() { } public FastFood(float price, String desc) { this.price = price; this.desc = desc; } //set、get 方法 public void setPrice(float price) { this.price = price; } public float getPrice() { return price; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public abstract float cost(); //获取价格 }FriedRice和FriedNoodies 即炒饭炒面 即具体构件//炒饭 public class FriedRice extends FastFood { public FriedRice() { super(10, "炒饭"); } // 获取价格 public float cost() { return getPrice(); } } //炒面 public class FriedNoodles extends FastFood { public FriedNoodles() { super(12, "炒面"); } // 获取价格 public float cost() { return getPrice(); } }Garnish 即配料类 抽象装饰public abstract class Garnish extends FastFood { private FastFood fastFood; public FastFood getFastFood() { return fastFood; } public void setFastFood(FastFood fastFood) { this.fastFood = fastFood; } public Garnish(FastFood fastFood, float price, String desc) { super(price,desc); this.fastFood = fastFood; } }Egg和Bacon 类,即鸡蛋和培根为 具体装饰类//鸡蛋配料 public class Egg extends Garnish { public Egg(FastFood fastFood) { super(fastFood,1,"鸡蛋"); } // 这里是返回了 炒饭加 鸡蛋的钱的 public float cost() { return getPrice() + getFastFood().getPrice(); } @Override public String getDesc() { return super.getDesc() + getFastFood().getDesc(); } } //培根配料 public class Bacon extends Garnish { public Bacon(FastFood fastFood) { super(fastFood,2,"培根"); } @Override public float cost() { return getPrice() + getFastFood().getPrice(); } @Override public String getDesc() { return super.getDesc() + getFastFood().getDesc(); } }测试类:public class Client { public static void main(String[] args) { //点一份炒饭 FastFood food = new FriedRice(); //花费的价格 System.out.println(food.getDesc() + " " + food.cost() + "元"); System.out.println("========"); //点一份加鸡蛋的炒饭 FastFood food1 = new FriedRice(); food1 = new Egg(food1); //花费的价格 System.out.println(food1.getDesc() + " " + food1.cost() + "元"); System.out.println("========"); //点一份加培根的炒面 FastFood food2 = new FriedNoodles(); food2 = new Bacon(food2); //花费的价格 System.out.println(food2.getDesc() + " " + food2.cost() + "元"); } }这就解决了我们刚开始的一个问题,如果还需要进行扩张,需要增加一个炒河粉 那么只需要写一个炒河粉的类来继承FastFoot快餐类即可,如需增加配料,也只要写个配料类来继承Garnish配料类即可。其他代码均不用改变,完全符合开闭原则。也比原本减少了类的产生。😁三、总结1、使用场景当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类)在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。当对象的功能要求可以动态地添加,也可以再动态地撤销时。2、优点:装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。装饰器模式完全遵守开闭原则3、缺点:装饰器模式会增加许多子类,过度使用会增加程序得复杂性。多层装饰比较复杂。四、自言自语你卷我卷,大家卷,什么时候这条路才是个头啊。😇(还是直接上天吧)有时候也想停下来歇一歇,一直做一个事情,感觉挺难坚持的。😁你好,如果你正巧看到这篇文章,并且觉得对你有益的话,就给个赞吧,让我感受一下分享的喜悦吧,蟹蟹。🤗如若有写的有误的地方,也请大家不啬赐教!!同样如若有存在疑惑的地方,请留言或私信,定会在第一时间回复你。持续更新中
继代理模式后又来到适配器模式啦,想看之前的也有哦。持续更新中哦。让我们一起加油吧兄弟们,干他。很喜欢一句话:”八小时内谋生活,八小时外谋发展".你好,如果喜欢,请一起坚持!!共勉😁一张旧照,恍惚间想起旧人设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言1)概述 在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的实例,这时需要第三者进行适配。例如,讲中文的人同讲英文的人对话时需要一个翻译,用直流电的笔记本电脑接交流电源时需要一个电源适配器,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。还有像下面这张图一样: 在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题。2)介绍适配器模式(Adapter)的定义如下:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。Adapter模式的宗旨:保留现有类所提供的服务,向客户提供接口,以满足客户的期望。3)角色结构适配器模式(Adapter)包含以下主要角色:目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。4)使用场景适配器模式(Adapter)通常适用于以下场景。以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。(就是所谓的加一层,一层不行就加两层)😁使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。二、类适配器当客户在接口中定义了他期望的行为时,我们就可以应用适配器模式,提供一个实现该接口的类,并且扩展已有的类,通过创建子类来实现适配。实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。我们直接用之前的那个图来做个例子:中国人到了欧洲,的给自己电脑充电,但因为自己电脑是双叉,欧式是三叉,这中间就得需要一个转换器。2.1、代码目标(Target)接口:即图中的欧式三叉public interface EuropeSocket { /** 欧式三叉 通电 接通电 插座*/ String useEuropesocket(); } // 欧式三叉实现类 public class EuropeSocketImpl implements EuropeSocket { @Override public String useEuropesocket() { String msg ="使用欧式三叉充电"; return msg; } }适配者(Adaptee):即中国双叉public interface ChineseSocket { /** * 使用中国双叉充电 * @return */ String useChineseSocket(); } // 中国插头的实现类 public class ChineseSocketImpl implements ChineseSocket { @Override public String useChineseSocket() { String msg="使用中国双叉充电"; return msg; } }适配器(Adapter)类:/** * 定义适配器类 中国双叉转为欧洲三叉 * */ public class ChineseAdapterEurope extends EuropeSocketImpl implements ChineseSocket { @Override public String useChineseSocket() { System.out.println("使用转换器转换完成"); return useEuropesocket(); } }电脑类public class Computer { public String useChineseSocket(ChineseSocket chineseSocket) { if(chineseSocket == null) { throw new NullPointerException("sd card null"); } return chineseSocket.useChineseSocket(); } }测试:public class Client { public static void main(String[] args) { Computer computer = new Computer(); ChineseSocket chineseSocket = new ChineseSocketImpl(); System.out.println(computer.useChineseSocket(chineseSocket)); System.out.println("------------"); ChineseAdapterEurope adapter = new ChineseAdapterEurope(); System.out.println(computer.useChineseSocket(adapter)); /** * 输出: * 使用中国双叉充电 * ------------ * 使用转换器转换完成 * 使用欧式三叉充电 */ } }上述代码就是简单的演示了适配器的使用。注:类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。三、对象适配器对象适配器”通过组合除了满足“用户期待接口”还降低了代码间的不良耦合。在工作中推荐使用“对象适配”。实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。题目还是和上面一样的哈。代码其实差异很小代码目标(Target)接口:即图中的欧式三叉public interface EuropeSocket { /** 欧式三叉 通电 接通电 插座*/ String useEuropesocket(); } // 欧式三叉实现类 public class EuropeSocketImpl implements EuropeSocket { @Override public String useEuropesocket() { String msg ="使用欧式三叉充电"; return msg; } }适配者(Adaptee):即中国双叉public interface ChineseSocket { /** * 使用中国双叉充电 * @return */ String useChineseSocket(); } // 中国插头的实现类 public class ChineseSocketImpl implements ChineseSocket { @Override public String useChineseSocket() { String msg="使用中国双叉充电"; return msg; } }适配器(Adapter)类: 就是这个适配器内做了一些更改 从继承改为了成员变量的方式public class ChineseAdapterEurope implements ChineseSocket { private EuropeSocket europeSocket; public ChineseAdapterEurope(EuropeSocket europeSocket) { this.europeSocket = europeSocket; } @Override public String useChineseSocket() { System.out.println("使用转换器转换完成"); return europeSocket.useEuropesocket(); } }电脑类public class Computer { public String useChineseSocket(ChineseSocket chineseSocket) { if(chineseSocket == null) { throw new NullPointerException("sd card null"); } return chineseSocket.useChineseSocket(); } }测试:public class Client { public static void main(String[] args) { Computer computer = new Computer(); ChineseSocket chineseSocket = new ChineseSocketImpl(); System.out.println(computer.useChineseSocket(chineseSocket)); System.out.println("------------"); //这里做了更改 EuropeSocket europeSocket=new EuropeSocketImpl(); ChineseAdapterEurope adapter = new ChineseAdapterEurope(europeSocket); System.out.println(computer.useChineseSocket(adapter)); /** * 输出: * 使用中国双叉充电 * ------------ * 使用转换器转换完成 * 使用欧式三叉充电 */ } }这就是对象适配器啦,适合于解决问题常见: 需要的东西有,但不能用,且短时间无法改造。即,使得一个功能适合不同的环境。 在开发中,系统的数据、行为都匹配,但接口不符时,可以考虑适配器。 希望复用一些现存的类,但是接口又与复用环境的要求不一致,应该考虑用适配器模式。(使用一个已经存在的类,但它的接口(即,方法),与需要的不相同时)扩展适配器模式(Adapter)可扩展为双向适配器模式,双向适配器类既可以把适配者接口转换成目标接口,也可以把目标接口转换成适配者接口。四、总结优点:客户端通过适配器可以透明地调用目标接口。复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。提高了类的复用将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。灵活性好可以让任何两个没有关联的类一起运行在很多业务场景中符合开闭原则其缺点是:适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。(如:明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难)五、自言自语你卷我卷,大家卷,什么时候这条路才是个头啊。😇(还是直接上天吧)有时候也想停下来歇一歇,一直做一个事情,感觉挺难坚持的。😁你好,如果你正巧看到这篇文章,并且觉得对你有益的话,就给个赞吧,让我感受一下分享的喜悦吧,蟹蟹。🤗如若有写的有误的地方,也请大家不啬赐教!!同样如若有存在疑惑的地方,请留言或私信,定会在第一时间回复你。持续更新中
继建造者模式后,又继续开启了代理模式啦。😁 Java设计模式系列-代理模式。你我一起坚持,让我们一起加油,还不会就一起学一学,会了咱就复习一下吧。😁 很喜欢一句话:“八小时内谋生活,八小时外谋生存”你好,如果喜欢,请一起坚持!!望别日与君相见时,君已有所成。😁共勉一张旧图,恍惚间想到旧人设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。又如找女朋友、找保姆、找工作等都可以通过找中介完成。在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间。还有因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等。1)概述:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。2)结构:代理(Proxy)模式分为三种角色:抽象角色: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。真实角色: 实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。代理(Proxy)类 : 实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。3)静态代理和动态代理根据代理的创建时期,代理模式分为静态代理和动态代理。静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。动态:在程序运行时,运用反射机制动态创建而成二、静态代理我们通过一个 客户要去买二手房的经历为例子,以前没有中介的时候,都是直接找到房东去买,现在房东忙着其他的事,没时间搞这个,房产中介就作为一个代理,帮助卖房子,再收手续费。现在我们只需要找房产中介就能搞定这件事情了。2.1、小案例先看看图:2.2、代码SellHouse (抽象角色:通过接口或抽象类声明真实主题和代理对象实现的业务方法。)public interface SellHouse { /**卖房接口方法*/ void sell(); }Landlord (实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。 )public class Landlord implements SellHouse{ @Override public void sell() { System.out.println("房东出售房子!!"); } }ProxyPoint (代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作)这里的附加操作就是收手续费啦😁public class ProxyPoint implements SellHouse{ private Landlord landlord=new Landlord(); @Override public void sell() { System.out.println("房产中介收取中介费,帮助房东卖房子,!!"); landlord.sell(); } }测试:public class Client { public static void main(String[] args) { ProxyPoint point = new ProxyPoint(); point.sell(); /** *房产中介收取中介费,帮助房东卖房子,!! * 房东出售房子!! */ } }从上面测试代码中可以看出我们直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。现在可以看到,代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。三、动态代理例子还是上面那个哈,图就不给啦接下来我们使用动态代理实现上面案例,先说说JDK提供的动态代理。Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。1、代码SellHouse (抽象角色:通过接口或抽象类声明真实主题和代理对象实现的业务方法。)public interface SellHouse { /**卖房接口方法*/ void sell(); }Landlord (实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。 )public class Landlord implements SellHouse { @Override public void sell() { System.out.println("房东出售房子!!"); } }ProxyFactory(它是代理类吗?)public class ProxyFactory { private Landlord landlord = new Landlord(); public SellHouse getProxyObject() { /** * 使用Proxy获取代理对象 newProxyInstance()方法参数说明: ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可 Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口 InvocationHandler h : 代理对象的调用处理程序 */ SellHouse sellHouse=(SellHouse) Proxy.newProxyInstance( landlord.getClass().getClassLoader(), landlord.getClass().getInterfaces(), new InvocationHandler() { /** InvocationHandler中invoke方法参数说明: proxy : 代理对象 method : 对应于在代理对象上调用的接口方法的 Method 实例 args : 代理对象调用接口方法时传递的实际参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("房产中介收取手续费"); // 执行真实对象 如果有返回值就将它返回回去 Object o = method.invoke(landlord, args); return o; } }); return sellHouse; } }昨天刚学到一招哈再回到上面那个问题哈。我们使用了JDK动态代理,ProxyFactory它是代理类?答案:并不是的。ProxyFactory不是代理模式中所说的代理类,代理类是程序在运行过程中动态的在内存中生成的类。我们可以先在测试代码中打印一下哈。System.out.println(proxyFactory.getClass()); System.out.println(house.getClass()); /** * 输出 * class com.crush.jdk_proxy.ProxyFactory * class com.sun.proxy.$Proxy0 */我们可以看到真正动态生成的代理其实是class com.sun.proxy.$Proxy0。这个才是程序运行过程中。接下来我用我昨天学到的东西,让大家一起看看,~~手法生疏 见谅见谅哈。~~😂2、动态代理分析慢慢来哈😁我们可以通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】Java 诊断工具-下载地址)Arthas官方文档 (这个的在jdk8环境下用,使用到jdk8中的一个tools.jar的工具)注意:(如果是其他的版本好像启动不了,我是电脑中有8和11,8没有配置环境变量,然后的话,就一直报错,我就将idea换成jdk8的版本,重新编译了,然后直接cmd在jdk8的环境下启动然后就还是可以)。注:为方便监控,我在测试方法中加上了一句while(ture){}。查看代理类的结构:我们将我们获得的代理类的名字com.sun.proxy.$Proxy0 通过命令 jad来反编译 就可以获得如下数据[arthas@3012]$ jad com.sun.proxy.$Proxy0 ClassLoader: +-sun.misc.Launcher$AppClassLoader@b4aac2 +-sun.misc.Launcher$ExtClassLoader@c21c27 Location: /* * Decompiled with CFR. * * Could not load the following classes: * com.crush.jdk_proxy.SellHouse */ package com.sun.proxy; import com.crush.jdk_proxy.SellHouse; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; public final class $Proxy0 extends Proxy implements SellHouse { private static Method m1; private static Method m2; private static Method m3; private static Method m0; public $Proxy0(InvocationHandler invocationHandler) { super(invocationHandler); } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); m3 = Class.forName("com.crush.jdk_proxy.SellHouse").getMethod("sell", new Class[0]); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); return; } catch (NoSuchMethodException noSuchMethodException) { throw new NoSuchMethodError(noSuchMethodException.getMessage()); } catch (ClassNotFoundException classNotFoundException) { throw new NoClassDefFoundError(classNotFoundException.getMessage()); } } public final boolean equals(Object object) { try { return (Boolean)this.h.invoke(this, m1, new Object[]{object}); } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } public final String toString() { try { return (String)this.h.invoke(this, m2, null); } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } public final int hashCode() { try { return (Integer)this.h.invoke(this, m0, null); } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } public final void sell() { try { this.h.invoke(this, m3, null); return; } catch (Error | RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new UndeclaredThrowableException(throwable); } } }去掉无用信息后:3、分析流程程序运行过程中动态生成的代理类//$Proxy0继承了Proxy实现了SellHouse public final class $Proxy0 extends Proxy implements SellHouse { private static Method m3; // 这里用的父类的构造方法 下面有 public $Proxy0(InvocationHandler invocationHandler) { super(invocationHandler); } static { m3 = Class.forName("com.crush.jdk_proxy.SellHouse").getMethod("sell", new Class[0]); return; } public final void sell() { // 可以看到这里是真正执行的方法 这里的h 是父类中的 InvocationHandler 成员 //invoke: 处理代理实例上的方法调用并返回结果。 当在与其关联的代理实例上调用方法时,将在调用处理程序上调用此方法。 this.h.invoke(this, m3, null); return; } }我们再接着看一下 Proxy关键东西哈。public class Proxy implements java.io.Serializable { protected InvocationHandler h; protected Proxy(InvocationHandler h) { Objects.requireNonNull(h); this.h = h; } } 现在我们再看一下我们的代理生成类ProxyFactorypublic class ProxyFactory { private Landlord landlord = new Landlord(); public SellHouse getProxyObject() { SellHouse sellHouse=(SellHouse) Proxy.newProxyInstance( landlord.getClass().getClassLoader(), landlord.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("房产中介收取手续费"); Object o = method.invoke(landlord, args); return o; } }); return sellHouse; } } //测试代码 public class Client { public static void main(String[] args) { ProxyFactory proxyFactory = new ProxyFactory(); SellHouse house = proxyFactory.getProxyObject(); house.sell(); } }执行流程如下:1. 在测试类中通过代理对象调用sell()方法 2. 根据多态的特性,执行的是代理类($Proxy0)中的sell()方法 3. 代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的子实现类对象的invoke方法 4. invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法GCLB的动态代理就没有继续分析啦, 一些使用稍有不同,不过还是可以去了解的哈。😁4优缺点:代理模式的主要优点有:代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;代理对象可以扩展目标对象的功能;代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性其主要缺点是:代理模式会造成系统设计中类的数量增加在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;增加了系统的复杂度;四、自言自语你卷我卷,大家卷,什么时候这条路才是个头啊。😇(还是直接上天吧)有时候也想停下来歇一歇,一直做一个事情,感觉挺难坚持的。😁你好,如果你正巧看到这篇文章,并且觉得对你有益的话,就给个赞吧,让我感受一下分享的喜悦吧,蟹蟹。🤗如若有写的有误的地方,也请大家不啬赐教!!同样如若有存在疑惑的地方,请留言或私信,定会在第一时间回复你。持续更新中
在Java设计模式-工厂模式(2)工厂方法模式 我们知道了工厂方法模式解决了简单工厂模式中的缺陷,做到了满足开闭原则,但是时代是进步的,进而又产生新的问题,工厂难道只能生产一种东西吗。我们所见到的工厂大都都是综合性的。所以就有了抽象工厂模式。旧图设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言1)概述:抽象工厂模式(Abstract Factory Pattern)隶属于设计模式中的创建型模式,用于产品族的构建。抽象工厂是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂是指当有多个抽象角色时使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体情况下,创建多个产品族中的产品对象。工厂模式中的每一个形态都是针对一定问题的解决方案,工厂方法针对的是多个产品系列结构;而抽象工厂模式针对的是多个产品族结构,一个产品族内有多个产品系列。抽象工厂模式相对于工厂方法模式来说,就是工厂方法模式是针对一个产品系列的,而抽象工厂模式是针对多个产品系列的,即工厂方法模式是一个产品系列一个工厂类,而抽象工厂模式是多个产品系列一个工厂类。如果客户端需要创建一些产品结构,而这些产品结构又分别属于不同的产品类别,则可以使用抽象工厂模式,抽象工厂模式中抽象工厂类负责定义创建对象的接口,具体这一系列对象的创建工作由实现抽象工厂的具体工厂类来完成。2)角色概述:抽象工厂模式中存在四种角色,分别是抽象工厂角色,具体工厂角色,抽象产品角色,具体产品角色。抽象工厂:提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。具体工厂:主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。抽象产品:定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。3)前文在这里再次上一篇文章中👉Java设计模式-工厂模式(2)工厂方法模式 中出现的问题再做一次扩展。原问题是:需求:设计一个咖啡店点餐系统。设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。但是现在我们咖啡店进行扩张了。现咖啡店业务发生改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,要是按照工厂方法模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生类爆炸情况。其中拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味)。所以这个案例可以使用抽象工厂模式实现。类图如下:二、代码实现1)抽象产品及具体产品:第一种产品:Coffee(第一种抽象产品类)、AmericanCoffee和LatteCoffee (具体产品类)public abstract class Coffee { public abstract void addMilk(); public abstract void addSugar(); public abstract String getName(); }public class AmericanCoffee extends Coffee { @Override public void addMilk() { System.out.println("给咖啡加奶"); } @Override public void addSugar() { System.out.println("给咖啡加糖"); } @Override public String getName() { return "美式咖啡"; } }public class LatteCoffee extends Coffee { @Override public void addMilk() { System.out.println("给咖啡加奶"); } @Override public void addSugar() { System.out.println("给咖啡加糖"); } @Override public String getName() { return "拿铁咖啡"; } }第二种产品:Dessert (第二种抽象产品 甜点) MatchaMousse、Tiramisu(具体产品类)public abstract class Dessert { public abstract void show(); }public class MatchaMousse extends Dessert{ @Override public void show() { System.out.println("抹茶慕斯"); } }public class Tiramisu extends Dessert{ @Override public void show() { System.out.println("提拉米苏"); } }2)抽象工厂 及具体工厂DessertFactory (抽象工厂) AmericanDessertFactory 和(具体工厂)public interface DessertFactory { Coffee createCoffee(); Dessert createDessert(); }public class AmericanDessertFactory implements DessertFactory { @Override public Coffee createCoffee() { return new AmericanCoffee(); } @Override public Dessert createDessert() { return new MatchaMousse(); } }public class ItalyDessertFactory implements DessertFactory { @Override public Coffee createCoffee() { return new LatteCoffee(); } @Override public Dessert createDessert() { return new Tiramisu(); } }3)测试public class Client { public static void main(String[] args) { // 想次美式东西 // AmericanDessertFactory factory = new AmericanDessertFactory(); // 想换成意大利风味,仅仅只需要换一个工厂类 其他的代码无需改变 ItalyDessertFactory factory= new ItalyDessertFactory(); Coffee coffee = factory.createCoffee(); Dessert dessert = factory.createDessert(); System.out.println(coffee.getName()); dessert.show(); } }如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。4)优缺点:抽象工厂模式除了具有工厂方法模式的优点外,其他主要优点如下。可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。当需要产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品组。抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。其缺点是:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。使用抽象工厂模式一般要满足以下条件。系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。系统一次只可能消费其中某一族产品,即同族的产品一起使用。5)使用场景:如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。三、自言自语我也不知道文章写出来是有用还是无用,只是想做一个分享。希望大家能够喜欢并且在这里能有收获。你好啊,要天天开心哦。下篇文章再见。此系列还在持续更新中.... 我一定还会回来的。😁
在Java设计模式-工厂模式(1)简单工厂模式 中我们介绍了简单工厂模式,提到了简单工厂模式违背了开闭原则,而“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。地点:湖南永州市蓝山县舜河村作者:用心笑* 😁 每天开心设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言1)概述:工厂方法(Factory Method)模式的意义是定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类当中。核心工厂类不再负责产品的创建,这样核心类成为一个抽象工厂角色,仅负责具体工厂子类必须实现的接口,这样进一步抽象化的好处是使得工厂方法模式可以使系统在不修改具体工厂角色的情况下引进新的产品。工厂方法模式是简单工厂模式的衍生,解决了许多简单工厂模式的问题。首先完全实现‘开-闭 原则’,实现了可扩展。其次更复杂的层次结构,可以应用于产品结果复杂的场合。工厂方法模式对简单工厂模式进行了抽象。有一个抽象的Factory类(可以是抽象类和接口),这个类将不再负责具体的产品生产,而是只制定一些规范,具体的生产工作由其子类去完成。在这个模式中,工厂类和产品类往往可以依次对应。即一个抽象工厂对应一个抽象产品,一个具体工厂对应一个具体产品,这个具体的工厂就负责生产对应的产品。2)角色结构:抽象工厂(Creator):是工厂方法模式的核心,与应用程序无关。任何在模式中创建的对象的工厂类必须实现这个接口。具体工厂(Concrete Creator):这是实现抽象工厂接口的具体工厂类,包含与应用程序密切相关的逻辑,并且受到应用程序调用以创建产品对象。在上图中有两个这样的角色:BulbCreator与TubeCreator。抽象产品(Product):工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。在上图中,这个角色是Light。具体产品(Concrete Product):这个角色实现了抽象产品角色所定义的接口。某具体产品有专门的具体工厂创建,它们之间往往一一对应。博主自语: 说人话就是往上再抽取一层(所以果然是没有什么是加一层不能解决的,一层不行就加两层)😁还是上次那个问题:需求:设计一个咖啡店点餐系统。设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。3)类图关系:上一文中的简单工厂类图:工厂方法类图:之前在简单工厂模式中,CoffeeStore和SimpleCoffeeFactory工厂直接进行关联,那个时候在SimpleCoffeeFactory简单工厂中还需要做一些具体的操作,但是在这里再次进行了一个抽取,将工厂分为了两层,一是抽象工厂,二是具体的实现工厂。再一次做了一个解耦操作。想了解简单工厂模式点👉Java设计模式-工厂模式(1)简单工厂模式具体还是看代码实现吧,在看文末比较总结吧😁二、代码实现1)Coffce咖啡抽象类(产品抽象类)public abstract class Coffee { public abstract void addMilk(); public abstract void addSugar(); public abstract String getName(); }2)AmericanCoffee 、LatteCoffee类(具体产品类)public class AmericanCoffee extends Coffee { @Override public void addMilk() { System.out.println("给咖啡加奶"); } @Override public void addSugar() { System.out.println("给咖啡加糖"); } @Override public String getName() { return "美式咖啡"; } }public class LatteCoffee extends Coffee { @Override public void addMilk() { System.out.println("给咖啡加奶"); } @Override public void addSugar() { System.out.println("给咖啡加糖"); } @Override public String getName() { return "拿铁咖啡"; } }3)CoffeeFactory(抽象工厂类)public interface CoffeeFactory { Coffee createCoffee(); }4)AmericanCoffeeFactory、LatteCoffeeFactory类 (具体实现工厂)public class AmericanCoffeeFactory implements CoffeeFactory { @Override public Coffee createCoffee() { return new AmericanCoffee(); } }public class LatteCoffeeFactory implements CoffeeFactory { @Override public Coffee createCoffee() { return new LatteCoffee(); } }5)咖啡店(用具体产品的客户)public class CoffeeStore { private CoffeeFactory factory; public CoffeeStore(CoffeeFactory factory) { this.factory = factory; } public Coffee orderCoffee(String type) { Coffee coffee = factory.createCoffee(); coffee.addMilk(); coffee.addSugar(); return coffee; } }6)测试我们现在来测试一下public class Client { public static void main(String[] args) { CoffeeStore coffeeStore = new CoffeeStore(new AmericanCoffeeFactory()); Coffee americano = coffeeStore.orderCoffee("americano"); System.out.println(americano.getName()); /** * 给咖啡加奶 * 给咖啡加糖 * 美式咖啡 */ } }三、总结3.1、小结:从上面的代码来看,这完美的解决了简单工厂方法中所违背的”开闭原则“,在这里如果需要再增加新的产品,只需要再写一个具体的实现工厂类就可以了,而不需要对原代码进行修改。工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。3.2、优点:用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程。灵活性增强,对于新产品的创建,只需多写一个相应的具体工厂类。无须对原工厂进行任何修改,满足开闭原则典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则。3.3、缺点:类的个数容易过多,增加复杂度增加了系统的抽象性和理解难度抽象产品只能生产一种产品,此弊端可使用抽象工厂模式解决。(下一篇文章😀持续更新中)3.4、应用场景:客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等。创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。客户不关心创建产品的细节,只关心产品的品牌四、自言自语我也不知道文章写出来是有用还是无用,只是想做一个分享。希望大家能够喜欢并且在这里能有收获。你好啊,要天天开心哦。下篇文章再见。此系列还在持续更新中....👉 我一定还会回来的。😁
今天就让我们接着学习Java设计模式中的工厂模式吧,持续更新中。让我们一起学习设计模式吧,说它是基础也是基础,说它不是,又确实不是。它穿插在各处。学好它也是为了能让自己更进一步吧。很喜欢一句话:“八小时谋生活,八小时外谋发展”。共勉封面地点:湖南省永州市蓝山县舜河村作者:用心笑*😁设计模式系列:Java设计模式-单例模式Java设计模式-工厂模式(1)简单工厂模式Java设计模式-工厂模式(2)工厂方法模式Java设计模式-工厂模式(3)抽象工厂模式Java设计模式-建造者模式Java设计模式-代理模式Java设计模式-适配器模式Java设计模式-装饰器模式Java设计模式-桥接模式Java设计模式-外观模式Java设计模式-组合模式Java设计模式-享元模式Java设计模式-模板方法模式Java设计模式-策略模式Java设计模式-责任链模式Java设计模式-中介者模式Java设计模式-观察者模式(发布/订阅模式)持续更新中...一、前言我们先别急着想工厂模式是什么样的啊先看看下面这个例子啊,怎么设计,如何写,才能更好。一步一步引出Java工厂模式。1)例子需求:设计一个咖啡店点餐系统。设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。代码是比较简单的,我这是采取一步一步引入的,如果不喜欢,可以直接看下文。我们先用曾经的方式来设计和进行代码的编写。2)类图关系3)代码实现我们先用以前的方式来实现一遍哈。1、先写好Coffee这个抽象类public abstract class Coffee { public abstract void addMilk(); public abstract void addSugar(); public abstract String getName(); }2、再写好美式咖啡和拿铁咖啡继承Coffee抽象类public class AmericanCoffee extends Coffee { @Override public void addMilk() { System.out.println("给咖啡加奶"); } @Override public void addSugar() { System.out.println("给咖啡加糖"); } @Override public String getName() { return "美式咖啡"; } }public class LatteCoffee extends Coffee { @Override public void addMilk() { System.out.println("给咖啡加奶"); } @Override public void addSugar() { System.out.println("给咖啡加糖"); } @Override public String getName() { return "拿铁咖啡"; } }3、咖啡店public class CoffeeStore { public Coffee createCoffee(String type){ Coffee coffee = null; if("americano".equals(type)) { coffee = new AmericanCoffee(); } else if("latte".equals(type)) { coffee = new LatteCoffee(); } coffee.addMilk(); coffee.addSugar(); return coffee; } }4、写个客户端来测试点咖啡哈public class Client { public static void main(String[] args) { CoffeeStore coffeeStore = new CoffeeStore(); Coffee coffee = coffeeStore.createCoffee("americano"); System.out.println(coffee.getName()); /** * 输出: * 给咖啡加奶 * 给咖啡加糖 * 美式咖啡 */ } }其实乍一看没啥问题,但是如果我这个需要增加几种咖啡,你说该如何才合适勒?是不是需要修改CoffeeStore的代码。又如果要开设美团外卖点单呢?又如何改呢?在java中,万物皆对象。如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。(而且这种重复工作简直想死)如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦。接下来就出现了简单工厂模式(简单工厂模式并非23种经典模式之内,更像是一种编程习惯吧)。😁二、简单工厂模式2.1、概述:简单工厂模式是属于创建型模式,又叫做静态工厂方法(Static Factory Method)模式,但不属于23种GOF设计模式之一。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式,可以理解为是不同工厂模式的一个特殊实现。简单工厂包含如下角色:抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。 (例子中的咖啡)具体产品 :实现或者继承抽象产品的子类 (例子中的美式咖啡、拿铁咖啡等)具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。 (一个来创建对象的工厂)使用场景工厂类负责创建的对象比较少;客户只知道传入工厂类的参数,对于如何创建对象(逻辑)不关心;2.2、类图关系:简单来说就是在原有的设计上加了一层(没有什么是加一层解决不了的,不行就加两层(狗头保命))😁2.3、代码修改:在原有基础上做了一些修改:增加一个SimpleCoffeeFactory类,在这个地方进行对象的创建。😀public class SimpleCoffeeFactory { public Coffee createCoffee(String type) { Coffee coffee = null; if("americano".equals(type)) { coffee = new AmericanCoffee(); } else if("latte".equals(type)) { coffee = new LatteCoffee(); } return coffee; } }再修改一下CoffeeStore类public class CoffeeStore { public Coffee createCoffee(String type){ SimpleCoffeeFactory factory = new SimpleCoffeeFactory(); Coffee coffee = factory.createCoffee(type); coffee.addMilk(); coffee.addSugar(); return coffee; } }看起来好像只是把创建对象的权力给到了SimpleCoffeeFactory,没有什么其他操作,但是就是这个SimpleCoffeeFactory工厂类,已经将CoffeeStore类和Coffee对象解耦了,CoffeeStore不再需要管具体产品对象是如何创建的,只需要负责自己的事情就可以了,明确了各自的职责和权利,有利于整个软件体系结构的优化当然另一方面又产生了新的耦合,CoffeeStore对象和SimpleCoffeeFactory工厂对象的耦合,工厂对象和商品对象的耦合。后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。2.4、优缺点优点:1)封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。2)工厂类根据外界给定的信息,决定究竟应该创建哪个具体类的对象.通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的.明确了各自的职责和权利,有利于整个软件体系结构的优化。缺点:当系统中的具体产品类不断增多时候,可能会出现要求工厂类根据不同条件创建不同实例的需求.这种对条件的判断和对具体产品类型的判断交错在一起,很难避免模块功能的蔓延,对系统的维护和扩展非常不利,违背了“开闭原则”。2.5、扩展-简单静态工厂在开发中也有一部分人将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式,它也不是23种设计模式中的。代码如下public class SimpleCoffeeFactory { public static Coffee createCoffee(String type) { Coffee coffee = null; if("americano".equals(type)) { coffee = new AmericanoCoffee(); } else if("latte".equals(type)) { coffee = new LatteCoffee(); } return coffe; } }2.6、扩展-简单工厂+配置文件解除耦合可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。定义一个配置文件 my.propertiesamerican=com.crush.factory.simple_factory_properties.AmericanCoffee latte=com.crush.factory.simple_factory_properties.LatteCoffee改进工厂类public class CoffeeFactory { private static Map<String,Coffee> map = new HashMap(); static { Properties p = new Properties(); InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("my.properties"); try { p.load(is); //遍历Properties集合对象 Set<Object> keys = p.keySet(); for (Object key : keys) { //根据键获取值(全类名) String className = p.getProperty((String) key); //获取字节码对象 Class clazz = Class.forName(className); Coffee obj = (Coffee) clazz.newInstance(); map.put((String)key,obj); } } catch (Exception e) { e.printStackTrace(); } } public static Coffee createCoffee(String name) { return map.get(name); } }静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及创建对象写在静态代码块中,目的就是只需要执行一次。这种方式用的也很多,常见也很简单。三、结语这个简单工厂并不完善,增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”,所以才有了后文的工厂模式、抽象工厂模式。持续更新中哦。
简单介绍一下权限表设计,也是自己去了解的一个东西。大家一起加油哦😀😀一、介绍现阶段我们知道的大概就是两种权限设计一种是基于角色的权限设计另一种是基于资源的权限设计接下来我给大家讲一讲这两种权限的区别,以及那种更好。在后面也会给出数据库里表的设计的具体代码。二、基于角色的权限设计RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权。例如:比如:主体的角色为总经理可以查 询企业运营报表,查询员工工资信息等,访问控制流程如下:根据上图中的判断逻辑,授权代码可表示如下:if(主体.hasRole("总经理角色id")){ 查询工资 }如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是 总经理或部门经理”,修改代码如下:if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){ 查询工资 }根据上边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。我们敲代码都知道的 公司中最忌修改源码 因为牵一发而动全身。所以不是非常必要 就不要随便修改原来的代码。接下来 我们看一下基于资源的权限控制的设计是什么样子吧。三、基于资源的权限设计RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须 具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:根据上图中的判断,授权代码可以表示为:if(主体.hasPermission("查询工资权限标识")){ 查询工资 }优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改 授权代码,系统可扩展性强。四、主体、资源、权限关系图主体、资源、权限相关的数据模型主体(用户id、账号、密码、...)主体(用户)和角色关系(用户id、角色id、...)角色(角色id、角色名称、...)角色和权限关系(角色id、权限id、...)权限(权限id、权限标识、权限名称、资源名称、资源访问地址、...)数据模型关系图:具体表模型SQL:user表:DROP TABLE IF EXISTS `user_db`; CREATE TABLE `user_db` ( `id` bigint(20) NOT NULL, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `fullname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `mobile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic; INSERT INTO `user_db` VALUES (1, 'admin', '123456', '张三', '123');t_role表:DROP TABLE IF EXISTS `t_role`; CREATE TABLE `t_role` ( `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `create_time` datetime(0) NULL DEFAULT NULL, `update_time` datetime(0) NULL DEFAULT NULL, `status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `unique_role_name`(`role_name`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_role -- ---------------------------- INSERT INTO `t_role` VALUES ('1', '管理员', NULL, NULL, NULL, '');t_user_role表:DROP TABLE IF EXISTS `t_user_role`; CREATE TABLE `t_user_role` ( `user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `create_time` datetime(0) NULL DEFAULT NULL, `creator` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`user_id`, `role_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_user_role -- ---------------------------- INSERT INTO `t_user_role` VALUES ('1', '1', NULL, NULL);t_permission表:DROP TABLE IF EXISTS `t_permission`; CREATE TABLE `t_permission` ( `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '权限标识符', `description` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述', `url` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求地址', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_permission -- ---------------------------- INSERT INTO `t_permission` VALUES ('1', 'p1', '测试资源\r\n1', '/r/r1'); INSERT INTO `t_permission` VALUES ('2', 'p2', '测试资源2', '/r/r2');t_role_permission表:DROP TABLE IF EXISTS `t_role_permission`; CREATE TABLE `t_role_permission` ( `role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `permission_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, PRIMARY KEY (`role_id`, `permission_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of t_role_permission -- ---------------------------- INSERT INTO `t_role_permission` VALUES ('1', '1'); INSERT INTO `t_role_permission` VALUES ('2', '2');自言自语今天又完成一篇 ✌看完这一篇 大家应该也会对权限的设计有了一些浅浅的理解吧。一起加油哦。
上次偶然间看到这个知识点,发现自己有所欠缺,就来进行查漏补缺,没法实在是卷的厉害啊。😭那么不知道你对于Spring支持的常用数据库事务传播属性和隔离级别了解的怎么样呢?要不要一起复习复习勒😁很喜欢一句话:“八小时内谋生活,八小时外谋发展”共勉👩💻描述:进来先看看风景啦,要相信会有光的哦一、事务传播属性前言:对于数据库事务ACID(原子性、一致性、隔离性、持久性)性质我想大家都是知道的,这里就不写了😁我们都知道用事务是为了保证数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。但是如果一个方法嵌套关联着其他方法勒,这该怎么算呢?当前方法及关联方法都有事务呢,或者只是其中某几个有事务,该用谁的呢?概念:事务的传播行为:一个方法运行在一个开启了事务的方法上时,当前方法是使用原来的事务还是开启一个新的事务。通过 @Transaction 注解中 propagation 来设置事务传播行为。其中事务传播行为总共有以下七种:传播属性描述REQUIRED业务方法需要在一个事务中运行。如果方法运行时,已经处在一个事务中,那么加入到该事务,否则为自己创建一个新的事务。(默认值)NOT_SUPPORTED声明方法不需要事务。如果方法没有关联到一个事务,容器不会为它开启事务。如果方法在一个事务中被调用,该事务会被挂起,在方法调用结束后,原先的事务便会恢复执行。应用场景:有数据操作处理(需要事务)+异步调用(不需要事务,挂起)REQUIRESNEW不管是否存在事务,业务方法总会为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务会被挂起,新的事务会被创建,直到方法执行结束,新事务才算结束,原先的事务才会恢复执行。MANDATORY该属性指定业务方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果业务方法在没有事务的环境下调用,容器就会抛出例外。SUPPORTS这一事务属性表明,如果业务方法在某个事务范围内被调用,则方法成为该事务的一部分。如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行。NEVER指定业务方法绝对不能在事务范围内执行。如果业务方法在某个事务中执行,容器会抛出例外,只有业务方法没有关联到任何事务,才能正常执行。例:应用于报表统计程序NESTED如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按REQUIRED属性执行.启用一个新的事务, 这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效下面写了一个小demo来让理解更加快捷一些哈。二、事务传播代码演示2.1、数据库表:注意:account表中 balance字段是设置为无符号的(即不能为负数)。2.2、代码项目就是普通Spring项目模拟的是买书的一个过程,账户余额不足,但是一次买多本的情况,一起付款。在其中再测试事务传播行为的不同,来看数据的变化。初始代码:public interface CashierService { void checkout(int userId, List<Integer> isbns); }@Service public class CashierServiceImpl implements CashierService { @Autowired BookShopService bookShopService; @Transactional @Override public void checkout(int userId, List<Integer> isbns) { for (Integer isbn : isbns) { // 调用bookShopService 买书的方法 bookShopService.purchase(userId, isbn); } } }public interface BookShopService { void purchase(int userId,int isbn); }@Service public class BookShopServiceImpl implements BookShopService { @Autowired BookShopMapper bookShopMapper; @Transactional @Override public void purchase(int userId, int isbn) { // 获取要买的图书 double bookPrice = bookShopMapper.getBookPriceByIsbn(isbn); // 更新图书的库存 bookShopMapper.updateBootStock(isbn); // 更新用户的余额 bookShopMapper.updateAccountBalance(userId,bookPrice); } }mapper层代码@Mapper @Repository public interface BookShopMapper { @Select("select price from book where isbn=#{isbn}") double getBookPriceByIsbn(int isbn); @Update("update book_stock set stock=stock-1 where isbn=#{isbn}") void updateBootStock(int isbn); @Update("update account set balance=balance-#{bookPrice} where id=#{userId}") void updateAccountBalance(@Param("userId") int userId, double bookPrice); }2.3测试:测试一:默认事务传播行为我们在void checkout(int userId, List isbns) 和void purchase(int userId, int isbn)上都加了 @Transactional目前账户为 100元,两本书的价格分别为 60和50 ,因为我们的付款过程是 使用循环 购买的,你说我们会买到一本还是一本都买不到呢?@Autowired CashierService cashierService; @Test void test(){ List<Integer> isbns = new ArrayList<>(); // 加购两本书 isbns.add(1001); isbns.add(1002); // 结账 cashierService.checkout(1,isbns); }答案当然是一本都买不到,因为@Transactional 注解 ,默认事务的传播属性是:REQUIRED,即业务方法需要在一个事务中运行。如果方法运行时,已经处在一个事务中,那么加入到该事务,否则为自己创建一个新的事务。所以实际上 void purchase(int userId, int isbn)其实和调用它的方法用的同一个事务。简单画个图:测试二:测试 -->REQUIRES_NEW属性其他代码未改变,仅在purchase 上的注解上加了点东西@Transactional(propagation = Propagation.REQUIRES_NEW).REQUIRES_NEW: 不管是否存在事务,业务方法总会为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务会被挂起,新的事务会被创建,直到方法执行结束,新事务才算结束,原先的事务才会恢复执行。你说说答案和上面是一样的莫?😀@Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void purchase(int userId, int isbn) { // 获取要买的图书 double bookPrice = bookShopMapper.getBookPriceByIsbn(isbn); // 更新图书的库存 bookShopMapper.updateBootStock(isbn); // 更新用户的余额 bookShopMapper.updateAccountBalance(userId,bookPrice); }答案是不一样的,测试一 我们实际上用的就是checkout上的事务,并没有用到 purchase 的事务,从图上也能看出来。测试二它的事务传播属性 用 图来讲是这样的啦:所以是可以买到一本书的。还有很多,意思都解释过了,没有一一测完了。三、数据库事务隔离级别3.1、数据库事务并发问题假设现在有A和B 两个事务 并发执行。1)脏读:一个事务读取到另一事务未提交的更新新据A 将某条记录的 age 值 从 20修改为30B 读取了 A 更新后的值为 30A 回滚,age值回到20B 读取到的30 的值就是一个无效的值2)不可重复读: 同一事务中,多次读取同一数据返回的结果有所不同(针对的update操作)A 读取了 age 值 为 20B 将 age 值修改为30A 再次读去age 的值为303)幻读:一个事务读取到另一事务已提交的insert数据(针对的insert操作)A 读取 学生表 中一部分数据B 向学生表中插入了 新的数据A 读取 学生表时 多出了一些行3.2 数据库隔离性数据库事务的隔离性: 数据库系统必须具有隔离并发运行各个事务的能力, 使它们不会相互影响, 避免各种并发问题.一个事务与其他事务隔离的程度称为隔离级别. 数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度, 隔离级别越高, 数据一致性就越好, 但并发性越弱在代码中,我们可以通过数据库提供了4种隔离级别:脏读不可重复读幻读Read uncommitted (读未提交)有有有Read committed(读已提交)无有有Repeatable read (重复读)无无有Serializable(序列化)无无无Oracle 默认的事务隔离级别为: READ COMMITED ,Oracle 支持的 2 种事务隔离级别:READ COMMITED, SERIALIZABLE.Mysql 默认的事务隔离级别为: REPEATABLE READ,Mysql 支持 4 种事务隔离级别.3.3、测试注: 模拟并发情况。1) 测试以下 mysql 的默认隔离级别:public interface BookShopService { void purchase(int userId,int isbn); // 测试事务隔离级别 void transactionIsolationTest(int isbn); }/** * propagation :用来设置事务传播属性 * Propagation.REQUIRED 默认事务传播属性 * * isolation :用来设置事务隔离级别 * Isolation.REPEATABLE_READ: mysql 默认事务隔离 * Isolation.READ_COMMITTED: oracle 默认事务隔离级别 */ @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.REPEATABLE_READ) @Override public void transactionIsolationTest( int isbn) { // 获取要买的图书 double bookPrice = bookShopMapper.getBookPriceByIsbn(isbn); //此处应打上断点,待代码执行完上一句后,应手动将书的价格修改一下,看读到的数据是多少 System.out.println(bookPrice); double bookPrice2 = bookShopMapper.getBookPriceByIsbn(isbn); System.out.println(bookPrice2); }测试代码特别简单,但因为我是手动模拟,得打断点、debug启动,@Autowired BookShopService bookShopService; @Test void transactionIsolationTest(){ bookShopService.transactionIsolationTest(1001); }当执行完第一个double bookPrice = bookShopMapper.getBookPriceByIsbn(isbn)语句时,应该去mysql 修改一下书的价格,这样看一下结果。这个时候再接着执行。看输出什么。最后的结果仍然是50、50。因为mysql的默认事务隔级别是可重复读,意思在这同一个事务中,可以重复读。注:因为这是直接修改数据库,其操作行为并不可取,此处只是为了模拟。其结果有时也非一定准确。四、自言自语每天进步一点点,那么很快就可以进步很多。你好,我是博主宁在春,下篇文章再见。😁😛
写这篇文章的原因还是得归咎于👇上一篇博客写了👉 SpringBoot整合Redis实现发布/订阅模式以及上上一篇博客写了👉Docker搭建Redis Cluster 集群环境我自己是认为对于每个知识点,光看了不操作是没有用的(遗忘太快...),多少得在手上用上几回才可以,才能对它加深印象。昨天搭建了Redis Cluster 集群环境,今天就来拿它玩一玩Redis 消息队列吧于是便有了这个Redis 实现消息队列的Demo,很喜欢一句话:”八小时内谋生活,八小时外谋发展“。共勉.😁地点:家里看到的云作者:😁一、前言概念消息队列:“消息队列”是在消息的传输过程中保存消息的容器。其实就是个 生产者--->消息队列<---消费者 的模型。集群就是蛮多蛮多而已。作用:主要解决应用耦合,异步消息,流量削锋等问题应用场景:异步处理,应用解耦(拆分多系统),流量削峰(秒杀活动、请求量过大)和消息通讯(发布公告、日志)四个场景。此处只演示了最简单的一个图哈。举例子:异步消息使用消息队列后消息中间件其实市面上已经有很多,如RabbitMq,RocketMq、ActiveMq、Kafka等,我拿Redis来做消息队列,其本意是1)为了熟悉Redis;2)Redis 确实可以来做简单的消息队列(狗头保命)二、前期准备就是需要个Redis,其他的倒是没啥特殊的啦。😁2.1、项目结构一普通的SpringBoot的项目...😊2.2、依赖的jar包jar 也都是一些正常的jar包哈,没啥新奇玩意。😜<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.72</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>2.3、yml配置文件分单机和集群,主要是上一篇文章带的....🙄😶单机配置文件spring: redis: database: 0 port: 6379 host: localhost password: lettuce: pool: # 连接池最大连接数(使用负值表示没有限制) max-active: 1024 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: 10000 # 连接池中的最大空闲连接 max-idle: 200 # 连接池中的最小空闲连接 min-idle: 0 # 连接超时时间(毫秒) timeout: 10000redis集群配置文件server: port: 8089 spring: application: name: springboot-redis redis: password: 1234 cluster: nodes: - IP地址:6379 - IP地址:6380 - IP地址:6381 - IP地址:6382 - IP地址:6383 - IP地址:6384 max-redirects: 3 # 获取失败 最大重定向次数 lettuce: pool: max-active: 1000 #连接池最大连接数(使用负值表示没有限制) max-idle: 10 # 连接池中的最大空闲连接 min-idle: 5 # 连接池中的最小空闲连接 #===========jedis配置方式============================================= # jedis: # pool: # max-active: 1000 # 连接池最大连接数(使用负值表示没有限制) # max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制) # max-idle: 10 # 连接池中的最大空闲连接 # min-idle: 5 # 连接池中的最小空闲连接 #三、编码3.1、config层没有什么特殊的配置,🤗import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * redis 配置类 * 1. 设置RedisTemplate序列化/返序列化 * * @author cuberxp * @since 1.0.0 * Create time 2020/1/23 0:06 */ @Configuration @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); //设置value hashValue值的序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>( Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(om); redisTemplate.setValueSerializer(serializer); redisTemplate.setHashValueSerializer(serializer); //key hasKey的序列化 redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.afterPropertiesSet(); return redisTemplate; } }3.2、信息实体类加个实体类,模拟传递信息中需要用到的实体类。import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * @author crush */ @Data @AllArgsConstructor @NoArgsConstructor public class AnnouncementMessage implements Serializable { private static final long serialVersionUID = 8632296967087444509L; private String id; /*** 内容 */ private String content; }3.3、MyThread类随项目启动而启动。import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; /** * @Author: crush * @Date: 2021-08-06 22:17 * version 1.0 * ApplicationRunner: * 用于指示 bean 在包含在SpringApplication时应该运行的SpringApplication 。 * 通俗说就是 在这个项目运行的时候,它也会自动运行起来。 */ @Component public class MyThread implements ApplicationRunner { @Autowired MessageConsumerService messageConsumerService; @Override public void run(ApplicationArguments args) throws Exception { messageConsumerService.start(); } }3.4、消费者import java.util.concurrent.TimeUnit; import com.crush.queue.entity.AnnouncementMessage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; /** * ApplicationRunner 实现这个接口可以跟随项目启动而启动 * @author crush */ @Service public class MessageConsumerService extends Thread { @Autowired private RedisTemplate<String,Object> redisTemplate; private volatile boolean flag = true; private String queueKey="queue"; private Long popTime=1000L; @Override public void run() { try { AnnouncementMessage message; // 为了能一直循环而不结束 while(flag && !Thread.currentThread().isInterrupted()) { message = (AnnouncementMessage) redisTemplate.opsForList().rightPop(queueKey,popTime,TimeUnit.SECONDS); System.out.println("接收到了" + message); } } catch (Exception e) { System.err.println(e.getMessage()); } } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }3.5、生产者import com.crush.queue.entity.AnnouncementMessage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @Service public class MessageProducerService { @Autowired private RedisTemplate<String, Object> redisTemplate; private String queueKey="queue"; public Long sendMeassage(AnnouncementMessage message) { System.out.println("发送了" + message); return redisTemplate.opsForList().leftPush(queueKey, message); } }四、测试就是简单写了一个测试代码。😝import com.crush.queue.entity.AnnouncementMessage; import com.crush.queue.service.MessageConsumerService; import com.crush.queue.service.MessageProducerService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; /** * @Author: crush * @Date: 2021-08-06 17:11 * version 1.0 */ @SpringBootTest public class MessageQueueTest { @Autowired private MessageProducerService producer; @Autowired private MessageConsumerService consumer; /** * 这个测时 的先启动主启动累, * 然后消费者可以一直在监听。 */ @Test public void testQueue2() { producer.sendMeassage(new AnnouncementMessage("1", "aaaa")); producer.sendMeassage(new AnnouncementMessage("2", "bbbb")); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } }注:这只是一个小demo ,很多细节都没有去考虑,只是一次对Redis做消息队列的初探,大家见谅。五、自言自语一次由搭建Redis Cluster集群开启的博客,终于结束了,算了好像还没,感觉下次可以多写点实用的。😂🤣不知道大家学习是什么样的,博主自己的感觉就是学了的东西,要通过自己去梳理一遍,或者说是去实践一遍,我觉得这样子,无论是对于理解还是记忆,都会更加深刻。如若有不足之处,请不啬赐教!!😁有疑惑之处,也可以留言或私信,定会第一时间回复。👩💻这篇文章就到这里啦,下篇文章再见。👉一篇文章用Redis 实现消息队列(还在写)
我自己是认为对于每个知识点,光看了不操作是没有用的(遗忘太快...),多少得在手上用上几回才可以,才能对它加深印象。昨天搭建了Redis Cluster 集群环境,今天就来拿它玩一玩Redis 发布/订阅模式吧很喜欢一句话:”八小时内谋生活,八小时外谋发展“。共勉.😁地点:😂不知道作者:L @[TOC](SpringBoot 整合Redis集群配置 实现发布/订阅模式)一、前言其实光从代码层面上讲,其实没有什么变化,主要是变化是关于Redis的配置需要更改为集群配置而已,之前接触过redis的话,那么就只需要看一下redis集群配置文件即可了。对redis实现发布/订阅感兴趣的话,那就可以接着看下去了哈。发布/订阅模式 :所谓发布/订阅模式,其实就是和你关注微信公众号一样的意思。举个例子:你订阅了两个微信公众号(一个叫青年湖南,一个叫央视新闻),假如我也订阅了青年湖南,某一天央视发布了一条新新闻,你能收到,我没有关注,则我不能收到。但是某周看青年大学习发布王冰冰叫你去学习时,你我都订阅了,就都可以收到。二、前期准备两份配置文件都有。单机也是可以的,想一起搭集群玩的可以👉Docker搭建Redis Cluster 集群环境。2.1、项目结构:2.2、依赖的jar包我这里是因为是习惯创建maven项目,然后将SpringBoot的版本抽出来,方便控制版本。<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.72</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> </dependencies>2.3 、yml配置文件单机配置文件spring: redis: database: 0 port: 6379 host: localhost password: lettuce: pool: # 连接池最大连接数(使用负值表示没有限制) max-active: 1024 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: 10000 # 连接池中的最大空闲连接 max-idle: 200 # 连接池中的最小空闲连接 min-idle: 0 # 连接超时时间(毫秒) timeout: 10000redis集群配置文件server: port: 8089 spring: application: name: springboot-redis redis: password: 1234 cluster: nodes: - IP地址:6379 - IP地址:6380 - IP地址:6381 - IP地址:6382 - IP地址:6383 - IP地址:6384 max-redirects: 3 # 获取失败 最大重定向次数 lettuce: pool: max-active: 1000 #连接池最大连接数(使用负值表示没有限制) max-idle: 10 # 连接池中的最大空闲连接 min-idle: 5 # 连接池中的最小空闲连接 #===========jedis配置方式============================================= # jedis: # pool: # max-active: 1000 # 连接池最大连接数(使用负值表示没有限制) # max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制) # max-idle: 10 # 连接池中的最大空闲连接 # min-idle: 5 # 连接池中的最小空闲连接 #三、编码3.1、config层redis配置类import com.crush.ps.subscribe.AConsumerRedisListener; import com.crush.ps.subscribe.BConsumerRedisListener; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * redis 配置类 * 1. 设置RedisTemplate序列化/返序列化 * 2. 监听消息 * @author cuberxp * @since 1.0.0 * Create time 2020/1/23 0:06 */ @Configuration @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) public class RedisConfig { @Autowired AConsumerRedisListener aConsumerRedisListener; @Autowired BConsumerRedisListener bConsumerRedisListener; @Bean public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(redisConnectionFactory); //将消息侦听器添加到(可能正在运行的)容器中。 如果容器正在运行,则侦听器会尽快开始接收(匹配)消息。 // a 订阅了 topica、topicb 两个 频道 container.addMessageListener(aConsumerRedisListener, new PatternTopic("topica")); container.addMessageListener(aConsumerRedisListener, new PatternTopic("topicb")); // b 只订阅了 topicb 频道 container.addMessageListener(bConsumerRedisListener, new PatternTopic("topicb")); return container; } @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); //设置value hashValue值的序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>( Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(om); redisTemplate.setValueSerializer(serializer); redisTemplate.setHashValueSerializer(serializer); //key hasKey的序列化 redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.afterPropertiesSet(); return redisTemplate; } }3.2、订阅者我在这边写了两个订阅者,方便演示例子罢了。A消费者import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; /** * @author crush * MessageListener : Redis中发布的消息的侦听器。 */ @Component public class AConsumerRedisListener implements MessageListener { @Autowired private RedisTemplate<String, Object> redisTemplate; /** * @param message 传递过来的信息数据 * @param pattern 频道 */ @Override public void onMessage(Message message, byte[] pattern) { doBusiness(message); } /** * 打印 message body 内容 * * deserialize 从给定的二进制数据反序列化一个对象。 * @param message */ public void doBusiness(Message message) { Object value = redisTemplate.getValueSerializer().deserialize(message.getBody()); System.out.println("A==>consumer message: " + value.toString()); } }B消费者:package com.crush.ps.subscribe; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; /** * @author crush */ @Component public class BConsumerRedisListener implements MessageListener { @Autowired private RedisTemplate<String, Object> redisTemplate; /** * @param message 传递过来的信息数据 * @param pattern 频道 */ @Override public void onMessage(Message message, byte[] pattern) { doBusiness(message); } /** * 打印 message body 内容 * * deserialize 从给定的二进制数据反序列化一个对象。 * @param message */ public void doBusiness(Message message) { Object value = redisTemplate.getValueSerializer().deserialize(message.getBody()); System.out.println("B==>consumer message: " + value.toString()); } }3.3、AnnouncementMessage实体类就是自己写的传递消息的实体类,(AnnouncementMessage 意思就是拿来模拟发布公布的实体类)import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * @author crush */ @Data @AllArgsConstructor @NoArgsConstructor public class AnnouncementMessage implements Serializable { private static final long serialVersionUID = 8632296967087444509L; /** * 公告信息id */ private String id; /** * 公告内容 */ private String content; }四、测试@SpringBootTest public class SubscribeTest { @Autowired private RedisTemplate<String, Object> redisTemplate; @Test public void testSubscribe() { String achannel = "topica"; String bchannel = "topicb"; redisTemplate.convertAndSend(achannel, "hello world"); redisTemplate.convertAndSend(bchannel, new AnnouncementMessage("1", "模拟发通告")); } }结果:输出: A==>consumer message: hello world A==>consumer message: AnnouncementMessage(id=1, content=模拟发通告) B==>consumer message: AnnouncementMessage(id=1, content=模拟发通告)因为A 消费者订阅两个频道,而B 消费者只订阅了一个频道,所以A 会多一条。注 :测试时需要把主启动类也给启动起来,方便查看输出。(主启动自己写就好了,没有什么其他的注解,普普通通的)五、自言自语不知道大家学习是什么样的,博主自己的感觉就是学了的东西,要通过自己去梳理一遍,或者说是去实践一遍,我觉得这样子,无论是对于理解还是记忆,都会更加深刻。如若有不足之处,请不啬赐教!!😁有疑惑之处,也可以留言或私信,定会第一时间回复。👩💻这篇文章就到这里啦,下篇文章再见。👉一篇文章用Redis 实现消息队列(还在写)
之前学的少,大都自己用junit 测试一遍就可以,不怎么会去用postman测试。但是此次和队友一起合作写一个前后端分离的项目,就必须使用到postman这个测试工具啦。在写权限的时候,用了token。登录成功会返回token,并且每次登录返回的token都不一样,一开始是为了安全性,根本没想测试的麻烦。一开始不会postman,只能每次都去复制,让我直接炸开了。我就感觉这么重复的事情,不用这么傻的做吧。然后就有这篇博客的产生。😂曾经我的登录接口返回的数据是这样的。我访问其他带有权限的接口的时候 每次都需要带上这个token去请求。曾经的写法改进后:接下来就是设置环境变量和使用啦。点开之后是这样的我们接着点Add然后我们在右上角选中我们刚刚写的环境变量名 再点进Tests中。 我们需要在Tests 中写一些脚本才能将值存进环境变量。接下来才是重点。我的数据格式:// 此处是设置环境变量 将pm.response.json().data.token 设置进名为 userToken的键中 pm.environment.set("userToken", pm.response.json().data.token);这个时候我们再看 右上角的环境变量 就已经存进去啦。具体使用:接下来就是怎么用啦。以前是这么写是吧。有了环境变量 并且是动态的 即使是每次刷新也不用重新更改。对了 一些常用到请求数据也可以直接存进 环境变量。像userId、或者是什么其他常用的,都可以这样做。可以省去不少事情。每套环境适用一组测试 选中那一套环境变量就是使用那一套环境变量自言自语对一个东西了解的越多,越觉得以前的自己可笑。
前言:基本上每个项目,都会有个上传文件、头像这样的需求,文件可以存储在阿里云、腾讯云、七牛云这样的对象存储服务上,但是使用这些都不能白嫖,这就让人很难受啊。然后就找到了这个Minio,感觉还是很爽的,全部由自己掌控。代码中附带详细解释,不懂的也可以留言或私信,会及时作出回复!封面地点:湖南省永州市蓝山县舜河村作者: 用心笑*👩源码仓库在文末。一、前言及环境准备minio介绍: MinIO是根据GNU Affero通用公共许可证v3.0发布的高性能对象存储。minio特点:高性能(读/写速度上高达183 GB / 秒 和 171 GB / 秒)可扩展性(扩展从单个群集开始,该群集可以与其他MinIO群集联合以创建全局名称空间, 并在需要时可以跨越多个不同的数据中心。)可存储文件类型多,视频、execl文件、图片等等都是可以的。实战的话 1)文件存储 2) 数据库文件备份等大家都使用过云存储,minio其实也差不多,只是可以更加的方便。别看我写这么多代码,其实逻辑非常简单,大家安装好minio,直接CV大法就能跑了。😀👨💻对了,如果你需要找一个判断文件类型的工具类,此文也涵盖了。🙆♂️环境准备服务器上Docker安装MInio ☞(服务器上Docker安装Minio)本地下载Minio:minio官网项目结构只要搭建好minio服务后,项目编码实际上特别简单。二、项目初始化2.1、新建一个SpringBoot 项目我想这个大家都会哈2.2、pom.xml文件<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <!--此处我用的最近更新的minio jar包--> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.2.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.6.5</version> </dependency> <!--为了兼容性 我用的是jdk11--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> </dependencies>2.3、yml文件spring: profiles: active: prodserver: port: 8085 spring: application: name: springboot-minio minio: endpoint: http://IP地址 :9000 port: 9000 accessKey: 登录账号 secretKey: 登录密码 secure: false bucket-name: commons # 桶名 我这是给出了一个默认桶名 image-size: 10485760 # 我在这里设定了 图片文件的最大大小 file-size: 1073741824 # 此处是设定了文件的最大大小2.4、完善包结构大家随自己习惯哈。(🐕保命)三、敲代码(CV大法)3.1、MinioProperties存在于config包下,此类的主要作用就是与配置文件进行绑定,方便注入以及后期维护。import io.minio.MinioClient; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author crush */ @Data @Configuration @ConfigurationProperties(prefix = "minio") public class MinioProperties { /** * 是一个URL,域名,IPv4或者IPv6地址") */ private String endpoint; /** * //"TCP/IP端口号" */ private Integer port; /** * //"accessKey类似于用户ID,用于唯一标识你的账户" */ private String accessKey; /** * //"secretKey是你账户的密码" */ private String secretKey; /** * //"如果是true,则用的是https而不是http,默认值是true" */ private boolean secure; /** * //"默认存储桶" */ private String bucketName; /** * 图片的最大大小 */ private long imageSize; /** * 其他文件的最大大小 */ private long fileSize; /** * 官网给出的 构造方法,我只是去爬了一下官网 (狗头保命) * 此类是 客户端进行操作的类 */ @Bean public MinioClient minioClient() { MinioClient minioClient = MinioClient.builder() .credentials(accessKey, secretKey) .endpoint(endpoint,port,secure) .build(); return minioClient; } }3.2、使用到的工具类FileTypeUtils :是我结合Hutool 工具包 再次封装的一个工具类,为了方便调用的返回数据。自己觉得还是挺实用的(👩🚀🤱)MinioUtil:是对minioClient操作的再一次封装。FileTypeUtils我是将文件分了大类,然后再根据准确的文件后缀名选择文件保存方式。import cn.hutool.core.io.FileTypeUtil; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.InputStream; /** * @Author: crush * @Date: 2021-07-25 22:26 * version 1.0 */ public class FileTypeUtils { private final static String IMAGE_TYPE = "image/"; private final static String AUDIO_TYPE = "audio/"; private final static String VIDEO_TYPE = "video/"; private final static String APPLICATION_TYPE = "application/"; private final static String TXT_TYPE = "text/"; public static String getFileType(MultipartFile multipartFile) { InputStream inputStream = null; String type = null; try { inputStream = multipartFile.getInputStream(); type = FileTypeUtil.getType(inputStream); System.out.println(type); if (type.equalsIgnoreCase("JPG") || type.equalsIgnoreCase("JPEG") || type.equalsIgnoreCase("GIF") || type.equalsIgnoreCase("PNG") || type.equalsIgnoreCase("BMP") || type.equalsIgnoreCase("PCX") || type.equalsIgnoreCase("TGA") || type.equalsIgnoreCase("PSD") || type.equalsIgnoreCase("TIFF")) { return IMAGE_TYPE+type; } if (type.equalsIgnoreCase("mp3") || type.equalsIgnoreCase("OGG") || type.equalsIgnoreCase("WAV") || type.equalsIgnoreCase("REAL") || type.equalsIgnoreCase("APE") || type.equalsIgnoreCase("MODULE") || type.equalsIgnoreCase("MIDI") || type.equalsIgnoreCase("VQF") || type.equalsIgnoreCase("CD")) { return AUDIO_TYPE+type; } if (type.equalsIgnoreCase("mp4") || type.equalsIgnoreCase("avi") || type.equalsIgnoreCase("MPEG-1") || type.equalsIgnoreCase("RM") || type.equalsIgnoreCase("ASF") || type.equalsIgnoreCase("WMV") || type.equalsIgnoreCase("qlv") || type.equalsIgnoreCase("MPEG-2") || type.equalsIgnoreCase("MPEG4") || type.equalsIgnoreCase("mov") || type.equalsIgnoreCase("3gp")) { return VIDEO_TYPE+type; } if (type.equalsIgnoreCase("doc") || type.equalsIgnoreCase("docx") || type.equalsIgnoreCase("ppt") || type.equalsIgnoreCase("pptx") || type.equalsIgnoreCase("xls") || type.equalsIgnoreCase("xlsx") || type.equalsIgnoreCase("zip")||type.equalsIgnoreCase("jar")) { return APPLICATION_TYPE+type; } if (type.equalsIgnoreCase("txt")) { return TXT_TYPE+type; } } catch (IOException e) { e.printStackTrace(); } return null; } }MinioUtil这个就比较多了,毕竟是对minioClient 的再次封装。代码简单,你莫慌,直接CV 完慢慢看🧜♂️import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import com.crush.minio.config.MinioProperties; import io.minio.*; import io.minio.http.Method; import io.minio.messages.DeleteObject; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import io.minio.errors.ErrorResponseException; import io.minio.messages.Bucket; import io.minio.messages.DeleteError; import io.minio.messages.Item; import lombok.SneakyThrows; /** * @Author crush * @Date 2021/7/25 11:43 */ @Component public class MinioUtil { private final MinioClient minioClient; private final MinioProperties minioProperties; public MinioUtil(MinioClient minioClient, MinioProperties minioProperties) { this.minioClient = minioClient; this.minioProperties = minioProperties; } /** * 检查存储桶是否存在 * * @param bucketName 存储桶名称 * @return */ @SneakyThrows public boolean bucketExists(String bucketName) { boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); if (found) { System.out.println(bucketName + " exists"); } else { System.out.println(bucketName + " does not exist"); } return found; } /** * 创建存储桶 * * @param bucketName 存储桶名称 */ @SneakyThrows public boolean makeBucket(String bucketName) { boolean flag = bucketExists(bucketName); if (!flag) { minioClient.makeBucket( MakeBucketArgs.builder() .bucket(bucketName) .build()); return true; } else { return false; } } /** * 列出所有存储桶名称 * * @return */ @SneakyThrows public List<String> listBucketNames() { List<Bucket> bucketList = listBuckets(); List<String> bucketListName = new ArrayList<>(); for (Bucket bucket : bucketList) { bucketListName.add(bucket.name()); } return bucketListName; } /** * 列出所有存储桶 * * @return */ @SneakyThrows public List<Bucket> listBuckets() { return minioClient.listBuckets(); } /** * 删除存储桶 * * @param bucketName 存储桶名称 * @return */ @SneakyThrows public boolean removeBucket(String bucketName) { boolean flag = bucketExists(bucketName); if (flag) { Iterable<Result<Item>> myObjects = listObjects(bucketName); for (Result<Item> result : myObjects) { Item item = result.get(); // 有对象文件,则删除失败 if (item.size() > 0) { return false; } } // 删除存储桶,注意,只有存储桶为空时才能删除成功。 minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); flag = bucketExists(bucketName); if (!flag) { return true; } } return false; } /** * 列出存储桶中的所有对象名称 * * @param bucketName 存储桶名称 * @return */ @SneakyThrows public List<String> listObjectNames(String bucketName) { List<String> listObjectNames = new ArrayList<>(); boolean flag = bucketExists(bucketName); if (flag) { Iterable<Result<Item>> myObjects = listObjects(bucketName); for (Result<Item> result : myObjects) { Item item = result.get(); listObjectNames.add(item.objectName()); } }else{ listObjectNames.add("存储桶不存在"); } return listObjectNames; } /** * 列出存储桶中的所有对象 * * @param bucketName 存储桶名称 * @return */ @SneakyThrows public Iterable<Result<Item>> listObjects(String bucketName) { boolean flag = bucketExists(bucketName); if (flag) { return minioClient.listObjects( ListObjectsArgs.builder().bucket(bucketName).build()); } return null; } /** * 文件上传 * * @param bucketName * @param multipartFile */ @SneakyThrows public void putObject(String bucketName, MultipartFile multipartFile, String filename, String fileType) { InputStream inputStream = new ByteArrayInputStream(multipartFile.getBytes()); minioClient.putObject( PutObjectArgs.builder().bucket(bucketName).object(filename).stream( inputStream, -1, minioProperties.getFileSize()) .contentType(fileType) .build()); } /** * 文件访问路径 * * @param bucketName 存储桶名称 * @param objectName 存储桶里的对象名称 * @return */ @SneakyThrows public String getObjectUrl(String bucketName, String objectName) { boolean flag = bucketExists(bucketName); String url = ""; if (flag) { url = minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) .object(objectName) .expiry(2, TimeUnit.MINUTES) .build()); System.out.println(url); } return url; } /** * 删除一个对象 * * @param bucketName 存储桶名称 * @param objectName 存储桶里的对象名称 */ @SneakyThrows public boolean removeObject(String bucketName, String objectName) { boolean flag = bucketExists(bucketName); if (flag) { minioClient.removeObject( RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build()); return true; } return false; } /** * 以流的形式获取一个文件对象 * * @param bucketName 存储桶名称 * @param objectName 存储桶里的对象名称 * @return */ @SneakyThrows public InputStream getObject(String bucketName, String objectName) { boolean flag = bucketExists(bucketName); if (flag) { StatObjectResponse statObject = statObject(bucketName, objectName); if (statObject != null && statObject.size() > 0) { InputStream stream = minioClient.getObject( GetObjectArgs.builder() .bucket(bucketName) .object(objectName) .build()); return stream; } } return null; } /** * 获取对象的元数据 * * @param bucketName 存储桶名称 * @param objectName 存储桶里的对象名称 * @return */ @SneakyThrows public StatObjectResponse statObject(String bucketName, String objectName) { boolean flag = bucketExists(bucketName); if (flag) { StatObjectResponse stat = minioClient.statObject( StatObjectArgs.builder().bucket(bucketName).object(objectName).build()); return stat; } return null; } /** * 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表 * * @param bucketName 存储桶名称 * @param objectNames 含有要删除的多个object名称的迭代器对象 * @return */ @SneakyThrows public boolean removeObject(String bucketName, List<String> objectNames) { boolean flag = bucketExists(bucketName); if (flag) { List<DeleteObject> objects = new LinkedList<>(); for (int i = 0; i < objectNames.size(); i++) { objects.add(new DeleteObject(objectNames.get(i))); } Iterable<Result<DeleteError>> results = minioClient.removeObjects( RemoveObjectsArgs.builder().bucket(bucketName).objects(objects).build()); for (Result<DeleteError> result : results) { DeleteError error = result.get(); System.out.println( "Error in deleting object " + error.objectName() + "; " + error.message()); return false; } } return true; } /** * 以流的形式获取一个文件对象(断点下载) * * @param bucketName 存储桶名称 * @param objectName 存储桶里的对象名称 * @param offset 起始字节的位置 * @param length 要读取的长度 (可选,如果无值则代表读到文件结尾) * @return */ @SneakyThrows public InputStream getObject(String bucketName, String objectName, long offset, Long length) { boolean flag = bucketExists(bucketName); if (flag) { StatObjectResponse statObject = statObject(bucketName, objectName); if (statObject != null && statObject.size() > 0) { InputStream stream = minioClient.getObject( GetObjectArgs.builder() .bucket(bucketName) .object(objectName) .offset(offset) .length(length) .build()); return stream; } } return null; } /** * 通过InputStream上传对象 * * @param bucketName 存储桶名称 * @param objectName 存储桶里的对象名称 * @param inputStream 要上传的流 * @param contentType 要上传的文件类型 MimeTypeUtils.IMAGE_JPEG_VALUE * @return */ @SneakyThrows public boolean putObject(String bucketName, String objectName, InputStream inputStream,String contentType) { boolean flag = bucketExists(bucketName); if (flag) { minioClient.putObject( PutObjectArgs.builder().bucket(bucketName).object(objectName).stream( inputStream, -1, minioProperties.getFileSize()) .contentType(contentType) .build()); StatObjectResponse statObject = statObject(bucketName, objectName); if (statObject != null && statObject.size() > 0) { return true; } } return false; } }3.3、Service层编写MinioServiceimport io.minio.messages.Bucket; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.util.List; /** * @Author crush * @Date 2021/7/25 9:58 * @Description: MinioService */ public interface MinioService { /** * 判断 bucket是否存在 * * @param bucketName * @return */ boolean bucketExists(String bucketName); /** * 创建 bucket * * @param bucketName */ void makeBucket(String bucketName); /** * 列出所有存储桶名称 * @return */ List<String> listBucketName(); /** * 列出所有存储桶 信息 * * @return */ List<Bucket> listBuckets(); /** * 根据桶名删除桶 * @param bucketName */ boolean removeBucket(String bucketName); /** * 列出存储桶中的所有对象名称 * @param bucketName * @return */ List<String> listObjectNames(String bucketName); /** * 文件上传 * * @param multipartFile * @param bucketName */ String putObject( MultipartFile multipartFile, String bucketName,String fileType); /** * 文件流下载 * @param bucketName * @param objectName * @return */ InputStream downloadObject(String bucketName, String objectName); /** * 删除文件 * @param bucketName * @param objectName */ boolean removeObject(String bucketName, String objectName); /** * 批量删除文件 * @param bucketName * @param objectNameList * @return */ boolean removeListObject(String bucketName, List<String> objectNameList); /** * 获取文件路径 * @param bucketName * @param objectName * @return */ String getObjectUrl(String bucketName,String objectName); }MinioServiceImplimport com.crush.minio.config.MinioProperties; import com.crush.minio.service.MinioService; import com.crush.minio.utils.MinioUtil; import io.minio.MinioClient; import io.minio.messages.Bucket; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.util.List; import java.util.UUID; /** * @Author crush * @Date 2021/7/25 9:58 * @Description: MinioServiceImpl */ @Service public class MinioServiceImpl implements MinioService { private final MinioUtil minioUtil; private final MinioClient minioClient; private final MinioProperties minioProperties; public MinioServiceImpl(MinioUtil minioUtil, MinioClient minioClient, MinioProperties minioProperties) { this.minioUtil = minioUtil; this.minioClient = minioClient; this.minioProperties = minioProperties; } @Override public boolean bucketExists(String bucketName) { return minioUtil.bucketExists(bucketName); } @Override public void makeBucket(String bucketName) { minioUtil.makeBucket(bucketName); } @Override public List<String> listBucketName() { return minioUtil.listBucketNames(); } @Override public List<Bucket> listBuckets() { return minioUtil.listBuckets(); } @Override public boolean removeBucket(String bucketName) { return minioUtil.removeBucket(bucketName); } @Override public List<String> listObjectNames(String bucketName) { return minioUtil.listObjectNames(bucketName); } @Override public String putObject(MultipartFile file, String bucketName,String fileType) { try { bucketName = StringUtils.isNotBlank(bucketName) ? bucketName : minioProperties.getBucketName(); if (!this.bucketExists(bucketName)) { this.makeBucket(bucketName); } String fileName = file.getOriginalFilename(); String objectName = UUID.randomUUID().toString().replaceAll("-", "") + fileName.substring(fileName.lastIndexOf(".")); minioUtil.putObject(bucketName, file, objectName,fileType); return minioProperties.getEndpoint()+"/"+bucketName+"/"+objectName; } catch (Exception e) { e.printStackTrace(); return "上传失败"; } } @Override public InputStream downloadObject(String bucketName, String objectName) { return minioUtil.getObject(bucketName,objectName); } @Override public boolean removeObject(String bucketName, String objectName) { return minioUtil.removeObject(bucketName, objectName); } @Override public boolean removeListObject(String bucketName, List<String> objectNameList) { return minioUtil.removeObject(bucketName,objectNameList); } @Override public String getObjectUrl(String bucketName,String objectName) { return minioUtil.getObjectUrl(bucketName, objectName); } }3.4、Controller层编写import com.crush.minio.service.MinioService; import com.crush.minio.utils.FileTypeUtils; import org.apache.tomcat.util.http.fileupload.IOUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author crush */ @RequestMapping("/minio") @RestController public class MinioController { private final MinioService minioService; public MinioController(MinioService minioService) { this.minioService = minioService; } @PostMapping("/upload") public String uploadFile(MultipartFile file, String bucketName) { String fileType = FileTypeUtils.getFileType(file); if (fileType != null) { return minioService.putObject(file, bucketName, fileType); } return "不支持的文件格式。请确认格式,重新上传!!!"; } @PostMapping("/addBucket/{bucketName}") public String addBucket(@PathVariable String bucketName) { minioService.makeBucket(bucketName); return "创建成功!!!"; } @GetMapping("/show/{bucketName}") public List<String> show(@PathVariable String bucketName) { return minioService.listObjectNames(bucketName); } @GetMapping("/showBucketName") public List<String> showBucketName() { return minioService.listBucketName(); } @GetMapping("/showListObjectNameAndDownloadUrl/{bucketName}") public Map<String, String> showListObjectNameAndDownloadUrl(@PathVariable String bucketName) { Map<String, String> map = new HashMap<>(); List<String> listObjectNames = minioService.listObjectNames(bucketName); String url = "localhost:8085/minio/download/" + bucketName + "/"; listObjectNames.forEach(System.out::println); for (int i = 0; i <listObjectNames.size() ; i++) { map.put(listObjectNames.get(i),url+listObjectNames.get(i)); } return map; } @DeleteMapping("/removeBucket/{bucketName}") public String delBucketName(@PathVariable String bucketName) { return minioService.removeBucket(bucketName) == true ? "删除成功" : "删除失败"; } @DeleteMapping("/removeObject/{bucketName}/{objectName}") public String delObject(@PathVariable("bucketName") String bucketName, @PathVariable("objectName") String objectName) { return minioService.removeObject(bucketName, objectName) == true ? "删除成功" : "删除失败"; } @DeleteMapping("/removeListObject/{bucketName}") public String delListObject(@PathVariable("bucketName") String bucketName, @RequestBody List<String> objectNameList) { return minioService.removeListObject(bucketName, objectNameList) == true ? "删除成功" : "删除失败"; } @RequestMapping("/download/{bucketName}/{objectName}") public void download(HttpServletResponse response, @PathVariable("bucketName") String bucketName, @PathVariable("objectName") String objectName) { InputStream in = null; try { in = minioService.downloadObject(bucketName, objectName); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(objectName, "UTF-8")); response.setCharacterEncoding("UTF-8"); //将字节从InputStream复制到OutputStream 。 IOUtils.copy(in, response.getOutputStream()); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } } }主启动没啥要改的,直接跑就欧克拉莫慌,竟然带大家做了,肯定是要带大家看看测试结果的。👇四、实战测试我目前Minio 的所含有的桶4.1、文件上传在可视化平台上也可以看到已经上传成功了。4.2、文件下载这个就是文件下载接口。4.3、其他其他的没有一一测试,但是方法命名应该可以给予你提示。五、自言自语源码:gitee-SpringBoot_Minio如若遇到错误或疑惑之处,请留言或私信,会及时给予回复。Java这条路啊,真是越往前越卷啊。🛌
受多种情况的影响,又开始看JVM 方面的知识。1、Java 实在过于内卷,没法不往深了学。2、面试题问的多,被迫学习。3、纯粹的好奇。 很喜欢一句话:“八小时内谋生活,八小时外谋发展。” --- 望别日与君相见时,君已有所成。共勉地点:湖南一个小城市邵阳作者:博主一、运行时数据区图示:补充一个点: 在运行时数据区中,灰色的为单独线程私有的,红色的为多个线程共享的,即:每个线程:独立包括程序计数器、栈、本地栈。线程间共享:堆、堆外内存(永久代或元空间、代码缓存)运行时数据区的完整图:不同的JVM对于内存的划分方式和管理机制存在着部分差异。这里给出一张完整的运行时数据区图。🤱看完上面的两张图,我想应该对JVM中所谓的运行时数据区有个大概印象了吧。下面👇会给大家再给大家带来一些粗略的讲解哈。运行时数据区概述:当我们通过前面的:类的加载-> 验证 -> 准备 -> 解析 -> 初始化 这几个阶段完成后,就会用到执行引擎对我们的类进行使用,同时执行引擎将会使用到我们运行时数据区 🤸♂️运行时数据区,Runtime Data Area,用于保存java程序运行过程中需要用到的数据和相关信息;经常说的把数据读到内存,包括类加载之后的信息,从磁盘读取文件信息等。即:==Java虚拟机在执行Java程序的过程中,会将涉及的数据划分到不同的内存区域去管理。==课间休息会二、程序计数器(Program Counter)概述:程序计数器是用于存放下一条指令所在单元的地址的地方。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。程序计数器是线程私有内存,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。代码讲解JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。例如:使用javap -c -verbose ClassCode.class 命令反编译出来结果为:从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过==CPU时间片轮转==(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。 即私有性,每个线程都拥有私有的程序计数器使用PC寄存器存储字节码指令地址有什么用呢?其实在上一段文字中已经写了,这里写个缩句哈。因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。那么 PC寄存器为什么被设定为私有的?(图解)由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。还是决定手画几张图来帮助大家来理解记忆:我想看完这个图,大家对pc 程序计数器 为什么是私有是有个大概的理解了吧。自言自语:更多的文章在后面拉,还会有的,路还长勒。人生路上选择众多,但不要害怕选择,那都是人生宝贵的财富,
受多种情况的影响,又开始看JVM 方面的知识。1、Java 实在过于内卷,没法不往深了学。2、面试题问的多,被迫学习。3、纯粹的好奇。 很喜欢一句话:“八小时内谋生活,八小时外谋发展。”--- 望别日与君相见时,君已有所成。共勉封面地点:湖南邵阳作者:博主🤸♂️🤸♂️🤸♂️一、虚拟机栈概述先给大家来看一下 运行时数据区的图示👇如果大家没咋了解Java的内存结构,就常会粗粒度地将JVM中的内存区理解为仅有Java堆(heap)和Java战(stack)?为什么?🤳🧐首先栈是运行时的单位,而堆是存储的单位栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放哪里不过今天我们讨论的是虚拟机栈。堆的文章之后才更👨💻。虚拟机栈:java虚拟机栈是线程私有的,他与线程的声明周期同步。虚拟机栈描述的是java方法执行的内存模型,每个方法执行都会创建一个栈帧,栈帧包含局部变量表、操作数栈、动态连接、方法出口等。注意:🏂它的执行速度仅次于程序计数器对于栈来说不存在垃圾回收问题主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。二、栈帧2.1、栈与栈桢:每一个方法的执行到执行完成,对应着一个栈帧在虚拟机中从入栈到出栈的过程。 👨🚀 1、java虚拟机栈栈顶的栈帧就是当前执行方法的栈帧,PC寄存器会指向该地址。👇 2、当这个方法调用其他方法的时候就会创建一个新的栈帧,这个新的栈帧会被方法Java虚拟机栈的栈顶,变为当前的活动栈,在当前只有当前活动栈的本地变量才能被使用, 3、当这个栈帧所有指令都完成的时候,这个栈帧被移除,之前的栈帧变为活动栈,前面移除栈帧的返回值变为这个栈帧的一个操作数。2.2、栈帧概述栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。每个栈帧都包含了:局部变量表操作数栈(或表达式栈)动态连接 (或指向运行时常量池的方法引用)方法返回地址(或方法正常退出或者异常退出的定义)一些额外的附加信息👩💻 在编译代码的时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。如下图:左边是通过javap -v 类名的.class 命令反编译出来的。==注意啊,得在生成目标文件夹目录下👇==我们通过上图可以看到,在编译过程中,已经给每个栈桢分配好了 操作数栈 的深度啊,局部变量表的大小等等。局部变量表是4的原因:虽然我们在这个方法中只定义了a,b,c 三个局部变量,但是大家还记得 this吗,你没有想错,确实在这个局部变量表中,第一个是this。更深层次解释不了,技术不够。😂。到局部变量部分带大家一起看。在这里插一句哈,如果大家不熟悉这种命令行去反编译的话,在这里介绍一个idea 的插件。放心哈,那插件作者肯定没给我打钱,我是感觉真的挺可的,对于我们新手学习这方面。名字: jclasslib Bytecode Viewer用法我们将一个类编译完后,兄弟们,编译没有问题吧。不行,感觉还是要贴出来哈。编译完成之后。我们打开 菜单中 -->view 选项。里面的具体的东东靠大家慢慢发掘了哈,我们还是回归正文啦。==给大家个Oracle 的JVM 官方规范。方便指令的查找解释哈。==2.3、想一想我们遇到过哪些与栈相关的异常?Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。 🏄♂️1、如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个``StackoverflowError `异常。(栈溢出)举个栗子:相信大家肯定学过递归算法,如果它一直没有出口,结果就是栈溢出。🚣♂️2、如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutOfMemoryError 异常。(也就是内存溢出异常OOM)2.4、设置栈内存大小刚刚大家也看到了,我们可以使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。-Xss1m // m 是Mb -Xss1k // k 是Kb2.5、局部变量表概述:局部变量表:Local Variables,被称之为局部变量数组或本地变量表。定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。局部变量表是线程私有的。局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。👨💻👨🚀🤹♂️🤽♂️🏌️♂️🐱🚀🐱🐉🎊🧬🚀🛫🚢🛸🚤⛲🌈🌊 悄咪咪试一波小表情,课间休息会,怕看疲惫了,就放弃继续看下去啦哈。🤗局部变量的存放 Slot(变量槽)参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。🤭局部变量表,最基本的存储单元是Slot(变量槽)局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。静态变量与局部变量的对比变量的分类:😜按数据类型分:基本数据类型、引用数据类型按类中声明的位置分:成员变量(类变量,实例变量)、局部变量类变量:linking的paper阶段,给类变量默认赋值,init阶段给类变量显示赋值即静态代码块实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值局部变量:在使用前必须进行显式赋值,不然编译不通过。🎅我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程。这意味着如果创建了局部变量,并且在使用前不对它进行显示赋值,那么将无法通过编译。在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。 🛀局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收2.6、操作数栈1、每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈,也可以称之为 表达式栈(Expression Stack)🤪操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈比如:执行复制、交换、求和等操作🤦♂️ 2、操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。3、每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。4、操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。5、操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问6、如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。public void stackTest() { int a = 10; int b = 21; int c = a + b; }0 bipush 10 // 10被压入操作数堆栈。 2 istore_1 //从操作数堆栈中弹出一个数 ,将这个数赋值给局部变量 a 这里istore_的索引之所以是一,而不是0,是因为局部变量表中,第一个放进去的是this。 static方法中 没有 this,那个时候索引才是从0开始。 3 bipush 21 5 istore_2 // 同上 6 iload_1 // iload_<n> <n > 必须是当前帧的局部变量数组的索引。< n >处的局部变量必须包含一个int. < n >处的局部变量的值被压入操作数堆栈。 7 iload_2 8 iadd // 执行 add 操作 9 istore_3 // 将结果赋值到局部变量 索引为3的变量上。 10 return我想看完这个gif动图 ,我想大家大概能够明白操作数栈是一个什么样的流程了吧,或者已经明白了吧。如果没有明白的话,可以留言评论哈。2.7、动态链接概述动态链接(Dynamic Linking): 每个栈帧都保存了 一个 可以指向当前方法所在类的 运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接。 比如:invokedynamic指令 👍在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。小思考:为什么需要运行时常量池?因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间。常量池的作用:就是为了提供一些符号和常量,便于指令的识别比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。讲这么这么多,没有亲眼见过,其实还是会对所谓的动态链接感到陌生的,因为我也是的,所以接下来👇 给大家举了栗子和图哦。1、代码部分2、通过 javap -v 类名.class 进行反编译后main 方法描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的 。注意图中 调用test 方法中的那一行指令 invokevirtual #6 // Method test:()Vinvokevirtual #6 :调用实例方法;基于类调度 。那么#6是什么意思呢? 这就牵扯到了常量池啦。我们接着来看一下 常量池(Constant pool)#6 又接着指向了 #4.#33 但其实 # 6 后面的注释已经讲出来了。// StackFrameTest.test:()V#4 是 Class, StackFrameTest 实例。#33 又接着执行#15:#9 也就是后面的注解 // test:() Vtest 说的是方法名 ()V 说的返回值是 void。链接静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期克制,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。这个动态链接只从粗略的角度讲了,里面其实还有一些内容没讲,考虑到篇幅过长,有时间会再补一篇动态链接的文章。2.8、方法返回地址存放调用该方法的pc寄存器的值。当一个方法开始执行后,只有两种方式可以退出这个方法:正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。异常完成出口 :在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。异常表:方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值2.9、一些附加信息栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。课间休息会哈 接着看题啦面试面试提问:1、这个栈内存大小是设置的越大越好吗????是的话,是为什么?不是的话,又是为什么?不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。2、垃圾回收是否涉及到虚拟机栈?不会3、方法中定义的局部变量是否线程安全?具体问题具体分析。看到这一点你可能会产生一些疑惑,我也理解。为什么会产生疑惑呢?我讲过局部变量表是线程私有的,竟然都是私有的,肯定是线程安全的啊,但是这有一个前提的,如果这个局部变量在方法内部产生,又在方法内部消亡,生命周期是和栈桢相同的,那么可以肯定是它是线程安全的。但是如果这个方法是需要接收参数,或者是需要返回值,那么这个时候就可以需要具体分析啦。自言自语兄弟们,还是一起躺平吧。内卷太累辣吧。。。 👩💻->👨💻🛌🛌
2022年08月
2022年07月
2022年05月