• 默认 全屏
  • 默认 双列 三列
宝宝图像
我可以被点击哦

林子宸的博客

松花酿酒,春水煎茶。
  • 镜像推送阿里云和私有库

    当公司有新同事入职时,我们常常会让其从git或svn上拉代码下来,并在本地跑通。若公司开发阶段没有公共的服务器,那么还需要在自己电脑上装各种环境,例如mysql、redis、nginx等。如果环境的配置很复杂,或者对环境的各个版本要求很严格,那每个人都需要花大量的时间去配置,整体效率低下,耽误下班把妹时间。docker镜像是一种轻量级、可执行的独立软件包,它包含运行某个软件所需的所有内容,我们把应用程序和配置依赖打包好形成一个可交付的运行环境(包括代码、运行时需要的库、环境变量和配置文件等),这个打包好的运行环境就是image镜像文件。所以我们完全可以把所需要用到的环境,打成一个个的镜像文件,放到服务器上。等有新同事入职,直接让他拉取这个镜像文件并运行即可,省去了繁琐的配置过程。且如果后期做服务器文件迁移,也不用再挨个去安装配置环境了,所谓一人得道全家升天,岂不美哉。1.镜像准备上面罗里吧嗦了那么多,现在就以实际案例来演示此过程。首先我们需要准备一个镜像,本文就以nginx为例。需求是我们把nginx的欢迎页改为HelloDocker,并且打包为新的镜像。这样公司每个同事拉取镜像运行后,访问的欢迎页都是HelloDocker,就无需再去手动更改了。1.1拉取镜像[root@localhost/]#dockerpullnginxUsingdefaulttag:latestlatest:Pullingfromlibrary/nginxa2abf6c4d29d:Alreadyexistsa9edb18cadd1:Pullcomplete589b7251471a:Pullcomplete186b1aaa4aa6:Pullcompleteb4df32aa5a72:Pullcompletea0bcbecc962e:PullcompleteDigest:sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31Status:Downloadednewerimagefornginx:latestdocker.io/library/nginx:latest1.2启动nginx容器[root@localhost/]#dockerrun-p80:80-dnginxf02448ffdda35c01c618f81e8ac3d8647c24aa49c68004dcbae70ff99570b3b31.3查看实例[root@localhost~]#dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMESf02448ffdda3nginx"/docker-entrypoint.…"2secondsagoUp1second0.0.0.0:80->80/tcp,:::80->80/tcpmusing_lovelace1.4访问主页以上步骤完成之后,访问服务器地址,发现来到了nginx的欢迎页。1.5更改nginx欢迎页#根据容器id进入容器[root@localhost~]#dockerexec-itf024/bin/bash#进入nginx的index.html目录root@f02448ffdda3:/#cd/usr/share/nginx/html/#更改文件内容root@f02448ffdda3:/usr/share/nginx/html#echo"HelloDocker">index.html1.6更改后的效果此时再访问服务器地址,发现nginx欢迎页已经变更为了HelloDocker1.7将容器打包为新的镜像按ctrl+p+q退出容器,然后进行打包:[root@localhost~]#dockercommit-a="linzichen"-m="修改nginx欢迎页"f024mynginxsha256:c58dc897ed2999a1a189ef1451714d8505df750c7382e85692a4d58cd12060bb[root@localhost~]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEmynginxlatestc58dc897ed2920secondsago141MBnginxlatest605c77e624dd10monthsago141MB至此,我们打包好的镜像mynginx就已经ok了。2.镜像推送到阿里云镜像打包完成后,需要推送到远程仓库,这样别人才可以拉取到。这里选择阿里云的仓库。2.1容器镜像服务在阿里云官网,找到【产品】里的【容器镜像服务】,进入控制台后,创个【个人实例】(因为在本地测试,所以选择个人,公司的话需要花money购买企业版的)。2.2创建命名空间2.3创建镜像仓库2.4相关指令以上步骤完成后,我们可以根据操作指南里的描述,将mynginx镜像推送到阿里云仓库。[root@localhost~]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEmynginxlatestc58dc897ed2920secondsago141MBnginxlatest605c77e624dd10monthsago141MB[root@localhost~]#dockerlogin--username=linzichenregistry.cn-hangzhou.aliyuncs.comPassword:WARNING!Yourpasswordwillbestoredunencryptedin/root/.docker/config.json.Configureacredentialhelpertoremovethiswarning.Seehttps://docs.docker.com/engine/reference/commandline/login/#credentials-storeLoginSucceeded#tag后面需要跟镜像的IMAGEID[root@localhost~]#dockertagc58dc897ed29registry.cn-hangzhou.aliyuncs.com/linzichen/leolinzc-rep:1.1[root@localhost~]#dockerpushregistry.cn-hangzhou.aliyuncs.com/linzichen/leolinzc-rep:1.1Thepushreferstorepository[registry.cn-hangzhou.aliyuncs.com/linzichen/leolinzc-rep]0e6258cdeb54:Pushedd874fd2bc83b:Pushed32ce5f6a5106:Pushedf1db227348d0:Pushedb8d6e692a25e:Pushede379e8aedd4d:Pushed2edcec3590a4:Pushed1.1:digest:sha256:4511d49773f3b1cc8ac350befebbf00178af7f353393f0a073497bafbaf5453dsize:17782.5验证上述操作完成之后,去阿里云镜像仓库,发现我们刚刚推送的已经成功了。2.6拉取如果要拉取仓库中的镜像,只需要根据基本信息里的【操作指南】拉取即可。[root@localhost~]#dockerpullregistry.cn-hangzhou.aliyuncs.com/linzichen/leolinzc-rep:1.11.1:Pullingfromlinzichen/leolinzc-rep[root@localhost~]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEregistry.cn-hangzhou.aliyuncs.com/linzichen/leolinzc-rep1.1c58dc897ed2958minutesago141MB2.7验证新拉取的镜像[root@localhost~]#dockerrun-p80:80-dc58dc897ed2967463bd36895d7b54d31517799c5a2073cc08c93845d927b8c26967326de9903[root@localhost~]#dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMES67463bd36895c58dc897ed29"/docker-entrypoint.…"4secondsagoUp2seconds0.0.0.0:80->80/tcp,:::80->80/tcpelastic_curran3.镜像推送到私有库有些时候我们不希望代码放到外网,想类似于gitlab似的在本地服务器创建私有仓库。所以此时我们需要在自己服务器搭建一个私有仓库。##3.1下载镜像仓库[root@localhost~]#dockerpullregistryUsingdefaulttag:latestlatest:Pullingfromlibrary/registry79e9f2f55bf5:Pullcomplete0d96da54f60b:Pullcomplete5b27040df4a2:Pullcompletee2ead8259a04:Pullcomplete3790aef225b9:PullcompleteDigest:sha256:169211e20e2f2d5d115674681eb79d21a217b296b43374b8e39f97fcf866b375Status:Downloadednewerimageforregistry:latestdocker.io/library/registry:latest3.2运行私有库Registry就是相当于在本地服务器上搭建了个dockerhub。[root@localhost~]#dockerrun-d-p5000:5000-v/usr/local/registry/:/tmp/registry--privileged=trueregistry59e4ba84a1bd4102ff4c359d6833a9706f173016facbb89af3805d2be96a983a[root@localhost~]#dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMES59e4ba84a1bdregistry"/entrypoint.sh/etc…"AboutaminuteagoUpAboutaminute0.0.0.0:5000->5000/tcp,:::5000->5000/tcphappy_bardeen3.3验证本地私服仓库[root@localhost~]#curl-XGEThttp://192.168.2.123:5000/v2/_catalog{"repositories":[]}192.168.2.123是自己服务器的地址,repositories是空的,说明还未向仓库中添加任何镜像。3.4修改镜像命名我们需要把要推送的镜像,重命名为符合私服规范的。dockertag镜像:TagHost:Port/Repository:Tag[root@localhost~]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEmynginxlatest5db4431fb6836secondsago141MB[root@localhost~]#dockertagmynginx:latest192.168.2.123:5000/mynginx:latest[root@localhost~]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEmynginxlatest5db4431fb6836minutesago141MB192.168.2.123:5000/mynginxlatest5db4431fb6836minutesago141MB3.5修改配置文件支持http刚学docker时,我们都会把docker的镜像仓库地址改为阿里云的,目的是为了加快镜像的拉取速度。所在在/etc/docker中会存在daemon.json文件:[root@localhost/]#cat/etc/docker/daemon.json{"registry-mirrors":["https://lbws5pbg.mirror.aliyuncs.com"]}vim编辑文件,配置docker允许支持以http方式推送镜像。添加"insecure-registries":["192.168.2.123:5000"],ip为自己docker服务器的ip。[root@localhost/]#cat/etc/docker/daemon.json{"registry-mirrors":["https://lbws5pbg.mirror.aliyuncs.com"],"insecure-registries":["192.168.2.123:5000"]}3.6推送镜像到私服[root@localhost/]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZE192.168.2.123:5000/mynginxlatest5db4431fb68318minutesago141MB[root@localhost/]#dockerpush192.168.2.123:5000/mynginx:latestThepushreferstorepository[192.168.2.123:5000/mynginx]3cbd1830e9c0:Pushedd874fd2bc83b:Pushed32ce5f6a5106:Pushedf1db227348d0:Pushedb8d6e692a25e:Pushede379e8aedd4d:Pushed2edcec3590a4:Pushedlatest:digest:sha256:ff4733639d1731096c730b222d9dc471f06e83efa3fb6167a6a2c2eb2e5ec718size:1778如果推送时报错,例如:[root@localhost/]#dockerpush192.168.2.123:5000/mynginx:latestThepushreferstorepository[192.168.2.123:5000/mynginx]Get"https://192.168.2.123:5000/v2/":http:servergaveHTTPresponsetoHTTPSclient则需要重启下docker,并且重新运行registory的镜像:#重启docker[root@localhost/]#systemctldaemon-reload[root@localhost/]#systemctlrestartdocker#运行registory镜像[root@localhost/]#dockerrun-d-p5000:5000-v/usr/local/registry/:/tmp/registry--privileged=trueregistry87c6adc3360f8498ae0483f739636cdf6329ad712e58550f2af59516316af6243.7验证推送是否成功#已经存在mynginx,说明推送成功[root@localhost/]#curl-XGEThttp://192.168.2.123:5000/v2/_catalog{"repositories":["mynginx"]}3.8拉取私服镜像#拉取之前[root@localhost/]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEmynginxlatest5db4431fb68328minutesago141MBnginxlatest605c77e624dd10monthsago141MBregistrylatestb8604a3fe85412monthsago26.2MB[root@localhost/]#dockerpull192.168.2.123:5000/mynginx:latestlatest:PullingfrommynginxDigest:sha256:ff4733639d1731096c730b222d9dc471f06e83efa3fb6167a6a2c2eb2e5ec718Status:Downloadednewerimagefor192.168.2.123:5000/mynginx:latest192.168.2.123:5000/mynginx:latest#拉取之后[root@localhost/]#dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZE192.168.2.123:5000/mynginxlatest5db4431fb68329minutesago141MBmynginxlatest5db4431fb68329minutesago141MBnginxlatest605c77e624dd10monthsago141MBregistrylatestb8604a3fe85412monthsago26.2MB3.9访问私服的nginx[root@localhost/]#dockerrun-p80:80-d192.168.2.123:5000/mynginx9bef626b40946e8369b3f4af7052b4e19896cff184a0292029e6410027089526[root@localhost/]#dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMES9bef626b4094192.168.2.123:5000/mynginx"/docker-entrypoint.…"4secondsagoUp1second0.0.0.0:80->80/tcp,:::80->80/tcpgoofy_babbage87c6adc3360fregistry"/entrypoint.sh/etc…"10minutesagoUp10minutes0.0.0.0:5000->5000/tcp,:::5000->5000/tcpquizzical_lumiere··

    Docker
    27
    1
    2022-11-15 00:21:15
  • 聊聊ThreadLocal

    前言在后端接口中,从接收到请求直到响应结束,我们希望能随时获取到当前用户的信息,所以一般会在网关的拦截器中解析用户token,并将解析结果临时存储起来,从而实现在整个业务中能随时读取。问题:获取用户信息后,存储在哪里?方式一:可以将解析出来的用户信息存储在request对象中,然后在业务里通过request.getAttribute()来获取。这种方式需要在每个方法形参上都加上HttpServletRequest对象,需要更改形参列表,耦合性太高,虽然可以实现,但是不推荐。方式二:创建一个全局唯一map,解析用户token后将用户信息存储到全局map中,在业务里通过map.get()获取。此方式在单线程下没有问题,但是多线程下修改的也是同一个map,会造成线程不安全问题。ThreadLocal引出针对上述方案二的问题,如果能够在每个线程内部自己维护一个map,这样每个线程之间数据彼此独立,可以做到线程隔离,就避免了线程安全问题。而ThreadLocal就可以很好地帮我们实现这个功能。Map问题代码先看下用一个全局map存储用户信息产生的问题:publicclassThreadLocalDemo{staticMap<String,User>userMap=newHashMap<>();publicstaticvoidmain(String[]args){newThread(()->{userMap.put("userInfo",newUser(1,"张三"));try{TimeUnit.SECONDS.sleep(2);}catch(InterruptedExceptione){}System.out.println(Thread.currentThread().getName()+":"+userMap.get("userInfo"));},"张三线程").start();newThread(()->{userMap.put("userInfo",newUser(2,"李四"));System.out.println(Thread.currentThread().getName()+":"+userMap.get("userInfo"));},"李四线程").start();}}classUser{Integerid;Stringname;User(Integerid,Stringname){this.id=id;this.name=name;}@OverridepublicStringtoString(){return"User{"+"id="+id+",name='"+name+'\''+'}';}}打印结果如下:李四线程:User{id=2,name='李四'}张三线程:User{id=2,name='李四'}在上面代码中,用两个线程模拟了用户同时访问系统的过程,可以发现张三线程在2S之后再获取信息时,已经被李四线程修改了,造成了线程不安全现象。ThreadLocal实现ThreadLocal类中提供了实例对象的set()和get()方法来存储当前线程的变量。所以我们可以通过其set()方法存储用户信息,用其get()方法来获取用户信息。publicclassThreadLocalDemo{staticThreadLocal<User>threadLocal=newThreadLocal<>();publicstaticvoidmain(String[]args){newThread(()->{threadLocal.set(newUser(1,"张三"));try{TimeUnit.SECONDS.sleep(2);}catch(InterruptedExceptione){}System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());},"张三线程").start();newThread(()->{threadLocal.set(newUser(2,"李四"));System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());},"李四线程").start();}}classUser{Integerid;Stringname;User(Integerid,Stringname){this.id=id;this.name=name;}@OverridepublicStringtoString(){return"User{"+"id="+id+",name='"+name+'\''+'}';}}运行结果如下:李四线程:User{id=2,name='李四'}张三线程:User{id=1,name='张三'}通过代码逻辑不难看出,张三线程先往ThreadLocal中存储了张三用户信息,没有立即取,而是等待了2秒钟,在此期间李四线程也向ThreadLocal中存储了李四用户信息。2秒钟之后张三线程获取到的仍是自己当时存储的用户,没有像Map案例似的被替换成了李四。也就是说,ThreadLocal之间的线程变量是互相隔离的。接下来就看下ThreadLocal是如何现在的。ThreadLocal分析ThreadLocal是如何做到线程之间变量隔离的。通过查看源码,我们发现在每个Thread中存在一个属性threadLocals,其类型是ThreadLocalMap。也就是说,每new一个线程实例时,都会有对应的一个ThreadLocalMap对象。而我们再调用ThreadLocal实例的的get()和set(),通过获取到当前线程的ThreadLocalMap对象,分别通过此对象的getEntry()和set()来获取和存储数据。所以说我们通过ThreadLocal实例进行存储和获取时,实际上是通过操作的每一个线程对象自身的一个ThreadLocalMap来实现的,从而实现了每个线程之间互不影响,互相隔离。ThreadLocalMap由上面可知,每一个线程通过自身的ThreadLocalMap对象来完成数据存储,所以下面主要看下ThreadLocalMap到底是什么。打开源码发现,ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用。ThreadLocalMap内部还包含了一个Entry数组,其中:Entry=ThreadLocal+value。借用网上一张图,从宏观上认识一下Thread和ThreadLocalMap及Entry的关系:从上图中看出,在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据。Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。而value就是ThreadLocal类set的数据。下面用一张图总结下引用关系:上图中除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用。Entry的key为什么设计成弱引用我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。就会存在这样的强引用链:Thread变量->Thread对象->ThreadLocalMap->Entry->key->ThreadLocal对象。那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用。弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null,在一定程度上可以减少内存泄漏问题。如下图所示:key为null的问题如果当前Thread运行结束,那么ThreadLocal、ThreadLocalMap和Entry都没有强引用与之关联了,在GC的时候都会被回收。但在实际开发中,我们一般都会采用线程池的方式来维护线程,为了复用线程是不会结束的。这样一来,每个线程的ThreadLocalMap中就会出现key为null的Entry,我们就没有办法访问这些value,且这些value会一直存在一条强引用链,造成内存泄漏。虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是value指向的T对象是无法被回收的,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev()方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove()方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。清除脏Entry实际上,线程在调用ThreadLocal的set()、get()和remove()方法时,在ThreadLocal声明周期里,针对内存泄漏问题,都会通过expungeStaleEntry()、cleanSomeSlots和replaceStaleEntry()方法来清理掉key为null的脏entry。总结ThreadLocal并不解决线程间共享数据的问题。ThreadLocal适用于变量在线程间隔离且在方法间共享的场景。ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题。每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题。ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题都会通过expungestaleEntry,cleanSomeSlots,replacestaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及Entry对象本身从而防止内存泄漏,属于安全加固的方法。群雄逐鹿起纷争,人各一份天下安!

    Java
    29
    1
    2022-11-02 00:46:04
  • JS 生成文章目录树

    我们在网页浏览文章时,会发现在页面上总有一块固定区域,用来展示文章的目录结构,以帮助我们快速定位到对应的内容。今天我们就用JavaScript来实现文章目录树的功能。功能需求1).根据文章的<h>标签生成目录树;2).点击目录,页面可以翻动到对应的位置;3).滚动页面,对应的目录可以动态高亮;实现思路假设后端返回的html数据如下:<divclass="htmlbox"><h1>标题一</h1><p>…………</p><h2>标题1.1</h2><p>…………</p><h2>标题1.2</h2><p>…………</p><h1>标题二</h1><p>…………</p><h2>标题2.1</h2><p>…………</p><h3>标题2.2.1</h3><p>…………</p><p>…………</p></div>根据文章的<h>标签生成目录树创建一个<ul>元素作为目录的容器,然后获取到.htmlbox中所有的<h>标签,通过遍历<h>获取其内容,并且生成与之对应的<li>标签,最后把<li>依次添加到<ul>中。点击目录,页面翻动到对应的位置在遍历<h>元素过程中,需要为其生成一个唯一的id,并且需要将此id通过自定义属性绑定在动态创建的<li>元素上,目的是为了在点击<li>的时候,可以根据此自定义属性找到对应的<h>标签,最后通过scrollIntoView()进行页面滚动。滚动页面,对应的目录可以动态高亮需要监听浏览器的scroll事件,获取滚动条距离页面顶部的位置。然后倒叙遍历<h>元素时,判断滚动条到浏览器顶部的距离是否>=当前<h>标签到浏览器顶部的距离。如果条件成立,我们就把此<h>对应的<li>给设置高亮。(必须要倒叙遍历,否则高亮的<li>永远都是第一个)。代码实现<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>目录</title><style>*{margin:0;padding:0;box-sizing:border-box;}/*摸你文章每个段落的高度*/p{height:500px;background-color:beige;}/*文章整体宽度*/.htmlbox{width:700px;}/*目录容器样式*/.catalogbox{position:fixed;top:10px;right:10px;width:300px;max-height:80vh;}/*高亮目录样式*/.current-catalog{background-color:#0099dd;color:#fff;}</style></head><body><divclass="htmlbox"><h1>标题一</h1><p>…………</p><h2>标题1.1</h2><p>…………</p><h2>标题1.2</h2><p>…………</p><h1>标题二</h1><p>…………</p><h2>标题2.1</h2><p>…………</p><h3>标题2.2.1</h3><p>…………</p><p>…………</p></div><scriptsrc="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script><scripttype="text/javascript">$(function(){//*************功能一:生成目录******************************vareles=['h1','h2','h3','h4','h5','h6']//获取文章里h1到h6的元素vardoms=document.querySelector('.htmlbox').querySelectorAll(eles.toString())if(!doms||!doms.length){return;}//创建目录盒子let$ul=$('<ulclass="catalogbox"></ul>')//目录的下标letindex=0for(lethofdoms){lettag=h.nodeName.toLowerCase()if(!eles.includes(tag)){continue;}//生成每个目录的id,绑定在h标签上letid=`catalog_${++index}`h.setAttribute('id',id)//获取h标签的内容lettext=h.innerHTML.replace(/<\/?[^>]+>/g,'')//生成li元素,需要绑定h的id,以便于点击目录时可以找到对应的h标签let$li=`<licatalog="${id}">${text}</li>`$ul.append($li)}$('body').append($ul)//*************功能二:点击目录滚动到对应区域******************************$('li[catalog]').on('click',function(){//获取每个li上绑定的catalog值,对应着唯一的h标签letid=$(this).attr('catalog')document.querySelector(`#${id}`).scrollIntoView({behavior:'smooth'})})//*************功能三:目录跟随滚动高亮******************************window.addEventListener('scroll',function(){//获取浏览器滚动条距离顶部的高度letscroll=document.documentElement.scrollTop||document.body.scrollTopfor(leti=doms.length-1;i>=0;i--){//倒叙遍历所有的h标签,如果滚动条的scrollTop刚刚大于h区域的offsetTop,//将此h标签对应的目录设置为高亮if(parseInt(scroll)>=Math.ceil(doms[i].offsetTop)){letid=doms[i].getAttribute("id")$('li[catalog]').each(function(){if($(this).attr('catalog')===id){$(this).addClass('current-catalog')}else{$(this).removeClass('current-catalog')}})break;}}})})</script></body></html>运行效果如下:补充:让目录有层次感不同级别的目录缩进也不同。比如一级标题<h1>我们可以设置其padding-left为10px,二级标题<h2>需要设置padding-left为20px,依次类推。实现此功能思路也很简单:在js中创建<li>的时候,获取与之对应的<h>标签中的数字,然后给<li>绑定一个样式,最后我们只需要在css中对样式进行设置即可。比如<h1>标签对应的<li>,我们可以给其添加style="--level:1",<h2>标签对应的<li>,我们可以给其添加style="--level:2",然后在css中通过var()函数去设置样式。关于var()函数,可以看CSSvar()函数一文。代码实现js://获取当前目录级别,需要根据目录级别设置cssletlevel=tag.replace('h','')//生成li元素let$li=`<listyle="--level:${level}"catalog="${id}"">${text}</li>`css:li[catalog]{padding-left:calc(var(--level)*10px);}

    JS&TS
    63
    3
    2022-10-25 01:02:55
  • 线性表之链表

    在顺序表中查询效率很高,时间复杂度是O(1),但是其增删的效率是比较低的,因为每一次增删操作都需要伴随着大量元素的移动。如果我们需要经常线性表中的元素做增删操作,可以采用链表结构。链表简介链表时一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能表示为数据元素的逻辑结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。链表由一系列的结点(每个元素被称为一个结点)组成,结点可以在运行时动态生成。根据上图,元素11的指针指向元素13,如果我们要在11和13中插入一个元素22,那我们只需要生成一个22元素,将11的指针指向22,将22的指针指向13,以此就完成了链表中插入的动作。同样的,删除也是同理,只需要修改关联元素之间的指针,就可以实现元素的动态插入和删除。结点API设计如何使用链表,根据面向对象思想,我们可以设计一个类来描述结点这个事物,用一个属性描述此结点存储的元素,用另一个属性描述此结点的下一个结点.类名Node构造方法Node(Tt,Nodenext):创建Node对象成员变量Tt:存储数据Nodenext:指向的下一个结点单向链表单向链表是链表的一种,它由多个结点组成,每个结点都有一个数据域和一个指针域,分别用来存储数据和指向其后继的结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。API设计类名LinkList构造方法LinkList(intlength):创建长度为length的LinkList对象成员方法1.publicvoidclear():空置线性表2.publicbooleanisEmpty():判断线性表是否为空3.publicintlength():获取线性表中元素个数4.publicTget(inti):获取第i个元素的值5.publicvoidinsert(Tt,inti):在线性表中第i个元素之前插入t元素6.publicvoidinsert(Tt):向线性表中添加一个元素7.publicTremove(inti):删除并返回线性表中第i个数据元素8.publicintindexOf(Tt):返回线性表中t元素首次出现的位置,若不存在返回-1成员内部类privateclassNode:结点类成员变量1.privateNodehead:存储头结点2.privateintN:链表的长度代码实现/***单向链表*@param<T>*/publicclassLinkList<T>{//记录头结点privateNodehead;//记录链表长度privateintN;/***结点对象*/privateclassNode{//当前结点数据Tt;//指向的下一个结点Nodenext;publicNode(Tt,Nodenext){this.t=t;this.next=next;}}publicLinkList(){//初始化链表时,存在一个头结点,但不存储任何数据,//next指向第一个真实数据结点this.head=newNode(null,null);this.N=0;}/***空置线性表*/publicvoidclear(){head.next=null;N=0;}/***判断线性表是否为空*@return*/publicbooleanisEmpty(){returnN==0;}/***获取元素个数*@return*/publicintlength(){returnN;}/***获取线性表中下标为i的元素*@parami*@return*/publicTget(inti){//从头结点开始遍历Nodenode=head;//i-1结点的next元素就是i处的结点for(intindex=0;index<i;++index){node=node.next;}returnnode.t;}/***向线性表中插入元素*@paramt*/publicvoidinsert(Tt){Nodenode=head;//添加到链表的最后一个元素,所以需要找到最后一个结点//让最后一个结点的next=添加的元素即可//最后一个元素的判定条件是next为空while(node.next!=null){node=node.next;}NodenewNode=newNode(t,null);node.next=newNode;N+=1;}/***向线性表中i位置插入元素*1、找到原i处的前一个结点:pre*@paramt*@parami*/publicvoidinsert(Tt,inti){//1、找到原i处的前一个结点:preNodepre=head;for(intindex=0;index<i;++index){pre=pre.next;}//2、获取原i处的结点NodeoldINode=pre.next;//3、生成新的结点,并且next指向原i处的结点NodenewNode=newNode(t,oldINode);//4、让i-1处的结点next指向新的结点pre.next=newNode;N+=1;}/***删除并返回线性表中第i个元素*就是让i-1处元素的next指向i+1位置的元素*@parami*@return*/publicTremove(inti){//1、获取i-1处的结点Nodepre=head;for(intindex=0;index<i;++index){pre=head.next;}//2、获取i结点NodeiNode=pre.next;//3.让i-1的next指向i的nextpre.next=iNode.next;N-=1;returniNode.t;}/***返回线性表中t元素首次出现的位置*若没有,返回-1*@paramt*@return*/publicintindexOf(Tt){Nodenode=head;for(inti=0;node.next!=null;++i){node=node.next;if(node.t.equals(t)){returni;}}return-1;}}链表反转现有一个链表结构如下:head->1->2->3->4我们希望反转后的结构如下:head->4->3->2->1方案一:遍历思路:由上图可知,我们需要遍历每一个结点,然后将当前结点指向其前驱结点,当遍历完成后,需要将head头结点指向遍历的最后一个结点,即可实现反转。步骤:1).需要定义两个变量pre和next,分别记录当前正在遍历结点的前驱元素和后继元素,初始值为null。2).从第一个结点(head.next)开始遍历,条件为当前遍历允许不为null。3).将当前结点的next赋值给next。4).将当前结点的next重新指向pre。5).准备开始为下轮遍历构建条件,所以需要将当前结点赋值给pre,以作为下次遍历结点的前驱元素。6).把next元素赋值给当前元素。7).循环结束后,head指向最后遍历的结点。代码:/***遍历反转*/publicvoidreverse(){//前驱元素Nodepre=null;//当前元素Nodecurrent=head.next;//后继元素Nodenext=null;while(current!=null){//记录结点的下个元素next=current.next;//让本结点指向上一个结点current.next=pre;//让上一个结点变成本结点pre=current;//让本结点变成下一个结点,再去循环current=next;}//循环结束后,head指向最后的元素head.next=pre;}方案二:递归思路步骤:1).调用reverse(Nodecurrent)方法反转每一个结点,从a结点开始。2).如果发现current还有下一个结点,则递归调用reverse(current.next)对下一个结点先进行反转。3).最终递归的出口是d结点,因为它没有后继元素了,所以此时需要让head指向元素d结点。4).让递归开始返回。代码:/***递归调用*/publicvoidreverse(){if(isEmpty()){return;}Nodenode=reverse(head.next);System.out.println("最后的结点:"+node.t);}publicNodereverse(Nodecurrent){if(current.next!=null){//开始压栈,出栈的时候,oldNext一定是d元素//递归3次:oldNext为d,current为c//递归2次,oldNext为c,current为b//递归1次,oldNext为b,current为a//调用方法,返回的current为aNodeoldNext=reverse(current.next);oldNext.next=current;current.next=null;returncurrent;}else{head.next=current;returncurrent;}}快慢指针快慢指针指的是定义两个指针,这两个指针的移动速度一块一慢,以此来制造出自己想要的差值,这个差值可以让我们找到链表上相应的结点。一般情况下,快指针的移动步长为慢指针的两倍。中间值问题如果要查找链表中的中间值,我们就可以用快慢指针。利用快慢指针,可以把链表看成一个跑到,假设a的速度是b的两倍,那么当a跑完全程后,b刚好跑一半,以此来达到找到中间结点的目的。如下图,最开始slow和fast指针都指向链表的第一个结点,然后slow每次移动一个指针,fast每次移动两个指针。/***获取中间值*@return*/publicTgetMid(){Nodefast=head.next;Nodeslow=head.next;while(fast!=null&&fast.next!=null){//变化fast和slow的值fast=fast.next.next;slow=slow.next;}returnslow.t;}单向链表是否有环使用快慢指针的思想,还是把链表比作一条跑道,链表中有环,那么这条跑道就是一条圆环跑道,在一条圆环跑道中,两个人有速度差,那么迟早两个人会相遇,只要相遇那么就说明有环。/***链表是否有环*@return*/publicbooleanisCircle(){Nodefast=head.next;Nodeslow=head.next;while(fast!=null&&fast.next!=null){//变化fast和slow的值fast=fast.next.next;slow=slow.next;//if(fast.compareTo(slow)==0){if(fast.equals(slow)){returntrue;}}returnfalse;}单向链表入口问题当快慢指针相遇时,我们可以判断到链表中有环,这时重新设定一个新指针指向链表的起点,且步长与慢指针一样为1,则慢指针与“新"指针相遇的地方就是环的入口。/***查找有环链表中环的入口结点*@return环的入口结点*/publicstaticNodegetEntrance(){//定义快慢指针Node<String>fast=head.next;Node<String>slow=head.next;Node<String>temp=null;//遍历链表,先找到环(快慢指针相遇),准备一个临时指针,指向链表的首结点,继续遍历,直到慢指针和临时指针相遇,那么相遇时所指向的结点就是环的入口while(fast!=null&&fast.next!=null){//变换快慢指针fast=fast.next.next;slow=slow.next;//判断快慢指针是否相遇if(fast.equals(slow)){temp=first;continue;}//让临时结点变换if(temp!=null){temp=temp.next;//判断临时指针是否和慢指针相遇if(temp.equals(slow)){break;}}}returntemp;}//结点类privatestaticclassNode<T>{//存储数据Titem;//下一个结点Nodenext;publicNode(Titem,Nodenext){this.item=item;this.next=next;}}循环链表循环链表,顾名思义,链表整体要形成一个圆环状。在单向链表中,最后一个结点的指针为null,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个结点的指针指向头结点即可。约瑟夫问题问题描述︰传说有这样一个故事,在罗马人占领乔塔帕特后,39个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,第一个人从1开始报数,依次往后,如果有人报数到3,那么这个人就必须自杀,然后再由他的下一个人重新从1开始报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,从而逃过了这场死亡游戏。问题转换︰41个人坐一圈,第一个人编号为1,第二个人编号为2,第n个人编号为n。1.编号为1的人开始从1报数,依次向后,报数为3的那个人退出圈;⒉.自退出那个人开始的下一个人再次从1开始报数,以此类推;3.求出最后退出的那个人的编号。图示:代码实现:publicclassJosephTest{publicstaticvoidmain(String[]args){//解决约瑟夫问题//1.构建循环链表,包含41个结点,分别存储1~41之间的值//用来就首结点Node<Integer>first=null;//用来记录前一个结点Node<Integer>pre=null;for(inti=1;i<=41;i++){//如果是第一个结点if(i==1){first=newNode<>(i,null);pre=first;continue;}//如果不是第一个结点Node<Integer>newNode=newNode<>(i,null);pre.next=newNode;pre=newNode;//如果是最后一个结点,那么需要让最后一个结点的下一个结点变为first,变为循环链表了if(i==41){pre.next=first;}}//2.需要count计数器,模拟报数intcount=0;//3.遍历循环链表//记录每次遍历拿到的结点,默认从首结点开始Node<Integer>n=first;//记录当前结点的上一个结点Node<Integer>before=null;while(n!=n.next){//模拟报数count++;//判断当前报数是不是为3if(count==3){//如果是3,则把当前结点删除调用,打印当前结点,重置count=0,让当前结点n后移before.next=n.next;System.out.print(n.item+",");count=0;n=n.next;}else{//如果不是3,让before变为当前结点,让当前结点后移;before=n;n=n.next;}}//打印最后一个元素System.out.println(n.item);}//结点类privatestaticclassNode<T>{//存储数据Titem;//下一个结点Nodenext;publicNode(Titem,Nodenext){this.item=item;this.next=next;}}}双向链表双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。实现双向链表的API及实现方式跟单向链表很像,就多维护了一个结点而已,这里不过多的去聊,直接贴上代码:importjava.util.Iterator;publicclassTowWayLinkList<T>implementsIterable<T>{//首结点privateNodehead;//最后一个结点privateNodelast;//链表的长度privateintN;//结点类privateclassNode{publicNode(Titem,Nodepre,Nodenext){this.item=item;this.pre=pre;this.next=next;}//存储数据publicTitem;//指向上一个结点publicNodepre;//指向下一个结点publicNodenext;}publicTowWayLinkList(){//初始化头结点和尾结点this.head=newNode(null,null,null);this.last=null;//初始化元素个数this.N=0;}//清空链表publicvoidclear(){this.head.next=null;this.head.pre=null;this.head.item=null;this.last=null;this.N=0;}//获取链表长度publicintlength(){returnN;}//判断链表是否为空publicbooleanisEmpty(){returnN==0;}//获取第一个元素publicTgetFirst(){if(isEmpty()){returnnull;}returnhead.next.item;}//获取最后一个元素publicTgetLast(){if(isEmpty()){returnnull;}returnlast.item;}//插入元素tpublicvoidinsert(Tt){if(isEmpty()){//如果链表为空://创建新的结点NodenewNode=newNode(t,head,null);//让新结点称为尾结点last=newNode;//让头结点指向尾结点head.next=last;}else{//如果链表不为空NodeoldLast=last;//创建新的结点NodenewNode=newNode(t,oldLast,null);//让当前的尾结点指向新结点oldLast.next=newNode;//让新结点称为尾结点last=newNode;}//元素个数+1N++;}//向指定位置i处插入元素tpublicvoidinsert(inti,Tt){//找到i位置的前一个结点Nodepre=head;for(intindex=0;index<i;index++){pre=pre.next;}//找到i位置的结点Nodecurr=pre.next;//创建新结点NodenewNode=newNode(t,pre,curr);//让i位置的前一个结点的下一个结点变为新结点pre.next=newNode;//让i位置的前一个结点变为新结点curr.pre=newNode;//元素个数+1N++;}//获取指定位置i处的元素publicTget(inti){Noden=head.next;for(intindex=0;index<i;index++){n=n.next;}returnn.item;}//找到元素t在链表中第一次出现的位置publicintindexOf(Tt){Noden=head;for(inti=0;n.next!=null;i++){n=n.next;if(n.next.equals(t)){returni;}}return-1;}//删除位置i处的元素,并返回该元素publicTremove(inti){//找到i位置的前一个结点Nodepre=head;for(intindex=0;index<i;index++){pre=pre.next;}//找到i位置的结点Nodecurr=pre.next;//找到i位置的下一个结点NodenextNode=curr.next;//让i位置的前一个结点的下一个结点变为i位置的下一个结点pre.next=nextNode;//让i位置的下一个结点的上一个结点变为i位置的前一个结点nextNode.pre=pre;//元素的个数-1N--;returncurr.item;}@OverridepublicIterator<T>iterator(){returnnewTIterator();}privateclassTIteratorimplementsIterator{privateNoden;publicTIterator(){this.n=head;}@OverridepublicbooleanhasNext(){returnn.next!=null;}@OverridepublicObjectnext(){n=n.next;returnn.item;}}}链表复杂度分析ge(inti):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为O(n);insert(inti,Tt);每一次插入,需要先找到位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n);remove(inti)每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n);相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作..同时它并没有涉及的元素的交换。相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。

    数据结构与算法
    21
    0
    2022-10-20 11:50:34
  • vue动态生成路由及常见问题

    在一些常见的RBAC系统中,对于角色和权限的管理是极其重要的。一个人可以拥有多个角色,而一个角色又会被赋予多个权限。不同的角色在登录后台系统后,看到的系统菜单也是不同的。在前后端不分离的项目中,后端可以整合SpringSecurity、Shiro等安全框架对页面元素进行标签化管理。但是随着前后分离模式的普及,我们现常常把展示逻辑放到前端来完成。本文就详细聊一聊在vue中如何实现对于菜单的动态控制。两种方案目前项目中对于角色菜单的控制,常见的有两种方案。1、前端记录所有路由,通过角色动态控制思路:前端在router.js中,定义出所有页面的路由,在每个router节点上,我们给增加一个角色属性,例如:由上图发现,我们自定义了一个属性roles,用户在登录系统后,后端返回的用户信息中,就有用户的角色。前端需要判断每个路由节点的roles数组中,是否包含这个角色,如果包含,就保留此router,否则就过滤掉。最后前端只需要将过滤完成后的路由展示在页面上即可。优缺点:此种方法优点是比较简单易实现,在角色固定的情况下可以考虑采取。但缺点是不灵活,如果后期增加了新的角色,我们需要修改代码。所以此种方式不是本文聊的重点2、后端返回路由数据,前端动态生成此方案也是目前项目中最常用的,但是需要跟后端有较强的约定性。2.1思路分析由于路由是动态生成的,所以我们先看一下如果要生成一个路由,需要哪些条件,我们一个常见的路由节点如下:我们发现,一个基本的路由节点,包含path、name、component、meta属性,如果是多级菜单,那么还需要children数组属性,在children中又是同样属性的路由节点对象。所以我们需要跟后端约定好,返回的路由中必须要存在这些字段。其中有个特殊的字段component,这个代表path路径指向的页面组件。所以后端在返回这个字段的时候,其值必须要跟前端组件的路径位置保持一致。例如前端有个组件的路径是@/views/user/index.vue,其中,/user/index是标记我们这个组件的唯一位置,所以后端返回的component值中,必须存在/user/index,这样前端才能根据这个路径,找到对应的组件。在跟后端约定好规则后,接下来我们看一下具体的实现步骤:1、在router.js中,定义公共的路由。比如登录、首页、404等路由,因为这些页面是所有角色都会存在的,所以就没有必要后端返回了。2、用户登录后,随即会跳转首页。所以我们需要在跳转页面之前,调用后端接口,获取该用户的路由信息。而监听路由跳转,一般是在路由守卫中实现。3、在路由守卫中,获取用户路由信息,动态生成路由数据,并与router.js定义的公共路由数据进行合并。4、将合并后的路由存储到vuex或者sessionStoreate中。5、将存储的路由数据遍历展示到页面上。2.2代码实现项目模板是基于花裤衩大神的vue-admin-template,gitee地址。前端项目页面组件位置如下:需求是我们希望红框中的路由是动态加载的。2.2.1定义公共路由在router/index.js中已经帮我们写了很多路由信息,为了跟上图路由保持一直,简化为以下(path为*的路由一定放最后):2.2.2获取路由接口数据假设后端定义getRouters为获取路由接口,那么它返回的数据应该是:[{"path":"/order","component":"order/index","name":"Order","meta":{"title":"订单管理"}},{"path":"/user","component":"user/index","name":"User","meta":{"title":"用户管理"},"children":[{"path":"/user/list","component":"user/components/list","name":"UserList","meta":{"title":"用户列表"}},{"path":"/user/consume","component":"user/components/consume","name":"UserConsume","meta":{"title":"消费记录"}}]}]2.2.3动态生成路由我们从原框架代码的逻辑中不难看出,用户在登录成功后,会跳转到首页,在views/login/index.vue中:而在src/permission.js文件中,框架配置了路由守卫,里面的逻辑也不复杂,这里简单说一下。去查看用户登录的逻辑,发现会在登录成功后,在cookie中存储一条token。框架这里通过getToken()方法来获取用户token。如果存在token信息,且如果当前路径是login登录页的话,会强制给回到首页;如果不是login路径,则会从vuex中获取用户的基本信息,如果vuex中存在则直接放行,否则会重新调用获取用户信息的方法,如果途中发生异常,则会强制返回loing页面。if(hasGetUserInfo)判断是为了避免重复获取用户信息,因为每次路由跳转都会走这段逻辑,如果不加判断,则每次都会去后端请求。如果不存在token信息,且访问的路径是非白名单路径,则强制退出返回login页面。由以上逻辑可知,我们获取用户路由信息的时机应该在跳转主页面之前,且vuex中的数据刷新页面后就没有了,页面刷新后会根据当前地址栏的路径进行路由跳转,也会走这段代码,所以应该在这里写我们的逻辑。src/permission.js文件代码:importrouterfrom'./router'importstorefrom'./store'importNProgressfrom'nprogress'//progressbarimport'nprogress/nprogress.css'//progressbarstyleimport{getToken}from'@/utils/auth'//gettokenfromcookieimportgetPageTitlefrom'@/utils/get-page-title'import{getRouters}from'@/api/user'import{constantRoutes}from'@/router'NProgress.configure({showSpinner:false})//NProgressConfigurationconstwhiteList=['/login']//noredirectwhitelistrouter.beforeEach(async(to,from,next)=>{//startprogressbarNProgress.start()//setpagetitledocument.title=getPageTitle(to.meta.title)//determinewhethertheuserhasloggedinconsthasToken=getToken()if(hasToken){if(to.path==='/login'){//ifisloggedin,redirecttothehomepagenext({path:'/'})NProgress.done()}else{//我们把最终的路由信息存储在vuex中,//这里加判断的原因也是为了避免每次路由跳转时都会重新获取一次//且页面刷新后vuex数据会消失if(store.getters.routers.length){//如果存在路由信息,则直接放行next();}else{//如果不存在,则重新获取//getRouters是定义在api中的接口,需要import进来getRouters(hasToken).then(res=>{//生成动态路由节点constdynamicRouters=handleRouter(res)//我们需要把动态生成的路由作为Layout组件的子路由,而Layout组件在常量路由数组中//倒数第二个元素,所以constantRoutes[constantRoutes.length-2]目的是获取Layout路由节点,//并将动态路由合并到Layout的子节点中constantRoutes[constantRoutes.length-2].children.push(...dynamicRouters)//将最终的路由信息保存到vuex中,保存完成后,再添加到router对象中。store.dispatch('router/setRouters',constantRoutes).then(()=>{router.addRoutes(store.getters.routers)})next()})}//consthasGetUserInfo=store.getters.name//if(hasGetUserInfo){//next()//}else{//try{////getuserinfo//awaitstore.dispatch('user/getInfo')//next()//}catch(error){////removetokenandgotologinpagetore-login//awaitstore.dispatch('user/resetToken')//Message.error(error||'HasError')//next(`/login?redirect=${to.path}`)//NProgress.done()//}//}}}else{/*hasnotoken*/if(whiteList.indexOf(to.path)!==-1){//inthefreeloginwhitelist,godirectlynext()}else{//otherpagesthatdonothavepermissiontoaccessareredirectedtotheloginpage.next(`/login?redirect=${to.path}`)NProgress.done()}}})/***组装动态路由函数*@param{*}routerList*@returns最终的动态路由数组*/consthandleRouter=(routerList)=>{constrouters=[]for(constrouterofrouterList){constnode={path:router.path,component:(resolve)=>require([`@/views/${router.component}.vue`],resolve),name:router.name,meta:router.meta}//如果当前路由节点存在子路由,需要递归组装数据if(router.children&&router.children.length){node.children=handleRouter(router.children)}routers.push(node)}returnrouters}router.afterEach(()=>{NProgress.done()})2.2.4关于route的state此框架的vuex是模块化管理,所以我们在src/store/modules中新建一个路由的router.js文件:conststate={routers:[]}constmutations={SET_ROUTERS:(state,routers)=>{state.routers=routers}}constactions={setRouters({commit},routers){returnnewPromise((resolve)=>{commit('SET_ROUTERS',routers)resolve()})}}exportdefault{namespaced:true,state,mutations,actions}在src/store/index.js中管理router的state:importVuefrom'vue'importVuexfrom'vuex'importgettersfrom'./getters'importappfrom'./modules/app'importsettingsfrom'./modules/settings'importuserfrom'./modules/user'importrouterfrom'./modules/router'Vue.use(Vuex)conststore=newVuex.Store({modules:{app,settings,user,router},getters})exportdefaultstore因为我们最终菜单获取的路由数据是从vuex获取的,所以我们需要在src/store/getters.js中对外提供路由数据:constgetters={sidebar:state=>state.app.sidebar,device:state=>state.app.device,token:state=>state.user.token,avatar:state=>state.user.avatar,name:state=>state.user.name,routers:state=>state.router.routers}exportdefaultgetters2.2.5从vuex中获取路由,并展示在页面上根据框架代码,我们发现有关菜单侧边栏的内容,都在src/layout/components/Sidebar/index.vue组件中,我们把其计算属性中的routes改为从vuex中获取。至此,我们动态生成路由的代码就已经实现了。常见问题1.用require而不用import我们在根据字符串生成路由组件的时候,要用require而不是import,否则会报以下错误信息:2.侧边栏消失在点击动态生成的路由时,页面可以正常跳转,但是测试边不见了。是因为动态生成的路由没有作为Layout组件的子组件,应该添加到Layout组件的children数组中。3.页面无内容在点击二级菜单的时候,页面是空白的,没有任何内容。以上面案例为例,我们只需要在对应组件的父目录下,新建index.vue文件,写入<router-view/>即可。4.二级菜单展示不全这个问题不属于动态路由的问题,elementui框架中,如果子菜单只有一个,那么就不会生成多级菜单的形式。

    Vue&React / BUG集
    79
    2
    2022-10-16 02:39:28
  • React脚手架配置别名(非eject)

    前言之前在用react脚手架create-react-app写项目时,发现在引入自定义组件或js的时候,不能够像vue似的通过别名@来引入,如果涉及层级过深,那么一堆的相对路径../../也是颇为头痛,且及不雅观。使用create-react-app初始化项目,不会有webpack的配置项。如果想手动修改配置,需要通过react-scripteject把webpack的配置文件分解到config文件夹,此过程是不可逆的。如果只是修改一些简单的配置,则推荐使用第三方依赖react-app-rewired,其作用是帮助我们重写react脚手架的配置。通过rewired修改路径别名安装react-app-rewirednpminstallreact-app-rewired--save-dev创建config-overrides.js文件在项目根目录下创建一个config-overrides.js文件+--your-project|+--config-overrides.js|+--node_modules|+--package.json|+--public|+--README.md|+--src提供路径配置:constpath=require('path');functionresolve(dir){returnpath.join(__dirname,'.',dir)}module.exports=functionoverride(config,env){config.resolve.alias={'@':resolve('src')}returnconfig;}替换package.json中scripts执行部分"scripts":{"start":"react-app-rewiredstart","build":"react-app-rewiredbuild","test":"react-app-rewiredtest","eject":"react-scriptseject"},注意:不用替换eject部分。工具中没有针对eject的配置替换,执行了eject命令会让工具失去作用。至此,就可以在项目中使用别名@来代表scr路径了。其他配置更多详细配置可参考官方github地址:react-app-rewired

    Vue&React
    43
    3
    2022-10-14 11:32:50
  • 线性表之顺序表

    顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素,使得线性表中在逻辑结构上相邻的元素存储在相邻的物理存储单元中,即通过元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。API设计类名SequenceList构造方法SequenceList(intlength):创建长度为length的SequenceList对象成员方法1.publicvoidclear():空置线性表2.publicbooleanisEmpty():判断线性表是否为空3.publicintlength():获取线性表中元素个数4.publicTget(inti):获取第i个元素的值5.publicvoidinsert(Tt,inti):在线性表中第i个元素之前插入t元素6.publicvoidinsert(Tt):向线性表中添加一个元素7.publicTremove(inti):删除并返回线性表中第i个数据元素8.publicintindexOf(Tt):返回线性表中t元素首次出现的位置,若不存在返回-1成员变量1.privateT[]eles:存储元素的数组2.privateintN:当前线性表的长度基本操作构造方法初始化表publicclassSequenceList<T>{//存储元素数组privateT[]eles;//记录顺序表中元素个数privateintN;publicSequenceList(intlength){//初始化数组eles=(T[])newObject[length];//初始化长度N=0;}}空置线性表publicvoidclear(){N=0;}判断是否为空publicbooleanisEmpty(){returnN==0;}获取元素个数publicintlength(){returnN;}返回指定位置的元素publicTget(inti){if(N==0||i>=N){thrownewRuntimeException("error");}returneles[i];}返回元素首次位置的下标publicintindexOf(Tt){for(inti=0;i<N;i++){if(eles[i].equals(t)){returni;}}return-1;}添加、插入、删除元素在初始化线性表时,给了固定的长度。因为是用数组实现的,所以在添加元素时,我们需要判断元素的数量是否等于数组的初始化长度,若等于,则需要为数组扩容。在删除元素时,我们也需要判断数组中元素数量是否远远小于数组长度,如果是,则需要减少数组的长度,避免空间浪费。扩容:缩减:当我们发现数据元素不足数组容量的1/4时,那么需要创建一个是原数组容量的1/2的新数组来存储元素。所以我们需要提供一个动态改变数组长度的方法。数组扩容privatevoidresize(intnewLength){//定义一个临时数组,指向原数组T[]temp=eles;//创建新数组eles=(T[])newObject[newLength];//把原数组的数据拷贝到新数组即可for(inti=0;i<N;i++){eles[i]=temp[i];}}添加元素添加元素时需要判断数组长度是否需要扩容。publicvoidinsert(Tt){if(N==eles.length){resize(N*2);}eles[N++]=t;}在指定位置插入元素插入元素时需要判断数组长度是否需要扩容。publicvoidinsert(Tt,inti){if(N==eles.length){resize(N*2);}//从最后一个元素倒叙一直到i,均往后移动一个位置//也就是说,我们需要在让数组下标[length]的位置,赋值为原[length-1]的值//让原[length-1]位置的元素,赋值为[length-2]的值//依次类推,一直到原[i+1]位置的元素赋值为原[i]即可for(intindex=N;index>i;index--){eles[index]=eles[index-1];}//将t元素放到i索引处eles[i]=t;//元素个数加1N++;}删除指定位置的元素删除元素时需要判断数组长度是否需要缩减。publicTremove(inti){if(N==0||i>=N){thrownewRuntimeException("error");}//把要删除的元素记录下来Tcurrent=eles[i];//i下标后面的元素,均往前移动一个位置//也就是把原先i位置的元素,替换为i+1//以此类推,一直到把length-2位置的元素,替换为length-1即可for(intindex=i;index<N-1;index++){eles[index]=eles[index+1];}//元素个数减1N--;//判断是否需要减少数组长度if(N<eles.length/4){resize(eles.length/2);}returncurrent;}遍历线性表只需要让我们自定义SequenceList<T>类实现Iterable<T>接口并重写iterator()方法即可。@OverridepublicIterator<T>iterator(){returnnewIterator<T>(){//当前元素指针privateintcursor;@OverridepublicbooleanhasNext(){//当指针移动到N的位置时,说明已经没有下个元素了,返回falsereturncursor<N;}@OverridepublicTnext(){//返回元素并且让指针后移一位returneles[cursor++];}};}时间复杂度及优缺点get(i):不难看出,不论数据元素量N有多大,只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1);insert(inti,Tt)每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为O(n);remove(inti):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大移动的元素也越多,时间复杂度为O(n);由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显。所以从以上分析得出,顺序表在通过索引查找时效率极高,在插入或者删除时效率极差。ArrayList源码分析是否是数组从构造方法不难看出,ArrayList底层也是维护了一个数组,且初始化长度是10个元素。是否扩容从add()方法一直追溯下去,发现底层也是通过new一个新的数组来实现扩容机制的。是否提供遍历功能同样我们发现其也是通过一个内部类实现了Iterator接口来提供遍历功能。#总结1、顺序表底层通过数组实现,在磁盘中开辟一块连续的空间来存储。元素物理上的相邻关系就是逻辑上的相邻关系。2、通过索引查找元素时间负责度是O(1),所以顺序表在下标查找元素时效率很高。3、在插入或删除元素时因为需要动态扩容或缩减,非线性化操作,效率极低。4、ArrayList底层也是通过数组实现,且在jdk11中默认初始化大小是10个元素。

    数据结构与算法
    34
    0
    2022-10-13 00:03:44
  • thymeleaf 整合 pjax 无刷新跳转

    在一些需要做seo优化的应用里,比如门户网站、博客论坛网站、商城商品页网站等,我们的数据常常采用服务端渲染的方式来展现,目的是为了让爬虫更好的抓取到,从而在搜索引擎搜索时,可以搜到到我们自己的应用网站。模板引擎服务端渲染是在服务端通过模板引擎和其特定语法完成数据和页面DOM的拼接,然后统一响应给浏览器。市面上存在许许多多五花八门的模板引擎。就以Java为例,在JavaWeb阶段就存在JSP,到了Springboot又推荐Thymeleaf,还有其他的如FreeMarker、velocity等。每种引擎的语法、特性及优缺点这里就不再赘述,SpringBoot官方推荐使用Thymeleaf,且本站也是基于此引擎搭建的,所以本文就以Thymeleaf模板引擎为案例来演示。thymeleaf案例先实现页面跳转效果:公共组件抽取每个页面都会引入一些公共的css或js,还有一些公共的页面组件,比如上面案例中头部导航栏和底部栏,我们可以抽取出来,放在公共组件中。然后直接在页面中引入即可。在resources/templates/common目录中,创建fragment.html文件,可以用来存放公共组件。fragment.html:<!DOCTYPEhtml><htmllang="en"xmlns:th="http://www.thymeleaf.org"><!--公共head--><headth:fragment="head(title,links,scripts)"><metacharset="UTF-8"><titleth:replace="${title}"></title><th:blockth:replace="${links}"/><scriptth:replace="${scripts}"></script><style>ul{overflow:hidden}li{list-style:none;margin-right:20px;float:left}</style></head><body><!--公共header--><headerth:fragment="header"><ul><li><ahref="/view"data-pjax>首页</a></li><li><ahref="/view/a"data-pjax>a页面</a></li><li><ahref="/view/b"data-pjax>b页面</a></li></ul></header><!--公共footer--><footerth:fragment="footer"><p>这是footer部分</p></footer></body></html>页面创建在resources/templates/view目录下,创建3个页面:index.html(首页)、a.html和b.html。index.html:<!DOCTYPEhtml><htmllang="en"xmlns:th="http://www.thymeleaf.org"><headth:replace="common/fragment::head(~{::title},~{},~{})"><title>index页面</title></head><body><headerth:replace="common/fragment::header"></header><divclass="content">index页面内容</div><footerth:replace="common/fragment::footer"></footer></body></html>其他页面同理,只需改下title标签和<divclass="content">中的内容即可。路由控制我们把负责控制页面路由的controller单独抽取出来,为了方便管理,给加一个统一的前缀/view,这样可以在后期加监控或拦截器时,只需要针对此前缀的路由即可。@Controller@RequestMapping("/view")publicclassPageController{@GetMapping(value={"","/{name}"})publicModelAndViewpage(@PathVariable(value="name",required=false)Stringname){if(StringUtils.isEmpty(name)){name="index";}returnnewModelAndView("/view/"+name);}}因为此处的@RequestMapping中统一加了前缀/view,所以在fragment.html中a标签的链接会加上这个前缀,否则路由不到指定的页面。假设项目运行在9999端口,此时启动项目,访问localhost:9999/view则会自动跳转到index.html。且点击a标签时,与上面案例效果一致。案例不足的地方上面案例虽实现了基本页面跳转,且也提取了公共的组件。但是在页面切换时,浏览器是重新加载的,这样会存在几个弊端:1、公共静态资源重新请求,网页整体响应相对较慢。2、公共数据服务端重复获取,增加后台服务器压力。3、扩展性不足,无法定制化需求(比如加一个音乐播放器的功能,一直刷新体验不好)。针对以上问题,我们希望在页面跳转时,让浏览器不再刷新,且每次请求时,服务器只响应我们想要的数据,即已经加载过的静态资源和公共数据,我们就不让其重新加载了。解决方案?前后分离,前端采用router跳转我们知道现在前端框架vue或者react等都有router路由的概念。虽然路由的两种模式hash和history均可以实现我们想要的效果,但它们属于客户端渲染,即通过js实现数据与DOM的拼接,不利于我们一开始提到的seo,有悖初衷。后端forward转发我们也可以在controller层通过servlet的forward转发来控制页面跳转,但是此种方式不会改变浏览器地址,作为对外网站来说不友好,所以也不推荐此种方式。pjaxpjax是一个jQuery插件,它通过ajax和pushState技术提供了极速的(无刷新ajax加载)浏览体验,并且保持了真实的地址、网页标题,浏览器的后退(前进)按钮也可以正常使用。pjax的工作原理是通过ajax从服务器端获取HTML,在页面中用获取到的HTML替换指定容器元素中的内容。然后使用pushState技术更新浏览器地址栏中的当前地址。以下两点原因决定了pjax会有更快的浏览体验:1、不存在页面资源(js/css)的重复加载和应用;2、如果服务器端配置了pjax,它可以只渲染页面局部内容,从而避免服务器渲染完整布局的额外开销。具体细节使用这里不再赘述,详情可以参考其github:JQuery-Pjax文档pjax流程分析1、在页面中,有些链接我们希望返回的是局部页面,有些则是做其他的处理,所以我们需要把返回局部页面的a标签上,给加一个标识(例如data-pjax),标注此链接是交给pjax来管理。<ahref="xxx"data-pjax>pjax链接</a><ahref="xxx">普通链接</a>2、我们希望在每个页面中,存在一个容器.content,来存放本页面的主要内容。此容器也是pjax请求过来时,我们响应给它的局部html内容。2、我们希望在每个页面中,提供一个容器#pjax-container,用于展示pjax请求回来的其他局部页面。在没有pjax请求前,此容器中展示自己页面中的.content内容。关键代码实现pjax管理a标签<scriptsrc="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script><scriptsrc="https://cdn.bootcdn.net/ajax/libs/jquery.pjax/2.0.1/jquery.pjax.min.js"></script><li><ahref="/view"data-pjax>首页</a></li><li><ahref="/view/a"data-pjax>a页面</a></li><li><ahref="/view/b"data-pjax>b页面</a></li><divid="pjax-container"><divth:fragment="content">这是index页面的内容</div></div><scripttype="text/javascript">//管理具有data-pjax属性的a标签,把a请求回来的内容,填充到本页面的#pjax-container中$(document).pjax('a[data-pjax]','#pjax-container');</script>controller部分页面@GetMapping("/a")publicModelAndViewa(){//返回a页面中th:fragment为content的内容Stringfragment="/view/a::content";returnnewModelAndView(fragment);}效果我们发现,我们在点击a标签后,页面没有刷新,且只返回了a页面的局部内容,可以达到我们预期的效果。遗留问题在controller层中,对于a页面,只返回了th:fragment="content"的内容。如果此链接是通过pjax访问的,那么正好是我们想要的效果,只返回局部页面。但如果用户一开始就访问的是a页面,会发生什么情况?答案是仍然只返回th:fragment中的内容。因为我们没有对请求做判断,我们需要判断请求是否是来自pjax请求,如果是,我们只返回部分页面。如果不是,我们需要返回整个a页面,包含导航栏和底部栏。拦截器判断请求是否是pjax请求针对上面遗留的问题,我们只需要判断请求是浏览器第一次加载的,还是通过点击站内链接发起的pjax。我们发现,pjax在发送请求时,会多携带两个请求头信息:所以我们只需要在拦截器中,判断请求头是否包含此请求头就好了。如果包含,就只返回部分页面,否则就返回整个页面:@OverridepublicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{Booleanpjax=Boolean.parseBoolean(request.getHeader("X-PJAX"));if(pjax){modelAndView.setViewName(modelAndView.getViewName()+"::content");}}加上拦截器判断之后,此时我们的跳转就一切正常了。如图:项目代码目录结构fragment.html<!DOCTYPEhtml><htmllang="en"xmlns:th="http://www.thymeleaf.org"><!--公共head--><headth:fragment="head(title,links,scripts)"><metacharset="UTF-8"><titleth:replace="${title}"></title><th:blockth:replace="${links}"/><scriptth:replace="${scripts}"></script><scriptsrc="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script><scriptsrc="https://cdn.bootcdn.net/ajax/libs/jquery.pjax/2.0.1/jquery.pjax.min.js"></script><style>ul{overflow:hidden}li{list-style:none;margin-right:20px;float:left}</style><scripttype="text/javascript">//管理具有data-pjax属性的a标签,把a请求回来的内容,填充到本页面的#pjax-container中$(document).pjax('a[data-pjax]','#pjax-container');</script></head><body><!--公共header--><headerth:fragment="header"><ul><li><ahref="/view"data-pjax>首页</a></li><li><ahref="/view/a"data-pjax>a页面</a></li><li><ahref="/view/b"data-pjax>b页面</a></li></ul></header><!--公共footer--><footerth:fragment="footer"><p>这是footer部分</p></footer></body></html>index.html(a/b.html一样)<!DOCTYPEhtml><htmllang="en"xmlns:th="http://www.thymeleaf.org"><headth:replace="common/fragment::head(~{::title},~{},~{})"><title>index页面</title></head><body><headerth:replace="common/fragment::header"></header><divid="pjax-container"><divth:fragment="content">index页面内容</div></div><footerth:replace="common/fragment::footer"></footer></body></html>PageController@Controller@RequestMapping("/view")publicclassPageController{@GetMapping(value={"","/{name}"})publicModelAndViewpage(@PathVariable(value="name",required=false)Stringname){if(StringUtils.isEmpty(name)){name="index";}returnnewModelAndView("/view/"+name);}}InterceptorConfig@ConfigurationpublicclassInterceptorConfigextendsWebMvcConfigurationSupport{@AutowiredprivatePjaxInterceptorpjaxInterceptor;privatestaticfinalString[]CLASSPATH_RESOURCE_LOCATIONS={"classpath:/META-INF/resources/","classpath:/resources/","classpath:/static/","classpath:/public/"};@OverrideprotectedvoidaddInterceptors(InterceptorRegistryregistry){String[]interceptPathList=newString[]{"/view/**"};registry.addInterceptor(pjaxInterceptor).addPathPatterns(interceptPathList);}@OverrideprotectedvoidaddResourceHandlers(ResourceHandlerRegistryregistry){registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);}}PjaxInterceptor@ComponentpublicclassPjaxInterceptorimplementsHandlerInterceptor{@OverridepublicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{Booleanpjax=Boolean.parseBoolean(request.getHeader("X-PJAX"));if(pjax){modelAndView.setViewName(modelAndView.getViewName()+"::content");}}}

    Spring系列 / Java
    105
    5
    2022-10-06 18:57:41
  • CompletableFuture 引发的线上问题

    有次在打开此网站的时候,发现请求一直在转圈圈,没有页面响应。第一反应是服务器被攻击了?服务器状态但随即就否定了这个可能,正经大佬谁会闲着没事找这个小站的麻烦。如果是学网络的同学拿着本站练手还有可能,但是本站知道的人寥寥无几,平时更是无人踏足,也不太可能。于是纠结中,还是打开了服务器的后台,发现各项指标一切正常,也就不存在被黑的情况。Nginx状态网站域名是通过nginx转发到后台服务的,随后想到了是不是nginx的问题,于是查看了下nginx的状态,发现nginx也是正常的,且网站的静态资源可以正常访问。看了下log,果然发现存在error日志,打开看下,异常信息为:upstreamtimedout(110:Connectiontimedout)whilereadingresponseheaderfromupstream很明显,请求上游服务器超时,那上游服务器可不就是后台应用嘛,大致定位到问题源头了,在程序端。应用程序排查查看后台日志,发现接口可以正常进来,但是程序走到某个地方停住了,且线程已经在这儿堆积了几十个在等待了。首先觉得不可能是并发导致的,因为并发再高,只要程序是正常的,即使没有做熔断降级处理,也不至于一直阻塞在同一个地方。且每次请求线程都是依次递增阻塞,说明之前的线程压根就没有释放。很明显是程序在某个地方被锁住了。锁排查分析到锁了,首先想到的是不是与其他服务的连接被阻塞了,导致一直获取不到连接,且没有设置超时时间,所以程序就一直锁在某个地方。心里觉得不太可能,首先不存在并发,且日志里没有连接超时相关的打印。但处于不确定因素,还是对程序里连接的其他服务挨个排查了下。es排查网站首页的列表数据是查的es,会不会是es阻塞了,但是日志里很明显有es响应的数据返回(201行有日志输出),所以不是es的问题。mysql排查项目里并没有复杂的业务,且读多写少,不会存在数据库层死锁的情况。出于严谨,还是查看了下数据库的连接数,发现也没有问题。redis排查用第三方工具连接了redis,发现也可以正常查询,说明也不是因为redis阻塞的Jstack排查百思不得其解,看了半天代码,实在没有发现哪里存在问题,于是打算用jstack分析下堆栈信息。1、获取java进程的pid[xxx@VM-16-3-centos/]#ps-ef|grepjavaxxx393225457021:50pts/000:00:00grep--color=autojavaxxx204721120:43?00:01:11java-jarxxx.jar获取到程序pid为204722、查看进程里占用资源最多的线程虽然发生了死锁,但是查看cpu的资源占用的并不多,找到进程中占用资源最多的线程的pid20586。3、将pid转16进制[xxx@VM-16-3-centos/]#printf"%x\n"20586506a得到506a4、jstack查询根据堆栈信息,发现CompletableFuture中的线程都处在WAITING(parking)中,定位到图中异常处代码的地方,发现在CompletableFuture.allOf().get()处发生了阻塞。CompletableFuture.allOf(categoryFuture,hotArticleFuture,websiteInfoFuture).get();排查到此,大致明白了阻塞的原因。阻塞原因在接口中,除了查询文章列表信息外,还会将网站的公共数据也查询出来,由于这些数据之间是不存在前后查询顺序的,所以为了提高接口的响应速度。我在代码里通过CompletableFuture做了几个异步查询。//查分类信息CompletableFuture<Void>categoryFuture=CompletableFuture.runAsync(()->{List<CategoryVo>categoryList=categoryService.selectCategoryList();commonData.setCategoryList(categoryList);},threadPoolExecutor);//查热门文章CompletableFuture<Void>hotArticleFuture=CompletableFuture.runAsync(()->{List<CommonData.HotArticle>hotArticleList=selectHotArticle();commonData.setHotArticleList(hotArticleList);},threadPoolExecutor);//查网站信息CompletableFuture<Void>websiteInfoFuture=CompletableFuture.runAsync(()->{CommonData.WebsiteInfowebsiteInfo=selectWebsiteInfo();commonData.setWebsiteInfo(websiteInfo);},threadPoolExecutor);这些异步任务的线程是来自同一个线程池threadPoolExecutor。但代码仅仅是这样,还不至于产生阻塞的情况,真正阻塞的是,我在每一个异步任务的service方法中,又细分了多个子异步任务,且它们都公用的一个线程池。比如在查询网站信息selectWebsiteInfo()方法中,里面又细分为查询文章数目、评论数目、运行天数等任务,还有一些其他的日志记录等代码都是通过异步去实现的。而以上这个情况,如果线程池设置的核心线程数不多,那么就很容易造成一种现象:父任务在等待子任务结束,而子任务又在等待父任务释放资源。所以就造成了死锁状态。验证为了验证上述结论是否正确,通过一个demo复现一下场景。publicclassThreadPoolTest{publicstaticThreadPoolExecutorthreadPoolExecutor=newThreadPoolExecutor(3,50,5,TimeUnit.SECONDS,newLinkedBlockingDeque<>(10),Executors.defaultThreadFactory(),newThreadPoolExecutor.AbortPolicy());publicstaticvoidmain(String[]args)throwsException{List<CompletableFuture<Void>>list=newArrayList<>(0);for(inti=0;i<3;i++){//2、线程池中就3个空闲线程,因为做了sleep,所以3个资源都给了父任务CompletableFuture<Void>parentTask=CompletableFuture.runAsync(()->{try{System.out.println("父任务执行了:"+Thread.currentThread().getName());Thread.sleep(10);}catch(Exceptione){e.printStackTrace();}//3、子任务在等待父任务释放资源,父任务在等待子任务执行完,死锁CompletableFuture<Void>childTask=CompletableFuture.runAsync(()->{System.out.println("子任务执行了:"+Thread.currentThread().getName());},threadPoolExecutor);childTask.join();},threadPoolExecutor);list.add(parentTask);}//1、开始创建3个异步任务并执行CompletableFuture.allOf(list.toArray(newCompletableFuture[0])).get();threadPoolExecutor.shutdown();System.out.println("exit");}}输出结果是阻塞的,接下来再根据jstack分析下原因:根据jps,找到类的pid:根据jstack15769,找到线程阻塞原因:根据日志,发现线程代码阻塞在第33行,而33行正式子任务获取线程执行的地方。由上可知,问题产生的原因确实是因为线程池获取连接导致死锁阻塞了。解决方案1、提高线程池的核心线程数,但是此方法治标不治本。2、嵌套线程之间最好不用同一个线程池,做线程池隔离,避免死锁问题。总结1、不建议直接使用CompletableFuture的get()方法,而是使用future.get(5,TimeUnit.SECONDS);方法指定超时时间。2、在使用CompletableFuture的时候线程池拒绝策略最好使用AbortPolicy,如果线程池满了直接抛出异常中断主线程,达到快速失败的效果。3、耗时的异步线程和CompletableFuture的线程做线程池隔离,让耗时操作不影响主线程的执行。

    BUG集 / Java
    87
    1
    2022-10-01 01:01:15
  • 数据结构之稀疏数组

    经典五子棋如上图所示,有一块11*11的棋盘,如果我们想通过编码的方式把棋盘数据存储起来,第一感觉是通过二维数组的方式进行存储。普通二维数组存储创建11*11的二位数组用来存储每个棋子的坐标。黑棋用1表示,蓝棋用2表示,没有棋子的地方默认填充0,那么在编码中,如下:/***创建普通二维数组存储*@return*/publicint[][]generalArray(){//1、创建11*11的二维数组int[][]chessArr=newint[11][11];//2、黑棋chessArr[1][2]=1;//3、蓝棋chessArr[2][3]=2;returnchessArr;}打印如下:二维数组的不足我们发现,用二维数组虽然可以实现功能,但是数组中保存了大量为0的非核心数据,我们在进行磁盘存储时,无疑会占用存储空间,增加IO的读写次数,影响系统性能。稀疏数组实际上,针对上面的案例,我们只需要把棋盘大小和棋子位置记录下来就好,没有必要记录非核心数据,由此引出了稀疏数组。第一行存储整个二维数组的规模大小和记录数。因为棋盘是11*11的,所以行和列都是11,因为共有两个棋子,所以值就是2。剩余的行用来存储每个棋子在二维数组中的下标和值在二维数组[1,2]的位置有颗白棋,所以行列分别为1和2,值为1。在二维数组[2,3]的位置有颗蓝棋,所以行列分别为2和3,值为2。通过此稀疏数组,剔除了原二维数组中无效的数据,有效节省了内存,在写盘时也能明显降低IO次数。二维数组转稀疏数组稀疏数组在初始化时,因为需要多一行记录存储棋盘的规模,所以它的行数=棋子个数+1/***普通二维数组转稀疏数组*@paramchessArr*@return*/publicint[][]getSparseArr(int[][]chessArr){//记录棋子个数intcount=0;for(inti=0;i<chessArr.length;i++){for(intj=0;j<chessArr[i].length;j++){if(chessArr[i][j]!=0){count++;}}}//1、稀疏数组需要多一行记录棋盘的规模,所以行数需要比count多一行int[][]sparseArr=newint[count+1][3];//2、存储棋盘规模数据sparseArr[0][0]=chessArr.length;sparseArr[0][1]=chessArr[0].length;sparseArr[0][2]=count;//稀疏数组第0行存储棋盘规模,所以棋子数据是从第1行开始存储//每存储一个棋子,line就+1,intline=1;for(inti=0;i<chessArr.length;i++){for(intj=0;j<chessArr[i].length;j++){if(chessArr[i][j]!=0){sparseArr[line][0]=i;sparseArr[line][1]=j;sparseArr[line][2]=chessArr[i][j];line++;}}}returnsparseArr;}打印下稀疏数组数据:稀疏数组转二维数组/***稀疏数组转二维数组*@paramsparseArr*@return*/publicint[][]getChessArr(int[][]sparseArr){//从稀疏数组的第一行数据中,获取棋盘初始化值int[][]chessArr=newint[sparseArr[0][0]][sparseArr[0][1]];//从下标为1的数据开始遍历for(inti=1;i<sparseArr.length;i++){int[]row=sparseArr[i];chessArr[row[0]][row[1]]=row[2];}returnchessArr;}总结当一个数组中大部分元素为0,或者大部分元素数值都相同时,可以使用稀疏数组来优化普通数组的存储结构。稀疏数组第一行存储普通数组的规模,后面的行才存储真正的数据。稀疏数组可以在一定程度上,节省内存空间,提高一定性能。源码packagecn.lzc.sparsearray;publicclassSparseArrayDemo{publicstaticvoidmain(String[]args){SparseArrayDemosparseArrayDemo=newSparseArrayDemo();//普通二维数组存储int[][]chessArr=sparseArrayDemo.generalArray();//二维数组转稀疏数组int[][]sparseArr=sparseArrayDemo.getSparseArr(chessArr);System.out.println("稀疏数组:");sparseArrayDemo.printArr(sparseArr);//稀疏数组转二位数组chessArr=sparseArrayDemo.getChessArr(sparseArr);System.out.println("二维数组:");sparseArrayDemo.printArr(chessArr);}/***创建普通二维数组存储*@return*/publicint[][]generalArray(){//1、创建11*11的二维数组int[][]chessArr=newint[11][11];//2、黑棋chessArr[1][2]=1;//3、蓝棋chessArr[2][3]=2;returnchessArr;}/***普通二维数组转稀疏数组*@paramchessArr*@return*/publicint[][]getSparseArr(int[][]chessArr){//记录棋子个数intcount=0;for(inti=0;i<chessArr.length;i++){for(intj=0;j<chessArr[i].length;j++){if(chessArr[i][j]!=0){count++;}}}//1、稀疏数组需要多一行记录棋盘的规模,所以行数需要比count多一行int[][]sparseArr=newint[count+1][3];//2、存储棋盘规模数据sparseArr[0][0]=chessArr.length;sparseArr[0][1]=chessArr[0].length;sparseArr[0][2]=count;//稀疏数组第0行存储棋盘规模,所以棋子数据是从第1行开始存储//每存储一个棋子,line就+1,intline=1;for(inti=0;i<chessArr.length;i++){for(intj=0;j<chessArr[i].length;j++){if(chessArr[i][j]!=0){sparseArr[line][0]=i;sparseArr[line][1]=j;sparseArr[line][2]=chessArr[i][j];line++;}}}returnsparseArr;}/***稀疏数组转二维数组*@paramsparseArr*@return*/publicint[][]getChessArr(int[][]sparseArr){//从稀疏数组的第一行数据中,获取棋盘初始化值int[][]chessArr=newint[sparseArr[0][0]][sparseArr[0][1]];//从下标为1的数据开始遍历for(inti=1;i<sparseArr.length;i++){int[]row=sparseArr[i];chessArr[row[0]][row[1]]=row[2];}returnchessArr;}/***打印数组*@paramarr*/publicvoidprintArr(int[][]arr){for(int[]row:arr){for(intdata:row){System.out.printf("%d\t",data);}System.out.println();}}}

    数据结构与算法
    63
    2
    2022-09-28 15:27:00
热门文章 最新评论
  • InnoDB索引数据结构

    8
  • thymeleaf 整合 pjax 无刷新跳转

    5
  • wmware创建虚拟机

    4
  • SQL响应慢如何分析解决

    4
  • JS 生成文章目录树

    3
网站信息 访客统计
  • 文章数目
    28
  • 评论数目
    61
  • 运行天数
    620天