暂时未有相关云产品技术能力~
01、为什么austin项目需要限流众所周知,服务器能处理的请求数是有限的,如果请求量特别大,我们就可能需要做限流。限流处理的姿势:要么就让请求等待,要么就把请求给扔了从系统架构来看,我们的统一处理入口在austin-api接入层上,austin-api接入层做完简单的参数校验以及参数拼接后,就将请求转发到消息队列上了按正常来说,因为接了消息队列且接入层没有什么耗时的操作,那对外的接口压力不大的。没错的,austin要接入限流也并不是在austin-api接入层上做,是在austin-handler消息处理下发层。austin-handler消息处理下发层我们是用线程池去隔离不同的消息渠道不同的消息类型。在系统本身上其实没有性能相关的问题,但我们下发的渠道可能就需要我们去控制调用的速率。腾讯云短信默认限制3000次/秒调用下发接口钉钉渠道对应用消息和群机器人消息都有接口调用的限制....在保证下发速度的前提下,为了让业务方所下发的消息其用户能正常接收到和下游渠道的稳定性,我们需要给某些渠道进行限流于是在这个背景下,我目前定义了两种限流策略:1、按照请求数限流2、按照下发用户数限流02、如何实现限流?想要实现限流,摆在我们面前有两个选择:1、单机限流2、分布式限流咋一看,线上不可能只部署一台机器去发送整个公司的消息推送的,我们的系统应用在线上环境绝对是集群部署的,那肯定就需要上分布式限流了,对吧?但实际上分布式限流实现并不简单,目前分布式限流的方案一般借助两个中间件1、Redis2、Sentinel我们可能会用Redis的setnx/incrby+expire命令(从而实现计数器、令牌桶限流)/zset数据结构(从而实现滑动窗口限流)Redis实现的限流想要比较准确,无论是哪种方式,都要依靠lua脚本而Sentinel支持单机限流和分布式限流,Sentinel分布式限流需要部署Token服务器对于分布式限流而言,不管用哪一种方案,使用的成本和技术挑战都是比较大的。如果用单机限流的话,那就简单得多了,省事直接用Guava包下的RateLimiter就完了。缺点就在于:它只能控制单机的限流,如果发生了服务器扩容和缩容,它是感知不到的。有的人就给出了方案:那我用Zookeeper监听服务器的数量不就好了吗。理论上确实是这样的:每台机器限流值=限流总值/服务器数量不过这又要去依赖Zookeeper,Zookeeper集群本身也有一堆状态相关的问题。我是怎么实现的?单机限流一把梭03、代码设计从上面的描述可以看到,austin的限流我是要做在具体渠道上的,根据现有的代码设计我要的就是在各个的Handler上写上限流的代码。我本身就设计了BaseHandler抽象类作为模板方法设计模式,此次限流的设计我的是:1、将flowControl定义为abstract抽象方法,子类渠道去实现限流的代码2、子类在初始化的时候定义限流参数,BaseHandler父类根据限流参数统一实现限流的逻辑我选择了第二种方式,主要是我认为对于各个渠道而言,只是限流值是不同的,限流的逻辑应该都是一样的,没必要在每个子类上实现类似的逻辑。而限流的逻辑就比较简单了,主要就使用RateLimit提供的易用API实现没错,限流值的大小我是配置在apollo分布式配置中心的。假设以后真的要扩缩容了,那到时候提前把分布式配置中心的值给改掉,也能解决一部分的问题。04、总结扯了半天,原来就用了Guava包的RateLimit实现了单机限流,就这么简单,只是把限流值配置在分布式配置中心上而已。很多时候,设计简单的代码可能实现并不完美,并不智能,并不优雅,但它付出的代价往往是最小的。虽说如此,如果大家想要了解Redis+lua实现的同学可以fetch下austin最新的代码,就我写文章这段时间里,已经有老哥提了pull request用Redis+lua实现了滑动窗口去重的功能了,本质上是一样的。
文章的内容主要由以下部分组成:应用发布重启了怎么办?内存数据不是丢失了吗?什么是优雅停机?如何实现优雅停机?如何优雅地调整线程池的参数?如果你的项目遇到了类似的问题,也可以借鉴下我今天所讲解的内容,读完我相信你肯定会有些收获。01、应用发布重启了怎么办众所周知,如果我们系统在运行的过程中,内存数据没存储起来那就会导致丢失。对于austin项目而言,就会使消息丢失,并且无法下发到用户上。这个在我讲述完我是如何设计「发送消息消费端」以及「读取文件」时,尤其问得比较多。为了部分没有追更的读者,我再简单讲述下我这边的设计:在austin-handler模块,每个渠道的每种消息类型我都用到了线程池进行隔离而消费:在austin-cron模块,我读取文件是把每一条记录放至了单线程池做LazyPending,目的为了延迟消费做批量下发。敏感的技术人看到内存队列或线程池(线程池也需要指定对应的内存队列)就很正常地想:内存队列可能的size为1024,而服务器在重启的时候可能内存队列的数据还没消费完,此时你怎么办?数据就丢了吗?我们使用线程池/内存队列在很多场景下都是为了提高吞吐量,有得就必有失。至于重启服务器导致内存数据的丢失,就看你评估对自己的业务带来多少的影响了。针对这种问题,austin本身就开发好了相关的功能作为「补充」,通过实时计算引擎flink的能力可以实时在后台查看消息下发的情况:可以在离线hive找到消息下发失败的userId(离线这块暂未实现),输入具体的receiverId 可以查看实时下发时失败的原因查明原因之后再通过csv文件上传的做补发。不过,这是平台提供做补发的能力,从技术上的角度,还有别的思路尽量避免线程池或者内存队列的数据因重启而丢失的数据吗?有的,优雅关闭线程池02、优雅停机所谓「优雅停机」就是关闭的时候先将自己需要处理的内容处理完了,之后才关闭。如果你直接kill -9,是没有「优雅」这一说法的,神仙都救不了。1、在网络层:TCP有四次挥手、TCP KeepAlive、HTTP KeepAlive 让连接 优雅地关闭,避免很多报错。2、在Java里边通过Runtime.getRuntime().addShutdownHook()注册事件,当虚拟机关闭的前调用该方法的具体逻辑进行善后。3、在Spring里边执行了ApplicationContext 的close之后,只要我们Bean配置了destroy策略,那Spring在关闭之前也会先执行我们的已实现好的destroy方法4、在Tomcat容器提供了几种关闭的姿势,先暂停请求,选择等待N秒才完全关闭容器。5、在Java线程池提供了shutdown和shutdownNow供我们关闭线程,显然shutdown是优雅关闭线程池的方法。我们的austin项目是基于SpringBoot环境构造的,所以我们可以重度依赖SpringBoot进行优雅停机。1、我们设置应用服务器的停机模式为gracefulserver.shutdown=graceful2、在austin已经引入动态线程池而非使用Spring管理下的ThreadPoolTaskExecutor,所以我们可以把自己创建出来的线程池在Spring关闭的时候,进行优雅shutdown(想要关闭其他的资源时,也可以类似干这种操作)注:如果是使用Spring封装过的线程池ThreadPoolTaskExecutor,默认就会优雅关闭,因为它是实现了DisposableBean接口的03、如何优雅地调整线程池的参数?austin在整个项目里边,还是有挺多地方是用到了线程池,特别重要的是从MQ里消费所创建的线程池。有小伙伴当时给过建议:有没有打算引入动态线程池,不用发布就调整线程池的参数从而临时提高消费能力。顺便在这给大家推荐美团的线程池文章:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html,如果没读过这篇文章的,建议都去读下,挺不错的。美团这篇文章讲述了动态线程池的思路,但应该是未官方开源,所以有很多小伙伴基于文章的思路造了好用的轮子。比如 Hippo4J 和dynamic-tp 都是比较优秀的轮子了。这两个仓库我都看了下源码, Hippo4J 有无依赖中间件实现动态线程池,也有默认实现Nacos和Apollo的版本,并有着管理后台,而dynamic-tp 默认实现依赖Nacos或Apollo。大佬们的代码都写得很不错,我推荐大家都可以去学学。我在最初的时候接的是dynamic-tp的代码,因为我本身austin就接入了Apollo,也感觉暂时不太需要管理后台。后来 Hippo4J 作者找我聊了下,希望我能接入Hippo4J。我按照我目前的使用场景对着代码看了一把,我是需要通过在创建线程池后再动态调参的场景。于是跟 Hippo4J 作者反馈了下,他果断说晚上或明天就给我实现(:恐怖如斯,太肝了不过,周三我反馈完,周四晚上我差不多就将 dynamic-tp 快接入完了。我目前现在打算先跑着(毕竟切换API其实也是需要时间成本的),后续看有没有遇到痛点或者空的时候再迁移到Hippo4J再体验体验也不为别的,就看中龙台大佬比我还肝(自己提出的场景,开源作者能很快地反馈并实现,太强了,丝毫不担心有大坑要我自己搞)04、总结对于austin而言,正常的重启发布我们通过优雅停机来尽可能减少系统的处理数据时的丢失。如果消息是真的非常重要而且需要做补发,在austin中也可以通过上传文件的方式再做补发,且能看到实时推送的数据链路统计和某个用户下发消息失败的原因。我相信,这已经能覆盖线上绝大多数的场景了。或许后续也可以针对某些场景在消费端做exactly once + 幂等 来解决kill -9的窘境,但要知道的是:想要保证数据不丢失、不重复发送给用户,一定会带来性能的损耗,这是需要做平衡的。在项目很少使用线程池之前,一直可能认为线程池的相关面试题就是八股文。但当你项目系统真的遇到线程池优雅关闭的问题、线程池参数动态调整的问题,你就会发现之前看的内容其实是很有意义的。阿,原来可以设置参数让核心线程数也会回收的(之前一直都没有注意过呢)阿,原来都大多数框架都有提供对应的扩展接口给我们监听关闭,默认的实现都有优雅停机的机制咯,之前一直都不知道呢。....austin还在持续优化和更新中,欢迎大佬们给点意见和想法一起讨论,对该项目感兴趣的同学也可以到我的GitHub上逛逛,或许有可能这个季度的KPI就有了咯。
01、使用定时任务推送消息对于austin消息推送平台而言,发送消息不单单是「技术侧」调用接口进行发送的,还有很多场景是「运营侧」通过设置定时进而推送。所以,作为使用消息推送平台的角色可以简单分为:「技术同学」、「运营同学」当运营或客服她们想发送消息给用户,她们使用消息推送的后台,步骤可简单分为:1、确定发送时间2、确定发送的人群(人群内可以是1~N的用户)3、确定发送文案对于发送时间则使用cron表达式,发送人群我们是让运营上传.csv文件,发送文案则配置在模板上(同样也可以使用占位符)这里值得讲述的是,为什么是上传.csv文件而不是excel,其最根本的原因:excel的行数是有大小限制的,而.csv的行数是没有大小限制的。我们限定.csv文件的格式为如下:第一列填写接收者Id、剩余的列如果使用了占位符,则需要填写占位符变量名保存了消息模板之后,等我们点击「启动」按钮,就根据模板的信息进行消息推送。(在线上环境上,在启动之前肯定会有审核的环节,并且一般会先点击「测试」按钮看文案是否正常才进行推送)点击启动按钮了之后,会发生什么?我们看日志就懂了(我在执行的过程中把关键的日志信息都打印出来了)//1. 消息模板ID的消息 定时任务被触发(入口) 2022-02-17 21:14:55.016 [Thread-61] INFO com.java3y.austin.cron.handler.CronTaskHandler - CronTaskHandler#execute messageTemplateId:7 cron exec! //2. api-service接口接收到的参数信息以及接口返回值 2022-02-17 21:14:56.095 [pool-27-thread-1] INFO com.java3y.austin.support.utils.LogUtils - {"bizId":"7","bizType":"SendService#batchSend","executionTime":72,"logId":"9fd2cdd1-79fa-4c5c-932b-da08aa3b247d","msg":"{\"code\":\"send\",\"messageParamList\":[{\"extra\":null,\"receiver\":\"13719383334,13719383336,13719383338,13719383340,13719383342,13719383344\",\"variables\":{\"content\":\"xixi\",\"url\":\"hehe.com\"}},{\"extra\":null,\"receiver\":\"13719383333,13719383335,13719383337,13719383339,13719383341,13719383343,13719383345\",\"variables\":{\"content\":\"hhaha\",\"url\":\"baidu.com\"}}],\"messageTemplateId\":7}","operateDate":1645103696095,"returnStr":"{\"code\":\"0\",\"msg\":\"操作成功\"}","success":true,"tag":"operation"} //3. receiver消息队列接收到的原始值 2022-02-17 21:14:56.252 [org.springframework.kafka.KafkaListenerEndpointContainer#6-0-C-1] INFO com.java3y.austin.support.utils.LogUtils - {"bizType":"Receiver#consumer","object":{"businessId":1000000720220217,"contentModel":{"content":"xixi","url":"hehe.com?track_code_bid=1000000720220217"},"idType":30,"messageTemplateId":7,"msgType":10,"receiver":["13719383340","13719383336","13719383338","13719383342","13719383344","13719383334"],"sendAccount":10,"sendChannel":30,"templateType":10},"timestamp":1645103696252} 2022-02-17 21:14:56.254 [org.springframework.kafka.KafkaListenerEndpointContainer#6-0-C-1] INFO com.java3y.austin.support.utils.LogUtils - {"bizType":"Receiver#consumer","object":{"businessId":1000000720220217,"contentModel":{"content":"hhaha","url":"baidu.com?track_code_bid=1000000720220217"},"idType":30,"messageTemplateId":7,"msgType":10,"receiver":["13719383341","13719383339","13719383335","13719383337","13719383343","13719383333","13719383345"],"sendAccount":10,"sendChannel":30,"templateType":10},"timestamp":1645103696253} //4.1 关键位置打印的日志(state=10)代表消息队列接收成功 2022-02-17 21:14:56.172 [org.springframework.kafka.KafkaListenerEndpointContainer#6-0-C-1] INFO com.java3y.austin.support.utils.LogUtils - {"businessId":1000000720220217,"ids":["13719383340","13719383336","13719383338","13719383342","13719383344","13719383334"],"state":10,"timestamp":1645103696172} 2022-02-17 21:14:56.253 [org.springframework.kafka.KafkaListenerEndpointContainer#6-0-C-1] INFO com.java3y.austin.support.utils.LogUtils - {"businessId":1000000720220217,"ids":["13719383341","13719383339","13719383335","13719383337","13719383343","13719383333","13719383345"],"state":10,"timestamp":1645103696253} //4.2 关键位置打印的日志(state=60)代表消息调用接口发送失败 2022-02-17 21:14:56.799 [pool-8-thread-3] INFO com.java3y.austin.support.utils.LogUtils - {"businessId":1000000720220217,"ids":["13719383340","13719383336","13719383338","13719383342","13719383344","13719383334"],"state":60,"timestamp":1645103696799}因为这次发送的渠道是短信,我们是有将短信的发送记录入库的,所以可以看看数据库的记录(刚好是13条,数据是没问题的。至于发失败,主要是该短信模板的参数不符合)我们整体的流程是没有问题的02、代码结构设计从消息推送后台层面上,当点击了「启动」按钮时,其实是「创建&&启动」或者「启动」了个定时任务(调用xxl-job的api,将定时任务存入到xxl-job的数据库表中)等我们定时任务到时间点了,xxl-job的调度中心就找到我们的执行器,进行调用。在创建定时任务的时候,我把消息模板ID写入到了xxl-job任务信息里,所以当任务被调度的时候,我又从xxl-job把参数信息取出来。在这,打印出了一条日志,表示当前模板ID被调度执行了。随后,我使用「线程池」对定时任务做处理。因为我这里认为「读取文件以及远程调用发送接口」是一件比较耗时的工作,所以我这里直接就用线程池做了层异步,及时返回xxl-job,避免定时任务超时。解释完为什么用了线程池以后,接着来看看读取.csv文件这块。在最最最开始的时候,我是直接一次性读取,然后得到List列表的。显然,这是不合理的。假设运营圈选的人群可能达到2000W人,那我直接将2000W条记录直接load到内存,那是不对的。所以,每当从文件读取一行,我就处理一行我在拿到每一行数据的时候,封装了一个VO,又扔给了内存队列LazyPending内存队列里会起一个线程消费队列里的数据,等到积压到给定的size或者timeout就会给到实际消费者进行处理我这样设计的目的在于:我要调用批量发送接口,使用内存队列作为介质实现生产者和消费者模式为了做batch处理。具体来说:如果我每读取一行就调用一次发送接口,假设人群有2000W,我就需要调用2000W次。在实际生产环境中,austin-cron和austin-api在很大概率上是分开部署的,所以每一次调用接口都是远程调用。为了减少这个消耗,所以我这样干了。另外,在具体执行消费的时候,我是设计了「线程池」进行接口调用的,更能充分利用系统资源(毕竟这次接口调用更多的是损耗网络的开销)看到了这,再回到我们的接口打印信息:2022-02-17 21:14:56.095 [pool-27-thread-1] INFO com.java3y.austin.support.utils.LogUtils - {"bizId":"7","bizType":"SendService#batchSend","executionTime":72,"logId":"9fd2cdd1-79fa-4c5c-932b-da08aa3b247d","msg":"{\"code\":\"send\",\"messageParamList\":[{\"extra\":null,\"receiver\":\"13719383334,13719383336,13719383338,13719383340,13719383342,13719383344\",\"variables\":{\"content\":\"xixi\",\"url\":\"hehe.com\"}},{\"extra\":null,\"receiver\":\"13719383333,13719383335,13719383337,13719383339,13719383341,13719383343,13719383345\",\"variables\":{\"content\":\"hhaha\",\"url\":\"baidu.com\"}}],\"messageTemplateId\":7}","operateDate":1645103696095,"returnStr":"{\"code\":\"0\",\"msg\":\"操作成功\"}","success":true,"tag":"operation"}人群数量一共是13,但我们仅用了一次接口调用。03、总结到这里,我已经把定时任务的实现核心逻辑应该就讲完了,大家看代码的时候应该就有个谱了,至少应该不会跑到群里说看不懂了。至于实现的细节上,如果有更好的办法或者思路可以在评论区一起讨论,又或是直接提个PR。一般我认为是合理的,我都会审核通过的哟!看完整篇文章,很有可能就会有同学有疑惑:你把数据放在内存队列里,这如果重启或系统挂了怎么办啊,数据不就丢了吗。其实我认为这是一种权衡traff-off。我们在系统里要保证数据不丢失不重复需要做大量的工作,很有可能会影响到系统的性能或者支持并发的大小。如果是处理订单类的系统,那是必须的。但如果是发消息的场景,或者并没有想象中那么重要(当然了,我们也可以实现就是啦)。但更多的是,我们可以额外通过一些手段来判断消息是否下发成功了:大概就是统计当前消息模板的下发人数、系统处理过程中的人数以及消息到达、点击的人数。这是系统核心功能以外的,但又很重要的功能,这几天已经在实现了,这周有望写完。
01、如何简单实现定时功能?我是看视频入门Java的,那时候学Java基础API的时候,看的视频也带有讲定时功能(JDK原生就支持),我记得视频讲师写了Timer来讲解定时任务。当时并不知道定时任务有什么实际作用,所以在初学阶段的我,从来没使用过Timer来实现定时的功能。再后来,我学到并发了。那时候的讲师提到了ScheduledExecutorService这个接口,它比Timer更加强大,一般我们在JDK里可以用它来实现定时的功能强就强在于ScheduledExecutorService内部是线程池,Timer是单线程,它能更合理的利用资源。我学并发的时候,我也并不太关注它(它并不是并发的重点),所以我也没用过ScheduledExecutorService来实现定时的功能。后来吧,要到学习做项目了,那时候视频有个Quartz课程。我记得理解了很久,最后我才反应过来了,原来写了这么多的代码就是用它来实现定时的功能。至于比ScheduledExecutorService和Timer好在哪里呢,最直观的是:它支持cron表达式。为啥我会理解很久呢,因为Quartz的api太复杂了(它也有着自己的专业术语和概念性的东西)。这种跟着做项目的,我是一步一步跟着敲代码的。而Quartz相关的API我是记不住了,但那时候我理解了:原来我们写代码可以靠「组件包」来完成想要的功能,原来这就是cron表达式。等到我大三的时候,我想用自己学过的知识点来写个小项目,也算是梳理一遍自己到底学了什么东西。于是,我想起了Quartz。那时候我也已经学到了Spring/SpringBoot了,所以当我在网上搜Spring与Quartz整合的时候,了解到了SpringTask,再后来发现了@Schedule注解。只需要一个简单的注解,就能实现定时任务的功能,并且支持cron表达式。那那那那,还要个锤子的Quartz啊!02、实习&&工作 定时任务等我工作了之后,我学到了一个新的名词「分布式定时任务框架」。等我踏入职场了以后,我才发现原来定时任务这么好使!列举下我真实工作时使用定时任务的常见姿势:1、动态创建定时任务推送运营类的消息(定时推送消息)2、广告结算定时任务扫表找到对应的可结算记录(定时扫表更新状态)3、每天定时更新数据记录(定时更新数据)还很多人问我有没有用过分布式事务,我往往会回答:没有啊,我们都是扫表一把梭保证数据最终一致性的。当然了,如果是面试的时候被问到,可以吹吹分布式事务。实际上是怎么扫表的呢?就是定时扫的咯。另外,我当时简单看了下公司自研的分布式定时任务框架是怎么做的,我记得是基于Quartz进行扩展的,扩展有failover、分片等等机制。一般来说,使用定时任务就是在应用启动或者提前在Web页面配置好定时任务(定时任务框架都是支持cron表达式的,所以是周期或者定时的任务),这种场景是最最最多的。03、为什么分布式定时任务在前面提到Timer/ScheduledExecutorService/SpringTask(@Schedule)都是单机的,但我们一旦上了生产环境,应用部署往往都是集群模式的。在集群下,我们一般是希望某个定时任务只在某台机器上执行,那这时候,单机实现的定时任务就不太好处理了。Quartz是有集群部署方案的,所以有的人会利用数据库行锁或者使用Redis分布式锁来自己实现定时任务跑在某一台应用机器上;做肯定是能做的,包括有些挺出名的分布式定时任务框架也是这样做的,能解决问题。但我们遇到的问题不单单只有这些,比如我想要支持容错功能(失败重试)、分片功能、手动触发一次任务、有一个比较好的管理定时任务的后台界面、路由负载均衡等等。这些功能,就是作为「分布式定时任务框架」所具备的。既然现在已经有这么多的轮子了,那我们作为使用方/需求方就没必要自己重新实现一套了,用现有的就好了,我们可以学习现有轮子的实现设计思想。04、分布式定时任务基础Quartz是优秀的开源组件,它将定时任务抽象了三个角色:调度器、执行器和任务,以至于市面上的分布式定时任务框架都有类似角色划分。对于我们使用方而言,一般是引入一个client包,然后根据它的规则(可能是使用注解标识,又或是实现某个接口),随后自定义我们自己的定时任务逻辑。看着上面的执行图对应的角色抽象以及一般使用姿势,应该还是比较容易理解这个过程的。我们又可以再稍微思考两个问题:1、 任务信息以及调度的信息是需要存储的,存储在哪?调度器是需要「通知」执行器去执行的,那「通知」是以什么方式去做?2、调度器是怎么找到即将需要执行的任务的呢?针对第一个问题,分布式定时任务框架又可以分成了两个流派:中心化和去中心化所谓的「中心化」指的是:调度器和执行器分离,调度器统一进行调度,通知执行器去执行定时任务所谓的「去中心化」指的是:调度器和执行器耦合,自己调度自己执行对于「中心化」流派来说,存储相关的信息很可能是在数据库(DataBase),而我们引入的client包实际上就是执行器相关的代码。调度器实现了任务调度的逻辑,远程调用执行器触发对应的逻辑。调度器「通知」执行器去执行任务时,可以是通过「RPC」调用,也可以是把任务信息写入消息队列给执行器消费来达到目的。对于「去中心化」流派来说存储相关的信息很可能是在注册中心(Zookeeper),而我们引入的client包实际上就是执行器+调度器相关的代码。依赖注册中心来完成任务的分配,「中心化」流派在调度的时候是需要保证一个任务只被一台机器消费,这就需要在代码里写分布式锁相关逻辑进行保证,而「去中心化」依赖注册中心就免去了这个环节。针对第二个问题,调度器是怎么找到即将需要执行的任务的呢?现在一般较新的分布式定时任务框架都用了「时间轮」。1、如果我们日常要找到准备要执行的任务,可能会把这些任务放在一个List里然后进行判断,那此时查询的时间复杂度为O(n)2、稍微改进下,我们可能把这些任务放在一个最小堆里(对时间进行排序),那此时的增删改时间复杂度为O(logn),而查询是O(1)3、再改进下,我们把这些任务放在一个环形数组里,那这时候的增删改查时间复杂度都是O(1)。但此时的环形数组大小决定着我们能存放任务的大小,超出环形数组的任务就需要用另外的数组结构存放。4、最后再改进下,我们可以有多层环形数组,不同层次的环形数组的精度是不一样的,使用多层环形数组能大大提高我们的精度。05、分布式定时任务框架选型分布式定时任务框架现在可选择的还是挺多的,比较出名的有:XXL-JOB/Elastic-Job/LTS/SchedulerX/Saturn/PowerJob等等等。有条件的公司可能会基于Quartz进行拓展,自研一套符合自己的公司内的分布式定时任务框架。我并不是做这块出身的,对于我而言,我的austin项目技术选型主要会关注两块(其实跟选择apollo作为分布式配置中心的理由是一样的):成熟、稳定、社区是否活跃。这一次我选择了xxl-job作为austin的分布式任务调度框架。xxl-job已经有很多公司都已经接入了(说明他的开箱即用还是很到位的)。不过最新的一个版本在2021-02,近一年没有比较大的更新了。06、为什么austin需要分布式定时任务框架回到austin的系统架构上,austin-admin后台管理页面已经被我造出来了,这个后台管理系统会提供「消息模板」的管理功能。那发送一条消息不单单是「技术侧」调用接口进行发送的,还有很多是「运营侧」通过设置定时进而推送。而这个功能,就需要用到分布式定时任务框架作为中间件支撑我的业务,并且很重要的一点:分布式定时任务框架需要支持动态创建定时任务的功能。当在页面点击「启动」的时候,就需要创建一个定时任务,当在页面点击「暂停」的时候,就需要停止定时任务,当在页面点击「删除」模板的时候,如果曾经有过定时任务,就需要把它给一起删掉。当在页面点击「编辑」并保存的时候,也需要把停止定时任务。嗯,所需要的流程就这些了07、austin接入xxl-job接入xxl-job分布式定时任务框架的步骤还是蛮简单的(看下文档基本就会了),我简单说下吧。接入具体的代码大家可以拉ausitn的下来看看,我会重点讲讲我接入时的感受。官网文档:https://www.xuxueli.com/xxl-job/#%E4%BA%8C%E3%80%81%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A81、自己项目上引入xxl-job-core的maven依赖2、在MySQL中执行/xxl-job/doc/db/tables_xxl_job.sql的SQL脚本3、从Gitee或GitHub下载xxl-job的源码,修改xxl-job-admin调度中心的数据库配置,启动xxl-job-admin项目。4、在自己项目上添加xxl-job相关的配置信息5、使用@XxlJob注解修饰方法编写定时任务的相关逻辑从接入或者已经看过文档的小伙伴应该就很容易发现,xxl-job它是属于「中心化」流派的分布式定时任务框架,调度器和执行器是分离的。在前面我提到了austin需要动态增删改定时任务,而xxl-job是支持的,但我觉得没封装得足够好,只在调度器上给出了http接口。而调用http接口是相对麻烦的,很多相关的JavaBean都没有在core包定义,只能我自己再写一次。所以,我花了挺长的时间和挺多的代码去完成动态增删改定时任务这个工作。调度器和执行器是分开部署的,意味着,调度器和执行器的网络是必须可通的:原本我在本地是没有装任何的环境的,包括MySQL我都是连接云服务器的,但是现在我要调试就必须在网络可通的环境内,所以我不得不在本地启动xxl-job-admin调度中心来调试。在启动执行器的时候,会开一个新的端口给xxl-job-admin调度中心调用而不是复用SpringBoot默认端口也是挺奇怪的?08、总结这篇文章主要讲了什么是定时任务、为什么要用定时任务、在Java领域中如果有定时任务相关的需求可以用什么来实现、分布式定时任务的基础知识以及如何接入XXL-JOB相信大家对分布式定时任务框架有了个基本的了解,如果感兴趣可以挑个开源框架去学学,想了解接入的代码可以把我的austin项目拉下来看看。主要的代码就在austin-cron的xxl包下,而分布式应用的代码主要在austin-web的MessageTemplateController跟模板的增删改查耦合在一起了。
01、注解日志打印日志的搭建我在austin最开始的前几篇已经有提及了,之前一直在等我的基友@蛮三刀酱他的日志组件库上传到Maven库,好让我使用使用下。在最近,他已经更新了两个版本,然后传到了Maven库了,所以我就来接入了这个组件库做的事情就是使用注解的方式来打印日志信息,并支持SpEL解析、自定义上下文以及自定义函数。它支持的东西听起来很牛逼,但说白了就是让记录日志的方式做得更装逼。我们写个破代码还能装逼,这谁受得了!这谁顶得住!现在我已经把注解在方法上定义了,当该方法被调用时,它打印了以下的日志:看起来很好用,对不对?通过一个注解,我就能把方法的入参信息打印出来,有bizType和bizId给我们自定义,那就可以很方便地定位出打印日志的地方了,并且他还贴心把response返回值也输出到日志上。至少在这个接口上,这非常符合我这个场景的需求,我们再通过一张图稍微重温下这个send接口到底做了什么事:在接口层面打印入参信息以及返回值就能定位到很多问题(懂的都懂),使用注解还不用干扰到我们正常的业务代码就能打印出这么好的日志信息了(这个逼是装上了)它的实现原理并不复杂,感兴趣的小伙伴可以拉代码自己看看,先看readmd再看代码!!GitHub:https://github.com/qqxx6661/logRecord总的来说,他通过SpEL表达式来读取到#sendRequest入参对象的信息,而注解解析则用的是Spring AOP。至于自定义上下文以及自定义函数我在这是没用到的,至少在austin项目场景下,我感觉都没什么用。哦,对了,它还能将日志输出到别的管道(MQ)。可惜的是,我这场景也用不到。在目前的实现下,我就只有这个接口能用到该组件,我承认他在某些场景是很好用。但它是有局限性的:打印的日志信息跟方法参数强相关:如果要打印方法参数以外的变量那需要用到上下文Context 或者自定义函数 。自定义函数的使用姿势是有局限性的,我们并不能把日志所涉及的变量都抽取到某函数上。如果用上下文Context的话,还是得嵌入业务代码里,那为啥不直接拼装好日志打呢?我一度怀疑是不是我的使用姿势不对,跟基友探讨了下,我的应用场景下还得自己抽取LogUtils进行日志打印。02、数据链路追踪从上面的接口打印的日志以及能很快地排查出接入层的问题了,其实重头戏其实是在处理层上,回顾下处理层目前做的事情:在处理层上会有不少的平台过滤规则,这些过滤规则大多都不是针对于消息模板的,而是针对于userId(接收者)的。在这个处理过程中,记录下每个消息模板中的每个用户的执行情况就尤其重要了。1、定位和排查问题。如果客户反馈用户收不到短信,一般情况下都在这个处理的过程中导致的(可能是被去重,可能是调用接口出问题)2、对模板执行的整体链路数据分析。一个消息模板一天发送的量级,中途被每个规则过滤的量级,成功下发的量级以及消息最后被点击的量级。除了点击数据,其他的数据都来源处理层基于上面的背景,我设计了一套埋点的规则,在处理关键链路上打上对应的点位📝目前点位的信息是不全的,随着系统的完善和接入各个渠道,这里的点位信息还会继续增加,只要我们认为有哪些地方是需要记录下来的,就可以增加。可能看到这里你会觉得有些抽象,我请求一次接口打印下日志就容易懂啦:// 1、接入层打印日志(returnStr打印处理结果,而msg打印出入参信息) 2022-01-08 15:44:53.512 [http-nio-8080-exec-7] INFO com.java3y.austin.utils.LogUtils - {"bizId":"1","bizType":"SendService#send","logId":"34df87fc-0489-46c1-b39f-cafd7652f55b", "msg":"{\"code\":\"send\",\"messageParam\":{\"extra\":null,\"receiver\":\"13288888888\",\"variables\":{\"title\":\"yyyyyy\",\"contentValue\":\"66661641627893157\"}},\"messageTemplateId\":1}","operateDate":1641627893512,"returnStr":"{\"code\":\"00000\",\"msg\":\"操作成功\"}","success":true,"tag":"operation"} // 2、处理层打印入口日志(表示成功消费到Kafka的消息 state=10) 2022-01-08 15:44:53.622 [org.springframework.kafka.KafkaListenerEndpointContainer#6-0-C-1] INFO com.java3y.austin.utils.LogUtils - {"businessId":1000000120220108,"ids":["13288888888"],"state":10,"timestamp":1641627893622} // 3、处理层打印入口日志(表示成功消费到Kafka的原始日志) 2022-01-08 15:44:53.622 [org.springframework.kafka.KafkaListenerEndpointContainer#6-0-C-1] INFO com.java3y.austin.utils.LogUtils - {"bizType":"Receiver#consumer","object":{"businessId":1000000120220108,"contentModel":{"content":"66661641627893157"},"deduplicationTime":1,"idType":30,"isNightShield":0,"messageTemplateId":1,"msgType":10,"receiver":["13288888888"],"sendAccount":66,"sendChannel":30,"templateType":10},"timestamp":1641627893622} // 4、处理层打印逻辑过滤日志(state=20,表示这条消息由于配置了丢弃,已经丢弃掉) 2022-01-08 15:44:53.623 [pool-8-thread-3] INFO com.java3y.austin.utils.LogUtils - {"businessId":1000000120220108,"ids":["13288888888"],"state":20,"timestamp":1641627893622}我打印日志的核心逻辑是:在入口侧(这里包括接口的入口以及刚消费Kafka的入口)需要打印出原始的信息。原始信息有了,才好对问题进行定位和排查,至少帮助我们复现在处理过程中使用某个标识来标明处理的过程(10代表成功消费Kafka,20代表该消息已经被丢弃...),并且日志的格式是统一的这样后续我们可以统一清洗该日志信息至于打日志的过程就很简单了,只要抽取一个LogUtils类就好咯:那对于点击是怎么追踪的呢?其实也好办,在下发的链接上拼接businessId就好了。只要我们能拿到点击的数据,在链接上就可以判断是否存在track_code_bid字符,进而找到是哪个用户点击了哪个模板消息。无论是打点日志还是原始日志,businessId会跟随着消息的生命周期始终。而businessId的构成只是通过消息模板内容+时间而成03、后续现在已经打印出对应的数据链路信息了,但这是不够的,这只是将数据链路信息写到了服务器的本地上,还需要考虑以下的情况:1、运行应用的服务器一般是集群,日志数据会记录到不同的机器上,排查和定位问题只能登录各个服务器查看2、链路的数据需要实时,通过提供Web后台的界面功能快速让业务方自助查看整个流程3、链路的数据需要离线保存用于对数据的分析以及留备份(本地日志往往存放不超过30天)后面这些功能都会一一实现,优先会接入ELK来有统一查询日志信息的入口以及配置相关的业务监控或告警,敬请期待。
01、什么是分布式配置中心在之前我就很早已经提及过:分布式配置中心这种组件在后端就是标配的。要理解分布式配置中心很简单:其实就是把一些配置的信息分离于自身的系统,而这些信息又能被应用实时获取得到。要做到上面的核心功能并不难,但是作为中间件会需要更多的配套服务,包括但不限于1、有后台界面供我们修改配置2、配置服务如果挂了有相关的容灾逻辑3、支持不同环境下的配置信息(我们线上的配置一般是分不同的环境配置不同的值)4、相关权限管理(只有负责人才能对配置进行update)5、简单易用(有对应的SDK支持或api支持)...有的公司会自研一套这种分布式配置中心的组件,实现了上面我提到的功能。作为个人或者小公司,直接上开源的就完事了。别老想着自研多么美妙,维护成本极大的。02、为什么分布式配置中心我们可以把常变动的配置信息存放在分布式配置中心上,比如:请求的ip地址、限流值、系统的配置值、各种业务开关等等。甚至,我老东家的规则引擎也是在分布式配置中心的基础上干的,分布式配置中心用到的场景是在是太多了...就以我们austin项目为例就好了,这期我们要实现丢弃消息。没错,你没看错。我们项目的核心是发消息,但需要在系统中实现丢弃消息的功能。austin作为推送平台,它的定位是面向整个公司的所有类型的消息推送。有了这个定位以后,我们很难去保证用这个系统的都是些什么人(自然在这里面就会有粗心的)。从austin的实现架构,我们可以发现的是:如果瞬间有大批量消息需要被下发时,数据会堵在MQ上等待消费我们是在austin-api层实现了判断模板是否被删除的校验,但很有可能的是:请求已经全部被austin-api处理完毕了,消息已经积压在MQ了。是可以在austini-handler再判断一遍模板是否被删除,但很多时候消息模板的拥有者并不是想把模板删掉(删掉意味着他们在控制台就看不到该模板的配置消息了),可能他们就只是发错了而已,希望还没下发的消息不再发送而已。除此之外,我们还得在austin项目实现白名单拦截的功能,这功能作用于dev和pre环境。对于austin项目而言,dev和pre环境跟线上环境其实没有什么本质上的区别。因为最终是下发消息,只要环境能把消息下发到用户手上,那就可以把他当做线上环境在用。一般业务在正式下发消息之前,都会在dev和pre环境走一遍流程。但我们是很难保证它们的测试一定是正常的,万一业务方就出Bug导致dev/pre环境大批量推送了呢?所以,我们会在dev/pre环境设置白名单,只有在白名单的内的用户才能收到消息。而白名单的列表我们又可以维护在分布式配置中心上PS :相信大家多多少少都见过很多推送的事故(各大厂貌似都有过类似的新闻和经历)。在很大原因上,就是环境混用了。本来想用dev或者pre环境去测试消息下发,不料使用了生产环境。(这种问题一般就需要通过权限和审批的干预了)像之前的实现的去重功能,我在代码硬编码写了具体的num和seconds值。这些值也许有一天都会随着运营规则有所变动,所以也会抽到分布式配置中心上。....03、分布式配置中心 选择从我第一天把Apollo写入到austin可能要引入的中间件,就有很多人问我:为什么选择Apollo。我还挺纳闷的,怎么就这个中间件问我的特别多呢?分布式配置中心可选择的项目也是蛮多的:在网上也有很多相关的对比,比如:功能特性重要性spring-cloud-configApollodisconfNacos静态配置管理高基于file支持支持支持动态配置管理高支持支持支持支持统一管理高无,需要github支持支持支持多环境中无,需要github支持支持支持本地配置缓存高无支持支持支持配置锁中支持不支持不支持不支持配置校验中无无无无配置生效时间高重启生效,或手动refresh生效实时实时实时配置更新推送高需要手工触发支持支持支持配置定时拉取高无支持配置更新目前依赖事件驱动, client重启或者server端推送操支持用户权限管理中无,需要github支持支持支持授权、审核、审计中无,需要github支持无支持配置版本管理高Git做版本管理界面上直接提供发布历史和回滚按钮操作记录有落数据库,但无查询接口界面操作,支持回滚配置合规检测高不支持支持(但还需完善)支持实例配置监控高需要结合spring admin支持支持,可以查看每个配置在哪些机器上加载支持灰度发布中不支持支持不支持部分更新支持告警通知中不支持支持,邮件方式告警支持,邮件方式告警支持总体来说:Apollo支持的功能齐全、社区活跃、中文文档丰富。所以,我就选择了Apollo。社区活跃太重要了,当你使用某个框架时出现问题,然后网上一搜,发现都没人有过类似的踩坑记录,这时候头都大了。之前我就提到过:技术选型并往往不跟技术挂钩。如果是个人项目,选个社区活跃的,并且该中间件已经被踩了很多坑的,学习它的思想和原理就能举一反三。等以后知识面上去了,觉得自己当时脑子进了屎选了个破玩意,切换成本一般也不会有多大。如果是在公司,本身就有类似的中间件,该用什么就用什么,在这基础上修修补补就好了。如果没有类似的中间件,那就多点花时间调研,但我认为在选取的时候最后还是离不开中间件的成熟度和社区活跃度(也有可能大老板按照以往的习惯一拍板...)虽说如此,感兴趣的还是可以多看看对比对比,这类文章在网上很多。(别老想着我什么都喂给你)04、分布式配置中心原理我以前的公司是自研的分布式配置中心,我曾经就看过其原理思想。那时候看到公司自研的技术实现是利用长连接使配置能实时被客户端监听到。这次引用了Apollo,我也去看了下设计文档,也是通过长轮询的方式实现客户端实时感知推荐大家去读一读,如果对分布式配置中心不太熟悉或者不了解它是什么东西的话。携程Apollo配置中心架构剖析演进https://www.apolloconfig.com/#/zh/design/apollo-design对于这块,我感觉我没什么可讲的,我平白无事也不会去捞源码看(除非特别对某个技术实现感兴趣,想看看人家是怎么实现的)。而Apollo文档这块做得是相当不错了。我针对性从头读到尾,感觉挺流畅的,貌似不太需要我补充什么内容。05、部署Apollo部署Apollo跟之前一样直接用docker-compose就完事了,在GitHub已经给出了对应的教程和docker-compose.yml以及相关的文件,直接复制粘贴就完事咯。https://www.apolloconfig.com/#/zh/deployment/quick-start-dockerhttps://github.com/apolloconfig/apollo/tree/master/scripts/docker-quick-start由于端口的占用问题,我换了下映射端口,最主要看两个端口吧:8070是后台控制页面的端口,8080是服务的端口06、SpringBoot 使用apollo写到这的时候,发现我是真的没啥好写的,我无非也是跟着官方文档弄弄。唯一的好处是我有现成的代码,跟着做的同学可以直接复制粘贴就完了。1、引入maven的依赖<dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client-config-data</artifactId> <version>1.9.1</version> </dependency>2、在配置文件上加入apollo的配置信息:# apollo TODO app: id: austin apollo: bootstrap: enabled: true namespaces: boss.austin配置的信息是在apollo的后台上新增的(这块大家只要能打开后台,问题就不大了,操作都挺简单的,感觉也没必要看啥文档)部门的创建其实也是一份"配置",输入organizations就能把现有的部门给改掉,我新增了boss股东部门,大家都是我的股东。3、在Spring中直接使用ApolloConfig就完了还值得一提的是,我们是在云服务器上使用docker部署的apollo的。一般获取姿势配置都是在内网上暴露对应的服务地址的,但我们这先体验的,所以可以直接跳过meta server为了方便使用,直接在启动的时候设置下参数就好了(跟着做的同学可以换下自己的ip和端口)08、总结这篇文章简单介绍了什么是分布式配置中心,以及分布式配置中心能用来干什么,介绍了如何入门Apollo,使用SpringBoot环境下使用Apollo。我强烈建议如果不了解分布式配置中心的同学可以从Apollo入手,根据上面给出的链接阅读下他的架构由来以及它的设计理念。作为一个markdown程序员而言,我觉得写得很不错的了。对这感兴趣的,也可以深入阅读下源码,看看关键的功能是怎么实现的(这不又是一条学习路径?)如果公司还没有用到分布式配置中心的,看完文章看看自己的项目有没有相关的场景,可以专研下来接入下(一整个Q的KPI/OKR就有了,不用愁了)
说起环境,跟大家回忆下austin到目前为止做了啥:使用Maven作为项目管理工具,使用SpringBoot作为技术的框架使用logback日志来记录系统运行时的信息引入了Hutool、Guava、OkHttp、fastjson等工具包助我们更块地编写代码接入腾讯云发送短信昨天晚上push了一把代码,大家可以先pull下来预习,明天或者后天我会详细说说austin数据库层面上的事(文章正在疯狂写中,每天熬夜写文章也是累啊!)pull代码后应该发现这次多了的内容是数据库层面的(sql 文件夹和对应yml的配置信息),并且把短信发送记录存储到DB中。这次austin项目我用的是MySQL作为关系型数据库,使用SpringData JPA 作为 ORM框架。至于原因并还有别的要聊的,我都放在下一篇文章了。01、MySQL安装环境:CentOS 7.6 64bit一、下载并安装mysql:wget -i -c http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm yum -y install mysql57-community-release-el7-10.noarch.rpm二、启动并查看状态MySQL:systemctl start mysqld.service systemctl status mysqld.service三、查看MySQL的默认密码:grep "password" /var/log/mysqld.log四、登录进MySQLmysql -uroot -p五、修改默认密码(设置密码需要有大小写符号组合---安全性),把下面的my passrod替换成自己的密码ALTER USER 'root'@'localhost' IDENTIFIED BY 'my password';六、开启远程访问 (把下面的my passrod替换成自己的密码)grant all privileges on *.* to 'root'@'%' identified by 'my password' with grant option; flush privileges; exit七、在云服务上增加MySQL的端口:我从购买云服务到用本地连接,应该只花了20分钟。02、到目前为止如何使用austin一、进入gitee,点个starhttps://gitee.com/zhongfucheng/austin二、使用 git clone命令把代码clone到本地git clone https://gitee.com/zhongfucheng/austin.git三、用你喜欢的IDE打开clone下来的文件夹,并等待Maven加载jar包四、打开properties.yml文件,修改对应的配置(主要是数据库和腾讯云短信账号信息)五、打开austin.sql文件,得到表创建的DDL(后面有新增的表也会在这里更新),执行表的DDL六、找到austin-web模块对应的Controller,调试短信(可以在这个过程中debug了解流程)
开胃菜(复习)作为用户,我们写好Flink的程序,上管理平台提交,Flink就跑起来了(只要程序代码没有问题),细节对用户都是屏蔽的。实际上大致的流程是这样的:Flink会根据我们所写代码,会生成一个StreamGraph的图出来,来代表我们所写程序的拓扑结构。然后在提交的之前会将StreamGraph这个图优化一把(可以合并的任务进行合并),变成JobGraph将JobGraph提交给JobManagerJobManager收到之后JobGraph之后会根据JobGraph生成ExecutionGraph(ExecutionGraph 是 JobGraph 的并行化版本)TaskManager接收到任务之后会将ExecutionGraph生成为真正的物理执行图可以看到物理执行图真正运行在TaskManager上Transform和Sink之间都会有ResultPartition和InputGate这俩个组件,ResultPartition用来发送数据,而InputGate用来接收数据。屏蔽掉这些Graph,可以发现Flink的架构是:Client->JobManager->TaskManager从名字就可以看出,JobManager是干「管理」,而TaskManager是真正干活的。回到我们今天的主题,checkpoint就是由JobManager发出。Flink本身就是有状态的,Flink可以让你选择执行过程中的数据保存在哪里,目前有三个地方,在Flink的角度称作State Backends:MemoryStateBackend(内存)FsStateBackend(文件系统,一般是HSFS)RocksDBStateBackend(RocksDB数据库)同样地,checkpoint信息就是保存在State Backends上先来简单描述一下checkpoint的实现流程:checkpoint的实现大致就是插入barrier,每个operator收到barrier就上报给JobManager,等到所有的operator都上报了barrier,那JobManager 就去完成一次checkpointi因为checkpoint机制是Flink实现容错机制的关键,我们在实际使用中,往往都要配置checkpoint相关的配置,例如有以下的配置:final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.enableCheckpointing(5000); env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE); env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500); env.getCheckpointConfig().setCheckpointTimeout(60000); env.getCheckpointConfig().setMaxConcurrentCheckpoints(1); env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);简单铺垫过后,我们就来撸源码了咯?Checkpoint(原理)JobManager发送checkpoint从上面的图我们可以发现 checkpoint是由JobManager发出的,并且JobManager收到的是JobGraph,会将JobGraph转换成ExecutionGraph。这块在JobMaster的构造器就能体现出来:public JobMaster(...) throws Exception { // 创建ExecutionGraph this.executionGraph = createAndRestoreExecutionGraph(jobManagerJobMetricGroup); }我们点击进去createAndRestoreExecutionGraph看下:看CheckpointCoordinator这个名字,就觉得他很重要,有木有?它从ExecutionGraph来,我们就进去createExecutionGraph里边看看呗。点了两层buildGraph()方法,可以看到在方法的末尾处有checkpoint相关的信息:executionGraph.enableCheckpointing( chkConfig.getCheckpointInterval(), chkConfig.getCheckpointTimeout(), chkConfig.getMinPauseBetweenCheckpoints(), chkConfig.getMaxConcurrentCheckpoints(), chkConfig.getCheckpointRetentionPolicy(), triggerVertices, ackVertices, confirmVertices, hooks, checkpointIdCounter, completedCheckpoints, rootBackend, checkpointStatsTracker);前面的几个参数就是我们在配置checkpoint参数的时候指定的,而triggerVertices/confirmVertices/ackVertices我们溯源看了一下,在源码中注释也写得清清楚楚的。// collect the vertices that receive "trigger checkpoint" messages. // currently, these are all the sources List<JobVertexID> triggerVertices = new ArrayList<>(); // collect the vertices that need to acknowledge the checkpoint // currently, these are all vertices List<JobVertexID> ackVertices = new ArrayList<>(jobVertices.size()); // collect the vertices that receive "commit checkpoint" messages // currently, these are all vertices List<JobVertexID> commitVertices = new ArrayList<>(jobVertices.size());下面还是进去enableCheckpointing()看看大致做了些什么吧:// 将上面的入参分别封装成ExecutionVertex数组 ExecutionVertex[] tasksToTrigger = collectExecutionVertices(verticesToTrigger); ExecutionVertex[] tasksToWaitFor = collectExecutionVertices(verticesToWaitFor); ExecutionVertex[] tasksToCommitTo = collectExecutionVertices(verticesToCommitTo); // 创建触发器 checkpointStatsTracker = checkNotNull(statsTracker, "CheckpointStatsTracker"); // 创建checkpoint协调器 checkpointCoordinator = new CheckpointCoordinator( jobInformation.getJobId(), interval, checkpointTimeout, minPauseBetweenCheckpoints, maxConcurrentCheckpoints, retentionPolicy, tasksToTrigger, tasksToWaitFor, tasksToCommitTo, checkpointIDCounter, checkpointStore, checkpointStateBackend, ioExecutor, SharedStateRegistry.DEFAULT_FACTORY); // 设置触发器 checkpointCoordinator.setCheckpointStatsTracker(checkpointStatsTracker); // 状态变更监听器 // job status changes (running -> on, all other states -> off) if (interval != Long.MAX_VALUE) { registerJobStatusListener(checkpointCoordinator.createActivatorDeactivator()); }值得一提的是,点进去CheckpointCoordinator()构造方法可以发现有状态后端StateBackend的身影(因为checkpoint就是保存在所配置的状态后端)如果Job的状态变更了,CheckpointCoordinatorDeActivator是能监听到的。当我们的Job启动的时候,又简单看看startCheckpointScheduler()里边究竟做了些什么操作:它会启动一个定时任务,我们具体看看定时任务具体做了些什么ScheduledTrigger,然后看到比较重要的方法:triggerCheckpoint()这块代码的逻辑有点多,我们简单来总结一下前置检查(是否可以触发checkpoint,距离上一次checkpoint的间隔时间是否符合...)检查是否所有的需要做checkpoint的Task都处于running状态生成checkpointId,然后生成PendingCheckpoint对象来代表待处理的检查点注册一个定时任务,如果checkpoint超时后取消checkpoint注:检查task的任务状态时,只会把source的task封装给进Execution[]数组JobManager侧只会发给source的task发送checkpointJobManager发送总结贴的图有点多,最后再来简单总结一波,顺便画个流程图,你就会发现还是比较清晰的。JobManager 收到client提交的JobGraphJobManger 需要通过JobGraph生成ExecutionGraph在生成ExcutionGraph的过程中实际上就会触发checkpoint的逻辑定时任务会前置检查(其实就是你实际上配置的各种参数是否符合)判断checkpoint相关的task是否都是running状态,将source的任务封装到Execution数组中创建checkpointID/checkpointStorageLocation(checkpoint保存的地方)/PendingCheckpoint(待处理的checkpoint)创建定时任务(如果当checkpoint超时,会将相关状态清除,重新触发)真正触发checkPoint给TaskManager(只会发给source的task)找出所有source和需要ack的Task创建checkpointCoordinator 协调器创建CheckpointCoordinatorDeActivator监听器,监听Job状态的变更当Job启动时,会触发ScheduledTrigger 定时任务TaskManager(source Task接收)前面提到了,JobManager 在生成ExcutionGraph时,会给所有的source 任务发送checkpoint,那么source收到barrier又是怎么处理的呢?会到TaskExecutor这里进行处理。TaskExecutor有个triggerCheckpoint()方法对接收到的checkpoint进行处理:进入triggerCheckpointBarrier()看看:再想点进去triggerCheckpoint()看实现时,我们会发现走到performCheckpoint()这个方法上:从实现的注释我们可以很方便看出方法大概做了什么:这块我们先在这里放着,知道Source的任务接收到Checkpoint会广播到下游,然后会做快照处理就好。下面看看非Source 的任务接收到checkpoint是怎么处理的。
kylin介绍kylin是我们国人主导并贡献到Apache基金会的开源项目,所以我们会有中文文档学习:http://kylin.apache.org/cn/从官方我们可以看到对kylin的介绍:Apache Kylin™是一个开源的、分布式的分析型数据仓库,提供Hadoop/Spark 之上的SQL查询接口及多维分析(OLAP)能力以支持超大规模数据,最初由 eBay 开发并贡献至开源社区,它能在亚秒内查询巨大的表。看到这个介绍,只能用两个字来形容kylin:牛逼🐂。那牛逼在哪呢?下面再说第一眼看过去,可能有的同学不知道OLAP是什么东西,我下面来简单解释一下吧。(Hadoop/Spark/SQL/大数据这些词天天能看见,即便不懂它的原理,你都知道这些东西是有什么用,是用来干嘛的,对吧?)看到OLAP就不得不提它的兄弟OLTP,我们简单来看看他们的全称和翻译的中文是什么:OLTP:On-Line Transaction Processing(联机事务处理)OLAP:On-Line Analytical Processing(联机分析处理)中文的翻译我们怕是看不懂的了,但我们可以发现他俩的区别一个是「事务」,一个是「分析」从应用层面看,我们可以简单地认为:OLTP主要用于业务系统,对事务的要求比较高,例如下单/交易(银行转账等业务)。OLAP主要用于数据仓库系统,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。我再画张思维导图图来给大家看一下,基本就懂了:看到这里,你应该对OLAP有个基本的了解了。那再回到上面那句话:多维分析(OLAP)能力以支持超大规模数据,你第一反应会想到什么?三歪第一反应想到的就是Hive(Hive底层是HDFS:支持超大规模的数据)。那既然说到Hive了,你会发现kylin前半段话,Hive好像几乎都可以支持,但除了最后一句「它能在亚秒内查询巨大的表」。没错,到这里就可以知道kylin的用途了:它可以在亚秒内查询巨大的表,来完成数据分析和决策每次跑Hive我们可能都得跑几分钟(像我SQL写得烂的,跑半小时也是经常有的事),我们从业务上就希望用来分析的数据可以跑得更快,支持这种需求的kylin就火🔥起来了。我以Hive来引申kylin,除了kylin就没其他选择了吗?那显然不是的。当年我刚进公司的时候,吐槽Hive跑得太慢了,隔壁的小哥就告诉我:你用presto啊,我们大数据平台都支持的。OLAP所提供的工具框架还是很多的,下面我们来简单认识一下吧众所周知,执行Hive实际上是跑Map-Reduce任务去HDFS拿数据。执行的过程涉及到计算和存储。有的人觉得Hive跑Map-Reduce计算这个过程太慢了,所以就不用Map-Reduce,用别的计算引擎,比如用MPP架构来跑,但存储没变...有的人觉得,存储在HDFS去拿数据太慢了,改个存储的地方,不从HDFS拿...有的人觉得,这啥破玩意,计算和存储我都改了,用我的框架一站式给你解决掉...有的人觉得,Hadoop生态还是可以的,我先聚合一把,你查的时候直接拿聚合后的数据,也是很快的...由于每个公司的业务场景和背景不一样,每个OLAP框架的长处也不一样,所以现在有如此多的OLAP技术在发光发热...Kylin入门从前面我们已经知道为什么会出现如此多的OLAP的技术了,从本质上来说就是我们希望分析的数据可以让我们查得更快,而kylin是这些技术其中的一员。从上图也可以看到kylin是完全依赖Hadoop生态的,那kylin是怎么实现提速的呢?答案就是:预聚合假设我们从MySQL检索日期大于2020-10-20的所有数据,只要我们在日期列加上索引,可以很快就能查出相关的数据。但如果我们从MySQL检索日期大于2020-10-20的所有数据且每个用户在这段时间内消费了多少钱且xxxx,只要数据量大,不论你怎么建索引,查询的速度就不尽人意了。那如果我按天的维度先做好对每个用户的统计,写到一张表中,等到用户按日期检索的时候是不是就很快了(因为我已经按天聚合了一次数据,这张表比起原来的原始表数量会大大减少)kylin就是用预聚合这种思路来提高查询的速度,使它可以在亚秒内实现查询响应。那我们使用kylin的步骤是什么?官方已经帮我们解答了:定义数据集上的一个星形或雪花形模型在定义的数据表上构建cube使用标准 SQL 通过 ODBC、JDBC 或 RESTFUL API 进行查询,仅需亚秒级响应时间即可获得查询结果上面几个步骤,可能你不太了解的几个词有以下 星形模型、雪花模型、cube,下面我来简单解释一下:在数据仓库领域上,我们的主表叫做事实表,事实表外键依赖的表叫做维度表。「星形模型」:所有的维度表都直连到事实表。(上图)「雪花形模型」:当有一个或多个维度表没有直接连接到事实表上,而需要通过其他维表连接到事实表(下图)在kylin里,分析数据的角度叫做「维度」,被分析的指标叫做「度量」好了,我们再来看看cube是什么意思吧:一个多维数据集称为一个OLAP Cube:上面的几张二维表我们可以形成一个数据立方体,这个数据立方体就是Cube一个Cube可以由不同的角度去看,可以看似这多个角度都是从一个完整的Cube拆分出来的,例如:结合上面所说的:Cube实际上就是从数据集中通过不同的维度构建出来的一个立方体(虽然图上的都是三维,但你构建的Cube可以远超三维)kylin就是在Cube这个立方体来获取数据的,从官方的说法也很明确,可以通过JDBC/RESTful的方式来获取数据。那kylin是将聚合的数据存储在哪的呢(肯定是有存储的地方的嘛)?在HBase上。如果还没学过HBase的同学,可以先看看我以往的文章:HBase入门使用kylin步骤:首先你得有数据(一般来自Hive/Kafka),在Kylin上定义对应的数据模型(结构)通过kylin系统配置需要聚合以及统计的字段(这块就是上面所提到的维度和度量),然后构建出Cube(这块就是kylin的预聚合,把需要统计的维度都定义好,提前计算)kylin会把数据存放在HBase上,你可以通过JDBC/RESTful的方式来查询数据使用kylin在官网上也列出比较常见的QA,大家可以看看:http://kylin.apache.org/cn/docs/gettingstarted/faq.html虽然kylin能支持多维度的聚合,但我们在建Cube一般要对Cube进行剪枝(即减少Cuboid的生成)假设我们有10 个维度,那么没有经过任何优化的Cube就会存在2的十次方 =1000+个Cuboid。Cube 的最大物理维度数量 (不包括衍生维度) 是 63,但是不推荐使用大于 30 个维度的 Cube,会引起维度灾难。常用的剪枝方式会用聚合组(Aggregation group)配置来实现,而在聚合组中,Mandatory(强制维度)又是用得比较多的。比如说,本来我有A、B、C三个维度,如果我不做任何优化,我的组合应该会有7个,分别是(A)(B)(C)(AB)(ABC)(AC)(BC),如果我指定A维度为强制维度,那最后的组合就只有(A)(AB)(ABC)(AC)。强制索引指的就是:指定的字段一定会被查询条件中除了强制维度(Mandatory),还有层级维度(Hierarchy)和联合维度(Joint)帮助我们剪枝(即减少Cuboid的生成),一般强制维度和联合维度用得比较多。我们去查kylin数据的时候,是已经被聚合过存放在HBase的,所以查询起来是相当快的,但是构建Cube这个过程其实是挺慢的(十几分钟到半小时都是正常的)。这就会带来延迟(Cube需要时间构建,同时也不可能秒级去请求构建一次Cube)那这能忍受吗?这意味着最新的数据得等Cube任务调度到了且Cube构建完成才能查到数据画外音:构建Cube一般都是定时任务的方式请求kylin的api进行构建的。Kylin 没有内置的调度程度。您可以通过 REST API 从外部调度程度服务中触发 Cube 的定时构建,如 Linux 的命令 crontab、Apache Airflow 等。但在新的kylin版本中已经支持realtime_olap了,kylin存储了实时的数据再加上HBase的数据merge后返回就实现了realtime最后这篇文章对kylin做了个简单的入门,细节还是得看官网(有中文,比较好读,文档也做得挺好的)。后面细节如果有必要我再来补充就好了
现在开发一般都是Mybatis,也有公司用的Hibernate或者Spring Data JPA。很多时候,不同的项目由不同的程序员开发,在公司层面可能没有将技术完全统一起来,一个项目用Mybatis,一个项目用Hibernate都是很有可能的。不管用的是什么ORM框架,都是在JDBC上封装了一层嘛,所以JDBC还是需要好好学习的。什么是ORM?Object_Relative DateBase-Mapping,在Java对象与关系数据库之间建立某种映射,以实现直接存取Java对象。很多同学不知道JDBC要学到怎么样的一种程度,这里我来讲讲JDBC的知识点有哪些,哪些应该是需要掌握的。JDBC基础知识什么是JDBC?JDBC全称为:Java Data Base Connectivity,它是可以执行SQL语句的Java API每种数据库都有自己的图形界面呀,我都可以在里边操作执行数据库相关的事,为什么我们要用JDBC?数据库里的数据是给谁用的?给程序用的。我们用的是Java程序语言,所以需要用Java程序去链接数据库来访问数据。市面上有非常多的数据库,本来我们是需要根据不同的数据库学习不同的API,sun公司为了简化这个操作,定义了JDBC API【接口】。对于我们来说,操作数据库都是在JDBC API【接口】上,使用不同的数据库,只要用数据库厂商提供的数据库驱动程序即可。其实可以好好细品一下JDBC,把接口定义出来,反正你给我实现就对了,无论数据库怎么变,用的时候是同一套API随后我们简单学习一下这几个接口:Connection、Statement、ResultSet。写出小白必学的Java连接数据库的代码:导入MySQL或者Oracle驱动包装载数据库驱动程序获取到与数据库连接获取可以执行SQL语句的对象执行SQL语句关闭连接Connection connection = null; Statement statement = null; ResultSet resultSet = null; try { /* * 加载驱动有两种方式 * * 1:会导致驱动会注册两次,过度依赖于mysql的api,脱离的mysql的开发包,程序则无法编译 * 2:驱动只会加载一次,不需要依赖具体的驱动,灵活性高 * * 我们一般都是使用第二种方式 * */ //1. //DriverManager.registerDriver(new com.mysql.jdbc.Driver()); //2. Class.forName("com.mysql.jdbc.Driver"); //获取与数据库连接的对象-Connetcion connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/zhongfucheng", "root", "root"); //获取执行sql语句的statement对象 statement = connection.createStatement(); //执行sql语句,拿到结果集 resultSet = statement.executeQuery("SELECT * FROM users"); //遍历结果集,得到数据 while (resultSet.next()) { System.out.println(resultSet.getString(1)); System.out.println(resultSet.getString(2)); } } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { /* * 关闭资源,后调用的先关闭 * * 关闭之前,要判断对象是否存在 * */ if (resultSet != null) { try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } }基本流程完了以后,我们要重点学习一下PreparedStatement接口与Statement接口的区别,为什么要用PreparedStatement。Statement对象编译SQL语句时,如果SQL语句有变量,就需要使用分隔符来隔开,如果变量非常多,就会使SQL变得非常复杂。PreparedStatement可以使用占位符,简化sql的编写Statement会频繁编译SQL。PreparedStatement可对SQL进行预编译,提高效率,预编译的SQL存储在PreparedStatement对象中PreparedStatement防止SQL注入。(Statement通过分隔符'++',编写永等式,可以不需要密码就进入数据库)数据库连接池为什么我们要使用数据库连接池?数据库的连接的建立和关闭是非常消耗资源的,频繁地打开、关闭连接造成系统性能低下常见的数据库连接池有C3P0、DBCP、Druid。大家在学习的时候可以用Druid,我曾经用C3P0写了个Demo被diss了(:Druid是阿里开源的一个项目,有中文文档,跟着学着连接数据库,我相信不会太难。GitHub搜「Druid」就能搜到了分页说到分页,面试和工作都是非常常见的了,是必须要掌握的技术。下面来简单说一下Oracle和MySQL是如何实现分页的,以及对应的解释:Oracle分页:/* Oracle分页语法: @lineSize---每页显示数据行数 @currentPage----当前所在页 */ SELECT *FROM ( SELECT 列名,列名,ROWNUM rn FROM 表名 WHERE ROWNUM<=(currentPage*lineSize)) temp WHERE temp.rn>(currentPage-1)*lineSize; /* Oracle分页: Oracle的分页依赖于ROWNUM这个伪列,ROWNUM主要作用就是产生行号。 分页原理: 1:子查询查出前n行数据,ROWNUM产生前N行的行号 2:使用子查询产生ROWNUM的行号,通过外部的筛选出想要的数据 例子: 我现在规定每页显示5行数据【lineSize=5】,我要查询第2页的数据【currentPage=2】 注:【对照着语法来看】 实现: 1:子查询查出前10条数据【ROWNUM<=10】 2:外部筛选出后面5条数据【ROWNUM>5】 3:这样我们就取到了后面5条的数据 */MySQL分页:/* Mysql分页语法: @start---偏移量,不设置就是从0开始【也就是(currentPage-1)*lineSize】 @length---长度,取多少行数据 */ SELECT * FROM 表名 LIMIT [START], length; /* 例子: 我现在规定每页显示5行数据,我要查询第2页的数据 分析: 1:第2页的数据其实就是从第6条数据开始,取5条 实现: 1:start为5【偏移量从0开始】 2:length为5 */总结:Mysql从(currentPage-1)*lineSize开始取数据,取lineSize条数据Oracle先获取currentPage*lineSize条数据,从(currentPage-1)*lineSize开始取数据DBUtilsDBUtils我觉得还算是一个挺好用组件,在学习Hibernate,Mybatis这些ORM框架之前,可以学着用用。可以极大简化我们的JDBC的代码,用起来也很方便。如果急忙着写毕业设计,还没时间来得及学ORM框架,用这个工具来写DAO数据访问层,我觉得是一个不错的选择。可以简单看看代码:/* * 使用DbUtils框架对数据库的CRUD * 批处理 * * */ public class Test { @org.junit.Test public void add() throws SQLException { //创建出QueryRunner对象 QueryRunner queryRunner = new QueryRunner(JdbcUtils.getDataSource()); String sql = "INSERT INTO student (id,name) VALUES(?,?)"; //我们发现query()方法有的需要传入Connection对象,有的不需要传入 //区别:你传入Connection对象是需要你来销毁该Connection,你不传入,由程序帮你把Connection放回到连接池中 queryRunner.update(sql, new Object[]{"100", "zhongfucheng"}); } @org.junit.Test public void query()throws SQLException { QueryRunner queryRunner = new QueryRunner(JdbcUtils.getDataSource()); String sql = "SELECT * FROM student"; List list = (List) queryRunner.query(sql, new BeanListHandler(Student.class)); System.out.println(list.size()); } @org.junit.Test public void delete() throws SQLException { QueryRunner queryRunner = new QueryRunner(JdbcUtils.getDataSource()); String sql = "DELETE FROM student WHERE id='100'"; queryRunner.update(sql); } @org.junit.Test public void update() throws SQLException { QueryRunner queryRunner = new QueryRunner(JdbcUtils.getDataSource()); String sql = "UPDATE student SET name=? WHERE id=?"; queryRunner.update(sql, new Object[]{"zhongfuchengaaa", 1}); } @org.junit.Test public void batch() throws SQLException { //创建出QueryRunner对象 QueryRunner queryRunner = new QueryRunner(JdbcUtils.getDataSource()); String sql = "INSERT INTO student (name,id) VALUES(?,?)"; Object[][] objects = new Object[10][]; for (int i = 0; i < 10; i++) { objects[i] = new Object[]{"aaa", i + 300}; } queryRunner.batch(sql, objects); } }放干货现在已经工作有一段时间了,为什么还来写JDBC呢,原因有以下几个:我是一个对排版有追求的人,如果早期关注我的同学可能会发现,我的GitHub、文章导航的read.me会经常更换。现在的GitHub导航也不合我心意了(太长了),并且早期的文章,说实话排版也不太行,我决定重新搞一波。我的文章会分发好几个平台,但文章发完了可能就没人看了,并且图床很可能因为平台的防盗链就挂掉了。又因为有很多的读者问我:”你能不能把你的文章转成PDF啊?“我写过很多系列级的文章,这些文章就几乎不会有太大的改动了,就非常适合把它们给”持久化“。基于上面的原因,我决定把我的系列文章汇总成一个PDF/HTML/WORD文档。说实话,打造这么一个文档花了我不少的时间。
前言2020年了,还需要学JSP吗?我相信现在还是在大学的同学肯定会有这个疑问。其实我在18年的时候已经见过类似的问题了「JSP还应该学习吗」。我在18年发了几篇JSP的文章,已经有不少的开发者评论『这不是上个世纪的东西了吗』『梦回几年前』『这么老的的东西,怎么还有人学』现在问题来了,JSP放在2020年,是真的老了吗?对,是真的老了现在问题又来了,为什么在几年前已经被定义『老』的技术,到2020年了还是有热度,每年还是有人在问:『还需要学习JSP吗』。我认为理由也很简单:JSP在之前用的是真的多!在我初学Java的时候,就经常听到:JSP和PHP是能够写动态网页的---《我的老师》。当我们去找相关的学习资料时,发现到处都是JSP的身影,会给我一种感觉:好像不懂JSP就压根没法继续往下学习一样。如果你是新手,如果你还没学习JSP,我建议还是可以了解一下,不需要深入去学习JSP的各种内容,但可以了解一下。至少别人说起JSP的时候,你能知道什么是JSP,能看懂JSP的代码。额外说一句:你去到公司,可能还能看到JSP的代码。虽然JSP是『老东西』,但我们去到公司可能就是维护老的项目。JSP可能不用你自己去写,但至少能看得懂,对不对。问题又来了,那JSP如果是『老东西』,那被什么替代了呢?要么就是用常见的模板引擎『freemarker』『Thymeleaf』『Velocity』,用法其实跟『JSP』差不太多,只是它们的性能会更好。要么前后端分离,后端只需要返回JSON给前端,页面完全不需要后端管。说了这么多,我想说的是:“JSP还是有必要了解一下,不需要花很多时间,知道即可,这篇文章我就能带你认识JSP”什么是JSP?JSP全名为Java Server Pages,java服务器页面。JSP是一种基于文本的程序,其特点就是HTML和Java代码共同存在!JSP是为了简化Servlet的工作出现的替代品,Servlet输出HTML非常困难,JSP就是替代Servlet输出HTML的。在Tomcat博客中我提到过:Tomcat访问任何的资源都是在访问Servlet!,当然了,JSP也不例外!JSP本身就是一种Servlet。为什么我说JSP本身就是一种Servlet呢?其实JSP在第一次被访问的时候会被编译为HttpJspPage类(该类是HttpServlet的一个子类)比如我随便找一个JSP,编译后的JSP长这个样:package org.apache.jsp; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; import java.util.Date; public final class _1_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent { private static final JspFactory _jspxFactory = JspFactory.getDefaultFactory(); private static java.util.List<String> _jspx_dependants; private javax.el.ExpressionFactory _el_expressionfactory; private org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.List<String> getDependants() { return _jspx_dependants; } public void _jspInit() { _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory(); _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig()); } public void _jspDestroy() { } public void _jspService(final HttpServletRequest request, final HttpServletResponse response) throws java.io.IOException, ServletException { final PageContext pageContext; HttpSession session = null; final ServletContext application; final ServletConfig config; JspWriter out = null; final Object page = this; JspWriter _jspx_out = null; PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out; out.write("\r\n"); out.write("\r\n"); out.write("<html>\r\n"); out.write("<head>\r\n"); out.write(" <title>简单使用JSP</title>\r\n"); out.write("</head>\r\n"); out.write("<body>\r\n"); String s = "HelloWorda"; out.println(s); out.write("\r\n"); out.write("</body>\r\n"); out.write("</html>\r\n"); } catch (Throwable t) { if (!(t instanceof SkipPageException)){ out = _jspx_out; if (out != null && out.getBufferSize() != 0) try { out.clearBuffer(); } catch (java.io.IOException e) {} if (_jspx_page_context != null) _jspx_page_context.handlePageException(t); } } finally { _jspxFactory.releasePageContext(_jspx_page_context); } } }编译过程是这样子的:浏览器第一次请求1.jsp时,Tomcat会将1.jsp转化成1_jsp.java这么一个类,并将该文件编译成class文件。编译完毕后再运行class文件来响应浏览器的请求。以后访问1.jsp就不再重新编译jsp文件了,直接调用class文件来响应浏览器。当然了,如果Tomcat检测到JSP页面改动了的话,会重新编译的。既然JSP是一个Servlet,那JSP页面中的HTML排版标签是怎么样被发送到浏览器的?我们来看下上面1_jsp.java的源码就知道了。原来就是用write()出去的罢了。说到底,JSP就是封装了Servlet的java程序罢了。out.write("\r\n"); out.write("\r\n"); out.write("<html>\r\n"); out.write("<head>\r\n"); out.write(" <title>简单使用JSP</title>\r\n"); out.write("</head>\r\n"); out.write("<body>\r\n");有人可能也会问:JSP页面的代码服务器是怎么执行的?再看回1_jsp.java文件,java代码就直接在类中的service()中。String s = "HelloWorda"; out.println(s);JSP内置了9个对象!内置对象有:out、session、response、request、config、page、application、pageContext、exception。重要要记住的是:JSP的本质其实就是Servlet。只是JSP当初设计的目的是为了简化Servlet输出HTML代码。什么时候用JSP重复一句:JSP的本质其实就是Servlet。只是JSP当初设计的目的是为了简化Servlet输出HTML代码。我们的Java代码还是写在Servlet上的,不会写在JSP上。在知乎曾经看到一个问题:“如何使用JSP连接JDBC”。显然,我们可以这样做,但是没必要。JSP看起来就像是一个HTML,再往里边增加大量的Java代码,这是不正常,不容易阅读的。所以,我们一般的模式是:在Servlet处理好的数据,转发到JSP,JSP只管对小部分的数据处理以及JSP本身写好的页面。例如,下面的Servlet处理好表单的数据,放在request对象,转发到JSP//验证表单的数据是否合法,如果不合法就跳转回去注册的页面 if(formBean.validate()==false){ //在跳转之前,把formbean对象传递给注册页面 request.setAttribute("formbean", formBean); request.getRequestDispatcher("/WEB-INF/register.jsp").forward(request, response); return; }JSP拿到Servlet处理好的数据,做显示使用:JSP需要学什么JSP我们要学的其实两块就够了:JSTL和EL表达式EL表达式表达式语言(Expression Language,EL),EL表达式是用${}括起来的脚本,用来更方便的读取对象!EL表达式主要用来读取数据,进行内容的显示!为什么要使用EL表达式?我们先来看一下没有EL表达式是怎么样读取对象数据的吧!在1.jsp中设置了Session属性<%@ page language="java" contentType="text/html" pageEncoding="UTF-8"%> <html> <head> <title>向session设置一个属性</title> </head> <body> <% //向session设置一个属性 session.setAttribute("name", "aaa"); System.out.println("向session设置了一个属性"); %> </body> </html>在2.jsp中获取Session设置的属性<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title></title> </head> <body> <% String value = (String) session.getAttribute("name"); out.write(value); %> </body> </html>效果:上面看起来,也没有多复杂呀,那我们试试EL表达式的!在2.jsp中读取Session设置的属性<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title></title> </head> <body> ${name} </body> </html>只用了简简单单的几个字母就能输出Session设置的属性了!并且输出在浏览器上!使用EL表达式可以方便地读取对象中的属性、提交的参数、JavaBean、甚至集合!JSTLJSTL全称为 JSP Standard Tag Library 即JSP标准标签库。JSTL作为最基本的标签库,提供了一系列的JSP标签,实现了基本的功能:集合的遍历、数据的输出、字符串的处理、数据的格式化等等!为什么要使用JSTL?EL表达式不够完美,需要JSTL的支持!在JSP中,我们前面已经用到了EL表达式,体会到了EL表达式的强大功能:使用EL表达式可以很方便地引用一些JavaBean以及其属性,不会抛出NullPointerException之类的错误!但是,EL表达式非常有限,它不能遍历集合,做逻辑的控制。这时,就需要JSTL的支持了!Scriptlet的可读性,维护性,重用性都十分差!JSTL与HTML代码十分类似,遵循着XML标签语法,使用JSTL让JSP页面显得整洁,可读性非常好,重用性非常高,可以完成复杂的功能!之前我们在使用EL表达式获取到集合的数据,遍历集合都是用scriptlet代码循环,现在我们学了forEach标签就可以舍弃scriptlet代码了。向Session中设置属性,属性的类型是List集合<% List list = new ArrayList<>(); list.add("zhongfucheng"); list.add("ouzicheng"); list.add("xiaoming"); session.setAttribute("list", list); %>遍历session属性中的List集合,items:即将要迭代的集合。var:当前迭代到的元素<c:forEach var="list" items="${list}" > ${list}<br> </c:forEach>效果:放干货现在已经工作有一段时间了,为什么还来写JSP呢,原因有以下几个:我是一个对排版有追求的人,如果早期关注我的同学可能会发现,我的GitHub、文章导航的read.me会经常更换。现在的GitHub导航也不合我心意了(太长了),并且早期的文章,说实话排版也不太行,我决定重新搞一波。我的文章会分发好几个平台,但文章发完了可能就没人看了,并且图床很可能因为平台的防盗链就挂掉了。又因为有很多的读者问我:”你能不能把你的文章转成PDF啊?“我写过很多系列级的文章,这些文章就几乎不会有太大的改动了,就非常适合把它们给”持久化“。基于上面的原因,我决定把我的系列文章汇总成一个PDF/HTML/WORD文档。说实话,打造这么一个文档花了我不少的时间。为了防止白嫖,关注我的公众号回复「888」即可获取。PDF的内容非常非常长,干货非常非常的硬,有兴趣的同学可以浏览一波。记住:JSP我们只需要了解即可,不需要深入去学习每个知识点,因为在现实开发中很可能用不上。
前言JSON相信大家对他也不陌生了,前后端交互中常常就以JSON来进行数据交换。而有的时候,我们也会将JSON直接保存在数据库中。可能就有人不太理解,为什么要将JSON保存在关系型数据库中?我在最开始的时候也有类似的疑惑,问了几个同事,得出的结论都差不多:方便扩展,如果那些字段不需要用到索引,改动比较频繁,你又不想改动表的结构,那就可以在数据库中存入JSON虽说存JSON会方便扩展,但如果你的MySQL版本还是相对较低的话,想要用SQL查JSON里某个属性,还是比较麻烦的。并且从数据库里边取出来也仅仅是一个String,而想要操作JSON里边的属性,自己写不太方便,所以就有fastjson给我们去用。这篇文章简单讲讲fastjson的使用,希望对大家有帮助。如果有帮助,给我点个赞呀!一、fastjson入门以下内容参考官网来源:https://github.com/alibaba/fastjson/wiki/Quick-Start-CN它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean说白了就是JSON和Java对象互相转换fastjson优点:速度快、使用广泛、使用简单、功能完备、测试完备(之前爆了很多漏洞,现在我司走发布流程都强制我们升级fastjson版本了),现在使用fastjson至少升级到1.2.60版本速度快的原因:1、自行编写类似StringBuilder的工具类SerializeWriter。2、使用ThreadLocal来缓存buf。3、使用asm避免反射4、集成jdk实现的一些优化算法二、使用fastjson首先我们在pom文件中引入fastjson的依赖就好了:1<dependency> 2 <groupId>com.alibaba</groupId> 3 <artifactId>fastjson</artifactId> 4 <version>x.x.x</version> 5</dependency>fastjson的使用主要是三个对象:JSONJSONObjectJSONArray 三个类JSONArray和JSONObject继承JSON:JSONArray和JSONObject继承JSON2.1 JSON对象JSON这个类主要用于转换:将Java对象序列化为JSON字符串将JSON字符串反序列化为Java对象所以,有三个方法我们用得特别多:parseObject(String text, Classclazz)parseArray(String text, Classclazz)toJSONString(Object object)2.2 JSONObjectJSON对象(JSONObject)中的数据都是以key-value形式出现,所以它实现了Map接口: 实现了Map接口使用起来也很简单,跟使用Map就没多大的区别(因为它底层实际上就是操作Map),常用的方法:getString(String key)remove(Object key) JSONObject有常用的Map方法2.3 JSONArrayJSONArray则是JSON数组,JSON数组对象中存储的是一个个JSON对象,所以类中的方法主要用于直接操作JSON对象 实现List接口最常用的方法:getJSONObject(int index)三、实战从上面的简单介绍我们已经可以知道了:JSON用于将字符串反序列化为JavaBean和JavaBean序列化为JSONJSONObject代表的是JSON对象,底层通过Map来操作,常用getString等方法来获取对应的值JSONArray代表的是JSON对象数组,底层实际上是List,它用作于操作JSON对象一般来说,我们从数据库拿到JSON数据以后,然后要对JSON进行修改。比如JSON串如下:1{ 2 "formId": "{$formId}", 3 "link": "www.java3y.com", 4 "text": [{ 5 "name": "java3y", 6 "label": "3y", 7 "value": { 8 "value": "{$tureName}", 9 "color": "", 10 "emphasis": "" 11 } 12 }, { 13 "name": "java4y", 14 "label": "3y", 15 "value": { 16 "value": "{$title}", 17 "color": "", 18 "emphasis": "" 19 } 20 }, { 21 "name": "java5y", 22 "label": "5y", 23 "value": { 24 "value": "关注我的公众号,更多干货", 25 "color": "#ff0040", 26 "emphasis": "" 27 } 28 }], 29 "yyyImg": "", 30 "yyyAge": "", 31 "pagepath": "" 32}我们是不会直接操作JSON的,我们会将JSON转成我们自己的JavaBean,再操作JavaBean,最后序列化为JSONString从上面的JSON结构上来看还是相对复杂的,思路:我们可以根据JSON的结构构建对应的JavaBean使用JSON类将JSON字符串反序列化为JavaBean修改JavaBean的值最后将JavaBean序列化为JSON字符串从上面的JSON结构,首先我们针对text这层抽象为一个JavaBean。(实际上最里层的结构是value,但我这边不需要处理value,所以就不抽象了)1/** 2 * "name": "java3y", 3 * "label": "3y", 4 * "value": { 5 * "value": "{$tureName}", 6 * "color": "", 7 * "emphasis": "" 8 * } 9 * 10 * 对Text进行抽象 11 */ 12public class TextInfo { 13 14 private String name; 15 private String label; 16 17 // 因为value我这边不需要操作,所以就不抽象了,如果每层都要处理,那就得抽象 18 private Object value; 19 20 21 // set get ... 省略 欢迎关注我的公众号:Javay 22 23}然后对外层进行抽象:1public class ContentValue { 2 private String formId; 3 private String link; 4 // 这里是一个数组,我们就抽象为List,属性名为text 5 private List<TextInfo> text; 6 7 private String yyyImg; 8 private String yyyAge; 9 private String pagepath; 10 11 // set get ... 省略 欢迎关注我的公众号:Javay 12 13}我们反序列化看一下:1public static void main(String[] args) { 2 3 // JSON 字符串 4 String s = "{\"formId\":\"{$formId}\",\"link\":\"www.java3y.com\",\"text\":[{\"name\":\"java3y\",\"label\":\"3y\",\"value\":{\"value\":\"{$tureName}\",\"color\":\"\",\"emphasis\":\"\"}},{\"name\":\"java4y\",\"label\":\"3y\",\"value\":{\"value\":\"{$title}\",\"color\":\"\",\"emphasis\":\"\"}},{\"name\":\"java5y\",\"label\":\"5y\",\"value\":{\"value\":\"关注我的公众号,更多干货\",\"color\":\"#ff0040\",\"emphasis\":\"\"}}],\"yyyImg\":\"\",\"yyyAge\":\"\",\"pagepath\":\"\"}"; 5 6 // 使用JSON对象 将JSON字符串反序列化为JavaBean 7 ContentValue contentValue = JSON.parse(s, ContentValue.class); 8 System.out.println(contentValue); 9 10 11 }反序列化结果: 反序列化我们要改text里边的值,只需要操作JavaBean就好了:1public static void main(String[] args) { 2 3 // JSON 字符串 4 String s = "{\"formId\":\"{$formId}\",\"link\":\"www.java3y.com\",\"text\":[{\"name\":\"java3y\",\"label\":\"3y\",\"value\":{\"value\":\"{$tureName}\",\"color\":\"\",\"emphasis\":\"\"}},{\"name\":\"java4y\",\"label\":\"3y\",\"value\":{\"value\":\"{$title}\",\"color\":\"\",\"emphasis\":\"\"}},{\"name\":\"java5y\",\"label\":\"5y\",\"value\":{\"value\":\"关注我的公众号,更多干货\",\"color\":\"#ff0040\",\"emphasis\":\"\"}}],\"yyyImg\":\"\",\"yyyAge\":\"\",\"pagepath\":\"\"}"; 5 6 // 使用JSON对象 将JSON字符串反序列化为JavaBean 7 ContentValue contentValue = JSON.parse(s, ContentValue.class); 8 List<TextInfo> text = contentValue.getText(); 9 for (TextInfo textInfo : text) { 10 textInfo.setName("Java3y"); 11 textInfo.setLabel("关注我的公众号呗"); 12 } 13 14 15 // 修改后,反序列化回去 16 String content = JSON.toJSONString(contentValue); 17}序列化结果: 序列化轻松将JSON字符串里边的字段改掉。最后总的来说,fastjson还是足够方便好用的,它的速度也很快,只是最近漏洞有点多。
一、本来就能实现异步非阻塞,为啥要用WebFlux?相信有过相关了解的同学都知道,Servlet 3.1就已经支持异步非阻塞了。我们可以以自维护线程池的方式实现异步说白了就是Tomcat的线程处理请求,然后把这个请求分发到自维护的线程处理,Tomcat的请求线程返回@WebServlet(value = "/nonBlockingThreadPoolAsync", asyncSupported = true) public class NonBlockingAsyncHelloServlet extends HttpServlet { private static ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100)); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext asyncContext = request.startAsync(); ServletInputStream inputStream = request.getInputStream(); inputStream.setReadListener(new ReadListener() { @Override public void onDataAvailable() throws IOException { } @Override public void onAllDataRead() throws IOException { executor.execute(() -> { new LongRunningProcess().run(); try { asyncContext.getResponse().getWriter().write("Hello World!"); } catch (IOException e) { e.printStackTrace(); } asyncContext.complete(); }); } @Override public void onError(Throwable t) { asyncContext.complete(); } }); } }流程图如下:上面的例子来源:https://www.cnblogs.com/davenkin/p/async-servlet.html简单的方式,我们还可以使用JDK 8 提供的CompletableFuture类,这个类可以方便的处理异步调用。protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long t1 = System.currentTimeMillis(); // 开启异步 AsyncContext asyncContext = request.startAsync(); // 执行业务代码(doSomething 指的是处理耗费时间长的方法) CompletableFuture.runAsync(() -> doSomeThing(asyncContext, asyncContext.getRequest(), asyncContext.getResponse())); System.out.println("async use:" + (System.currentTimeMillis() - t1)); }要处理复杂的逻辑时,无论是回调或 CompletableFuture在代码编写上都会比较复杂(代码量大,不易于看懂),而WebFlux使用的是Reactor响应式流,里边提供了一系列的API供我们去处理逻辑,就很方便了。更重要的是:WebFlux使用起来可以像使用SpringMVC一样,能够大大减小学习成本WebFlux也可以使用Functional Endpoints方式编程,总的来说还是要比回调/CompletableFuture要简洁和易编写。值得一提的是:如果Web容器使用的是Tomcat,那么就是使用Reactor桥接的servlet async api如果Web容器是Netty,那么就是使用的Netty,天生支持Reactive官方的推荐是使用Netty跑WebFlux二、WebFlux性能的问题我们从上篇文章中就发现,浏览器去调用处理慢的接口,无论是该接口是同步的,还是说是异步的,返回到浏览器的时间都是一致的。同步:服务器接收到请求,一个线程会处理请求,直到该请求处理完成,返回给浏览器异步:服务器接收到请求,一个线程会处理请求,然后指派别的线程处理请求,请求的线程直接空闲出来。官网也说了:Reactive and non-blocking generally do not make applications run faster使用异步非阻塞的好处就是:The key expected benefit of reactive and non-blocking is the ability to scale with a small, fixed number of threads and less memory.That makes applications more resilient under load, because they scale in a more predictable way好处:只需要在程序内启动少量线程扩展,而不是水平通过集群扩展。异步能够规避文件IO/网络IO阻塞所带来的线程堆积。下面来看一下针对相同的请求量,同步阻塞和异步非阻塞的吞吐量和响应时长对比:注:请求量不大时(3000左右),同步阻塞多线程处理请求,吞吐量和响应时长都没落后。(按道理WebFlux可能还要落后一些,毕竟多做了一步处理--&gt;将请求委派给另一个线程去做处理请求量大时,线程数不够用,同步阻塞(MVC)只能等待,所以吞吐量要下降,响应时长要提高(排队)。Spring WebFlux在应对高并发的请求时,借助于异步IO,能够以少量而稳定的线程处理更高吞吐量的请求,尤其是当请求处理过程如果因为业务复杂或IO阻塞等导致处理时长较长时,对比更加显著。三、WebFlux实际应用WebFlux需要非阻塞的业务代码,如果阻塞,需要自己开线程池去运行。WebFlux什么场景下可以替换SpringMVC呢?想要内存和线程数较少的场景网络较慢或者IO会经常出现问题的场景SpringMVC和WebFlux更多的是互补关系,而不是替换。阻塞的场景该SpringMVC还是SpringMVC,并不是WebFlux出来就把SpringMVC取代了。如果想要发挥出WebFlux的性能,需要从Dao到Service,全部都要是Mono和Flux,目前官方的数据层Reactive框架只支持Redis,Mongo等几个,没有JDBC。目前对于关系型数据库,Pivotal团队开源出R2DBC(Reactive Relational Database Connectivity),其GitHub地址为:https://github.com/r2dbc目前R2DBC支持三种数据源:PostgreSQLH2Microsoft SQL Server总的来说,因为WebFlux是响应式的,要想发挥出WebFlux的性能就得将代码全改成响应式的,而JDBC目前是没支持的(至少MySQL还没支持),而响应式的程序不好调试和编写(相对于同步的程序),所以现在WebFlux的应用场景还是相对较少的。所以,我认为在网关层用WebFlux比较合适(本来就是网络IO较多的场景)现在再回来看Spring官网的图,是不是就更亲切了?参考资料:https://blog.lovezhy.cc/2018/12/29/webflux性能问题四、有必要学Functional Endpoints 编程模式吗?前面也提到了,WebFlux提供了两种模式供我们使用,一种是SpringMVC 注解的,一种是叫Functional Endpoints的Lambda-based, lightweight, and functional programming model总的来看,就是配合Lambda和流式编程去使用WebFlux。如果你问我:有必要学吗?其实我觉得可以先放着。我认为现在WebFlux的应用场景还是比较少,等真正用到的时候再学也不是什么难事,反正就是学些API嘛~有Lambda表达式和Stream流的基础,等真正用到的时候再学也不是啥问题~以下是通过注解的方式来使用WebFlux的示例:以下是通过Functional Endpoints的方式来使用WebFlux的示例:路由分发器,相当于注解的GetMapping…UserHandler,相当于UserController:
而背压说白了就是:消费者能告诉生产者自己需要多少量的数据。这里就是Subscription接口所做的事。下面我们来看看JDK9接口的方法,或许就更加能理解上面所说的话了:// 发布者(生产者) public interface Publisher<T> { public void subscribe(Subscriber<? super T> s); } // 订阅者(消费者) public interface Subscriber<T> { public void onSubscribe(Subscription s); public void onNext(T t); public void onError(Throwable t); public void onComplete(); } // 用于发布者与订阅者之间的通信(实现背压:订阅者能够告诉生产者需要多少数据) public interface Subscription { public void request(long n); public void cancel(); } // 用于处理发布者 发布消息后,对消息进行处理,再交由消费者消费 public interface Processor<T,R> extends Subscriber<T>, Publisher<R> { }3.1 看个例子代码中有大量的注释,我就不多BB了,建议直接复制跑一下看看:class MyProcessor extends SubmissionPublisher<String> implements Processor<Integer, String> { private Subscription subscription; @Override public void onSubscribe(Subscription subscription) { // 保存订阅关系, 需要用它来给发布者响应 this.subscription = subscription; // 请求一个数据 this.subscription.request(1); } @Override public void onNext(Integer item) { // 接受到一个数据, 处理 System.out.println("处理器接受到数据: " + item); // 过滤掉小于0的, 然后发布出去 if (item > 0) { this.submit("转换后的数据:" + item); } // 处理完调用request再请求一个数据 this.subscription.request(1); // 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了 // this.subscription.cancel(); } @Override public void onError(Throwable throwable) { // 出现了异常(例如处理数据的时候产生了异常) throwable.printStackTrace(); // 我们可以告诉发布者, 后面不接受数据了 this.subscription.cancel(); } @Override public void onComplete() { // 全部数据处理完了(发布者关闭了) System.out.println("处理器处理完了!"); // 关闭发布者 this.close(); } } public class FlowDemo2 { public static void main(String[] args) throws Exception { // 1. 定义发布者, 发布的数据类型是 Integer // 直接使用jdk自带的SubmissionPublisher SubmissionPublisher<Integer> publiser = new SubmissionPublisher<Integer>(); // 2. 定义处理器, 对数据进行过滤, 并转换为String类型 MyProcessor processor = new MyProcessor(); // 3. 发布者 和 处理器 建立订阅关系 publiser.subscribe(processor); // 4. 定义最终订阅者, 消费 String 类型数据 Subscriber<String> subscriber = new Subscriber<String>() { private Subscription subscription; @Override public void onSubscribe(Subscription subscription) { // 保存订阅关系, 需要用它来给发布者响应 this.subscription = subscription; // 请求一个数据 this.subscription.request(1); } @Override public void onNext(String item) { // 接受到一个数据, 处理 System.out.println("接受到数据: " + item); // 处理完调用request再请求一个数据 this.subscription.request(1); // 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了 // this.subscription.cancel(); } @Override public void onError(Throwable throwable) { // 出现了异常(例如处理数据的时候产生了异常) throwable.printStackTrace(); // 我们可以告诉发布者, 后面不接受数据了 this.subscription.cancel(); } @Override public void onComplete() { // 全部数据处理完了(发布者关闭了) System.out.println("处理完了!"); } }; // 5. 处理器 和 最终订阅者 建立订阅关系 processor.subscribe(subscriber); // 6. 生产数据, 并发布 publiser.submit(-111); publiser.submit(111); // 7. 结束后 关闭发布者 // 正式环境 应该放 finally 或者使用 try-resouce 确保关闭 publiser.close(); // 主线程延迟停止, 否则数据没有消费就退出 Thread.currentThread().join(1000); } }输出的结果如下:流程实际上非常简单的:参考资料:https://yanbin.blog/java-9-talk-reactive-stream/#more-8877https://blog.csdn.net/wudaoshihun/article/details/83070086http://www.spring4all.com/article/6826https://www.cnblogs.com/IcanFixIt/p/7245377.htmlJava 8 的 Stream 主要关注在流的过滤,映射,合并,而 Reactive Stream 更进一层,侧重的是流的产生与消费,即流在生产与消费者之间的协调说白了就是:响应式流是异步非阻塞+流量控制的(可以告诉生产者自己需要多少的量/取消订阅关系)展望响应式编程的场景应用:比如一个日志监控系统,我们的前端页面将不再需要通过“命令式”的轮询的方式不断向服务器请求数据然后进行更新,而是在建立好通道之后,数据流从系统源源不断流向页面,从而展现实时的指标变化曲线;再比如一个社交平台,朋友的动态、点赞和留言不是手动刷出来的,而是当后台数据变化的时候自动体现到界面上的。四、入门WebFlux扯了一大堆,终于回到WebFlux了。经过上面的基础,我们现在已经能够得出一些结论的了:WebFlux是Spring推出响应式编程的一部分(web端)响应式编程是异步非阻塞的(是一种基于数据流(data stream)和变化传递(propagation of change)的声明式(declarative)的编程范式)我们再回来看官网的图:4.1 简单体验WebFluxSpring官方为了让我们更加快速/平滑到WebFlux上,之前SpringMVC那套都是支持的。也就是说:我们可以像使用SpringMVC一样使用着WebFlux。WebFlux使用的响应式流并不是用JDK9平台的,而是一个叫做Reactor响应式流库。所以,入门WebFlux其实更多是了解怎么使用Reactor的API,下面我们来看看~Reactor是一个响应式流,它也有对应的发布者(Publisher ),Reactor的发布者用两个类来表示:Mono(返回0或1个元素)Flux(返回0-n个元素)而消费者则是Spring框架帮我们去完成下面我们来看一个简单的例子(基于WebFlux环境构建):// 阻塞5秒钟 private String createStr() { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { } return "some string"; } // 普通的SpringMVC方法 @GetMapping("/1") private String get1() { log.info("get1 start"); String result = createStr(); log.info("get1 end."); return result; } // WebFlux(返回的是Mono) @GetMapping("/2") private Mono<String> get2() { log.info("get2 start"); Mono<String> result = Mono.fromSupplier(() -> createStr()); log.info("get2 end."); return result; }首先,值得说明的是,我们构建WebFlux环境启动时,应用服务器默认是Netty的:我们分别来访问一下SpringMVC的接口和WebFlux的接口,看一下有什么区别:SpringMVC:WebFlux:从调用者(浏览器)的角度而言,是感知不到有什么变化的,因为都是得等待5s才返回数据。但是,从服务端的日志我们可以看出,WebFlux是直接返回Mono对象的(而不是像SpringMVC一直同步阻塞5s,线程才返回)。这正是WebFlux的好处:能够以固定的线程来处理高并发(充分发挥机器的性能)。WebFlux还支持服务器推送(SSE - >Server Send Event),我们来看个例子:/** * Flux : 返回0-n个元素 * 注:需要指定MediaType * @return */ @GetMapping(value = "/3", produces = MediaType.TEXT_EVENT_STREAM_VALUE) private Flux<String> flux() { Flux<String> result = Flux .fromStream(IntStream.range(1, 5).mapToObj(i -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } return "flux data--" + i; })); return result; }效果就是每秒会给浏览器推送数据:WebFlux我还没写完,这篇写了WebFlux支持SpringMVC那套注解来开发,下篇写写如何使用WebFlux另一种模式(Functional Endpoints)来开发以及一些常见的问题还需要补充一下~
一、什么是WebFlux?我们从Spring的官网拉下一点点就可以看到介绍WebFlux的地方了从官网的简介中我们能得出什么样的信息?我们程序员往往根据不同的应用场景选择不同的技术,有的场景适合用于同步阻塞的,有的场景适合用于异步非阻塞的。而Spring5提供了一整套响应式(非阻塞)的技术栈供我们使用(包括Web控制器、权限控制、数据访问层等等)。而左侧的图则是技术栈的对比啦;响应式一般用Netty或者Servlet 3.1的容器(因为支持异步非阻塞),而Servlet技术栈用的是Servlet容器在Web端,响应式用的是WebFlux,Servlet用的是SpringMVC…..总结起来,WebFlux只是响应式编程中的一部分(在Web控制端),所以一般我们用它与SpringMVC来对比。二、如何理解响应式编程?在上面提到了响应式编程(Reactive Programming),而WebFlux只是响应式编程的其中一个技术栈而已,所以我们先来探讨一下什么是响应式编程从维基百科里边我们得到的定义:reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change响应式编程(reactive programming)是一种基于数据流(data stream)和变化传递(propagation of change)的声明式(declarative)的编程范式在维基百科上也举了个小例子:意思大概如下:在命令式编程(我们的日常编程模式)下,式子a=b+c,这就意味着a的值是由b和c计算出来的。如果b或者c后续有变化,不会影响到a的值在响应式编程下,式子a:=b+c,这就意味着a的值是由b和c计算出来的。但如果b或者c的值后续有变化,会影响到a的值我认为上面的例子已经可以帮助我们理解变化传递(propagation of change)那数据流(data stream)和声明式(declarative)怎么理解呢?那可以提一提我们的Stream流了。之前写过Lambda表达式和Stream流的文章,大家可以先去看看:最近学到的Lambda表达式基础知识手把手带你体验Stream流Lambda的语法是这样的(Stream流的使用会涉及到很多Lambda表达式的东西,所以一般先学Lambda再学Stream流):Stream流的使用分为三个步骤(创建Stream流、执行中间操作、执行最终操作):执行中间操作实际上就是给我们提供了很多的API去操作Stream流中的数据(求和/去重/过滤)等等说了这么多,怎么理解数据流和声明式呢?其实是这样的:本来数据是我们自行处理的,后来我们把要处理的数据抽象出来(变成了数据流),然后通过API去处理数据流中的数据(是声明式的)比如下面的代码;将数组中的数据变成数据流,通过显式声明调用.sum()来处理数据流中的数据,得到最终的结果:public static void main(String[] args) { int[] nums = { 1, 2, 3 }; int sum2 = IntStream.of(nums).parallel().sum(); System.out.println("结果为:" + sum2); }如图下所示:2.1 响应式编程->异步非阻塞上面讲了响应式编程是什么:响应式编程(reactive programming)是一种基于数据流(data stream)和变化传递(propagation of change)的声明式(declarative)的编程范式也讲解了数据流/变化传递/声明式是什么意思,但说到响应式编程就离不开异步非阻塞。从Spring官网介绍WebFlux的信息我们就可以发现asynchronous, nonblocking 这样的字样,因为响应式编程它是异步的,也可以理解成变化传递它是异步执行的。如下图,合计的金额会受其他的金额影响(更新的过程是异步的):我们的JDK8 Stream流是同步的,它就不适合用于响应式编程(但基础的用法是需要懂的,因为响应式流编程都是操作流嘛)而在JDK9 已经支持响应式流了,下面我们来看一下三、JDK9 Reactive响应式流的规范早已经被提出了:里面提到了:Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure ----->http://www.reactive-streams.org/翻译再加点信息:响应式流(Reactive Streams)通过定义一组实体,接口和互操作方法,给出了实现异步非阻塞背压的标准。第三方遵循这个标准来实现具体的解决方案,常见的有Reactor,RxJava,Akka Streams,Ratpack等。规范里头实际上就是定义了四个接口:Java 平台直到 JDK 9才提供了对于Reactive的完整支持,JDK9也定义了上述提到的四个接口,在java.util.concurrent包上一个通用的流处理架构一般会是这样的(生产者产生数据,对数据进行中间处理,消费者拿到数据消费):数据来源,一般称为生产者(Producer)数据的目的地,一般称为消费者(Consumer)在处理时,对数据执行某些操作一个或多个处理阶段。(Processor)到这里我们再看回响应式流的接口,我们应该就能懂了:Publisher(发布者)相当于生产者(Producer)Subscriber(订阅者)相当于消费者(Consumer)Processor就是在发布者与订阅者之间处理数据用的在响应式流上提到了back pressure(背压)这么一个概念,其实非常好理解。在响应式流实现异步非阻塞是基于生产者和消费者模式的,而生产者消费者很容易出现的一个问题就是:生产者生产数据多了,就把消费者给压垮了。
一、ABTest的介绍比如我写了一篇关于ABTest的文章,我希望这篇文章的阅读量能上2500,但是我没想好标题叫什么比较合适。一条推文的标题非常能影响到阅读量,于是我想了几个的标题:最近我学到的AbTest知识AbTest入门而我不知道哪个标题效果会更好一些,于是我做了这么一个尝试:《最近我学到的AbTest知识》这个标题推送给10%的用户《AbTest入门》这个标题推送给10%的用户过一段时间后,我看一下效果,哪个标题的阅读量更高,我就将效果高的标题推送给剩余80%的用户要注意的是:在推送的文章的时候,除了标题不同,其他因素都需要相同(不能被别的因素给干扰),这样看数据的时候才有说服力。1.1为什么要做ABTest?做ABTest的原因其实很简单,我们在做业务的时候会有各种各样的想法,比如说:“我觉得在文案上加入emoji表情,这个推送的消息的点击率肯定高”“我觉得这个按钮/图片换成别的颜色,转化率肯定会提高”“我觉得首页就应该设计成这样,还有图墙应该是这样这样..“…..但是,并不是所有的想法都是正确的,很可能因为你的想法把首页的样式改掉,用户不喜欢,就影响到了GMV等等等….一个好的产品都是迭代出来的,而我们很可能不清楚这次的迭代最终是好是坏(至少我们是觉得迭代对用户是好的,是有帮助的,对公司的转化也是好的),但是我们的用户未必就买账。于是,为了降低试错成本,我们就做ABTest。一个功能做出来,我们只放小流量看下效果,如果效果比原来的功能差,那很可能我们这个想法没有达到预期。如果小流量效果比预期要好,再逐步加大流量,直至全量。二、怎么做ABTest?从上面的案例,其实我们大概知道,ABTest最主要做的就是一个分流的事将10%流量分给用户群体A将10%流量分给用户群体A我们需要保证的是:一个用户再次请求进来,用户看到的结果是一样的比如说,我访问了Java3y,他的简介是:“一个坚持原创的Java技术公众号“。而一个小时后,我再访问了他一次,他的简介是:“一个干货满满的技术号“。而一个小时过后,我又访问了他一次,他的简介是:“一个坚持原创的Java技术公众号“。这是不合理的,理应上用户在一段时间内,看到的内容是相同的,不然就给用户带来一种错乱感。OK,于是一般可以这样做:对用户ID(设备ID/CookieId/userId/openId)取hash值,每次Hash的结果都是相同的。直接取用户ID的某一位现在看起来,ABTest好像就是一个分流的东西,只是取了个高大尚的名字叫做ABTest。2.1 ABTest更多的内容假如我做了一个UI层面上的ABTest,占用全站的流量80%,现在我还想做搜索结果的ABTest怎么办?只能用剩下的20%了?那我的流量不够用啊(我可能要做各种实验的呢)。UI层面上的ABTest和搜索结果的ABTest能不能同时进行啊?答案是可以的。因为UI层面和搜索结果(算法优化)的业务关联性是很低的。如果要做“同一份流量同时做UI层面上和搜索结果的ABTest”,那要保证“在UI层面做的ABTest不能影响到搜索结果的ABTest”业界应用最多的,是可重叠分层分桶方法层与层之间的流量互不干扰,这就是很多文章所讲的正交(流量在每一层都会被重新打散)来源:https://www.infoq.cn/article/BuP18dsaPyAg-hflDxPf我们就可以这样干:通过 Hash(userId, LayerId) % 1000类似的办法来实现每一层的实验不管有多少个,对其他层的影响都是均匀的我的理解:为了实现UI/算法/广告 这些业务上没什么关联的,能够使用同一份流量做ABTest测试,所以分了层。流量经过每一层都需要将流量重新打散(正交)----每层实验后,不会影响到下一层的实验如果业务关联强的应该放在同一层,同一层多个实验是互斥的(比如 一个按钮颜色改为绿色作为一个实验,一个按钮的样式改成大拇指作为一个实验。这两个实验的流量是要互斥的(不然你咋知道用户是因为你的按钮颜色还是样式而点击)最后一个完整的ABTest系统,不单单只做分流,还会给用户(我们程序员)提供一个方便可配置的后台系统,做完实验提供数据报表展示等等等~
一、先说背景某一天,小王跟我反馈:“麻烦检查一下线上邮件的发送情况,我这查出来发送失败啦”我去DB查了一下近期的邮件发送情况,表示:“看着都挺正常的,线上没有异常的情况。可能邮件在redis里边堆积了,还没消费”select * from email order by id desc limit 100先来说一下我这边发邮件的大致实现方式:这样做有什么好处?把Redis当做一个消息队列,把请求全部扔到Redis上,这能削峰。机器A/B/C的线程会在一定的间隔内向Redis拉取消息,然后调用邮件接口进行发送。而我这边会在页面上提供一个功能给业务方查询各类消息是否发送成功,由于发送邮件是一个异步的操作,而前同事在编写的时候又追求实时性。目前的逻辑是:如果push到Redis是成功的,并且Redis里边没有堆积着消息(说明机器A/B/C能及时处理掉这封邮件),那就认为这封邮件发送成功。PS:(如果系统不存在问题,其实这个实现也是OK的。因为邮件的发送量一般不会太大(Redis不会堆积消息),并且发送邮件的成功率也是挺高的。回到问题上,由于有上面的一个背景,所以我就猜测:是不是小王在查结果时,这封邮件还堆积在Redis上,所以就直接返回失败了。果不其然,我去查了一下Redis,还有200封邮件没消息。于是我就问小王:“你这发了多少封邮件啊?”小王表示:“20分钟500封,1qps都不到”。我想了一下:“那我们这有四台机器,按道理是不会堆积那么多的”。于是跑去线上服务器看一下消费的日志,发现只有一台机器在消费Redis的数据。又去看了一下错误的日志是不是有大量的错误信息,但并没找到错误的日志…于是去查了一下机器的监控信息,也没发现异样。那问题就来了:为啥就只有一台机器在消费Redis的消息呢?其他三台机器的日志和监控信息都没异常。二、解决从日志和机器的信息都判断不出有什么问题,这时我又想起在Java中的一个命令:jstackjstack命令主要用来查看Java线程的调用堆栈的,可以用来分析线程问题(如死锁)。jstack详细用法以及教程:https://www.cnblogs.com/kongzhongqijing/articles/3630264.html于是我就去执行了一下jstack命令,在信息中搜了一下"Email",真被我搜出来了:那就好办了,只要搜一下:“Java 发送邮箱 线程 阻塞”此类的关键字,应该就有解决方案了。最后,发现是因为在发送邮件的时候没有配置超时时间,导致某些线程在发送邮件的时候阻塞掉了(具体原因不明)mail.smtp.connectiontimeout:连接时间限制,单位毫秒。是关于与邮件服务器建立连接的时间长短的。默认是无限制。mail.smtp.timeout:邮件接收时间限制,单位毫秒。这个是有关邮件接收时间长短。默认是无限制。mail.smtp.writetimeout:邮件发送时间限制,单位毫秒。有关发送邮件时内容上传的时间长短。默认同样是无限制。
一、交代背景我一直都知道我现在的这个系统是前后端分离的,我的接口只会返回JSON出去,但我不曾关心前端是怎么处理我的JSON数据的(以及他是怎么跑通和运行的)在某一天,我在查接口的时候,习惯F12,想直接看一下这个请求返回的JSON数据是什么。但是一看,在network返回的是html格式: 请求的信息于是,我就很好奇啊,就看一下这个接口是不是我想象中的那个。于是就去找我的接口,看一下是不是真的返回JSON(我还专门Debug了一下,看看是不是真请求到这个接口上了): 接口信息得出的结果是:我的接口的确是返回JSON数据,浏览器的reponse返回的的确是HTML格式。于是,我就去找我前端的小伙伴,去问了一下这是怎么搞的。他回复我说:“在浏览器看到返回的是页面,那肯定是你们后端干的呀”我说:“没有啊,我Java接口返回的是JSON数据啊,是不是中途你们用node做了些处理啊?”(我之前听过Node.js,但仅仅是听过)他说:“Node.js也是你们后端的啊。”我一听,啊?Node.js不是属于前端的吗?二、初识Node.js在遇到这个事情之前,其实我在知乎已经看了一个帖子,话题名是这个《毕设答辩,老师说node不可能写后台怎么办?》有兴趣的小伙伴可以去了解一下,大多数内容还是挺通俗易懂的:https://www.zhihu.com/question/327657434/answer/704249816我在下载Node.js的时候,发现其简介十分简洁:Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.Node.js® 是一个基于 Chrome V8 引擎 的 JavaScript 运行时。然后点进去Chrome V8引擎,再看了一下介绍:V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. It is used in Chrome and in Node.js, among others.V8是Google的开源高性能JavaScript和WebAssembly引擎,用C ++编写。它用于Chrome和Node.js等。看了介绍,一脸懵逼,这是啥玩意啊。下面我来解释一下2.1 V8引擎是什么?众所周知,JavaScript是解析型语言,我们写好的JavaScript代码会由JavaScript引擎去解析,而V8是JavaScript引擎的一种。在传统意义上,我们会认为解析器是逐条解析(一边执行一边解析),但为了提高JavaScript的解析速度(相当于提高用户体验),在解析的时候做了点“手脚”。V8引擎:为了提高解析的性能,引入了一些“后端”的技术(不过他本来就由C++编写的)。它是先将JavaScript源代码转成抽象语法树,然后再将抽象语法树生成字节码。如果发现某个函数被多次调用或者是多次调用的循环体(热点代码),那就会将这部分的代码编译优化。说白了就是:对热点代码做编译,非热点代码直接解析。 示意图总结:V8引擎是JavaScript引擎的一种,这个引擎由C++来编写的,性能很不错。参考资料:https://zhuanlan.zhihu.com/p/27628685https://zhuanlan.zhihu.com/p/737683382.2回到Node.js浏览器为了安全,没有为JavaScript提供一套IO环境,而一门后端语言是肯定能进行网络通信、文件读写(IO)的。后来,有牛逼的人把V8引擎搬到了服务端上,在V8引擎的基础上加了网络通信、IO、HTTP等服务端的函数。取了一个名字叫:Node.js比如通过libuv库来进行文件读取,以及建立TCP/UDP连接。通过xxx库解析HTTP请求和响应….这些库都是由C/C++来编写的。 搬运所以,Node.js是运行在服务端的,只不过在基础语言是JavaScript。三、前后端分离入门回顾一下自己学JavaWeb的历程:刚学Servlet的时候,会在response对象上写一些HTML代码输出到浏览器看效果后来,学习到JSP了,就纯粹用Servlet做控制,JSP做视图。JSP本质上还是一个Servlet,只不过看起来像HTML文件,在编译的时候还是会变成一个HttpJspPage类(该类是HttpServlet的一个子类)再后来,学到了AJAX技术,发现我们完全可以通过AJAX来进行交互。AJAX请求Servlet,Servlet返回JSON数据回去,AJAX拿到Servlet返回的数据进行解析和处理。这里压根就不需要JSP了(纯HTML+AJAX),这算是前后端分离的一种了在开发上体验:如果完全使用HTML+AJAX的话,会发现其实需要写非常非常多的JavaScript代码,而且这些JavaScript代码都不好复用。在部署上,还是跟Java一起部署(放在resource下),没有将前端单独部署。再后来,学到了一些在常用的模板引擎(比如freemarker),其实用起来跟JSP没多大的区别,只不过性能要比JSP好不少。…流下不学无术的泪水目前我了解到的前后端分离,首先部署是分离的(至少不会跟Java绑定在一起部署): 前端和Java部署机器分离Java接口只返回JSON数据: Java接口都只返回JSON格式的数据关于前端这几大框架:angular/vue/react这几个我都是没有写过的,所以也就不多BB了。我一直想知道的是:前框框架和node是啥关系。问了一下前端的小伙伴,他回复是大致这样的:前端现在是讲究工程化的,工程化用到了node而已(就是打包编译那些会用到,项目里面真正跑起来的话是没有这些东西的)----以下文字摘录Webpack、Less、Sass、Gulp、Bower以及这些工具的插件都是Node上开发的---@知乎 陈龙举个例子:随着发展,前端的JavaScript需要依赖的包也非常复杂,类比于Java我们会有Maven,而前端现在有npm (包管理)而npm是随同Node.js一起安装的。所以前端(vue/angular/react)在开发环境下都是离不开Node.js的(编译、打包等等)参考资料(为什么要使用 npm):https://zhuanlan.zhihu.com/p/243577703.1 方式一(Nginx+Server)OK,现在假设我们用前端(vue/angular/react)开发完,开发环境下将JavaScript编译/打包完,那我们能得到纯静态的文件。我们可以直接将纯静态文件放到Nginx(CDN)等等地方【只要能够响应HTTP请求就行】。如果请求是调用后端服务,则经过Nginx转发到后端服务器,完成响应后经Nginx返回到浏览器。3.2 方式二(加入Node.js)在前边的基础上加入Node.js,至于为啥要Node.js,一个重要的原因就是:加快首屏渲染速度,解决SEO问题加入Node.js,此时的请求流程应该是这样的:浏览器发起的请求经过前端机的Nginx进行分发.URL请求统一分发到Node Server,在Node Server中根据请求类型从后端服务器上通过RPC服务请求页面的模板数据,然后进行页面的组装和渲染;API请求则直接转发到后端服务器,完成响应。最后好的,现在问题来了:你是觉得Node.js归属在后端还是前端?
区别在视频练的项目大多数都是由讲师在本地编码来讲解整一个项目的开发过程,而我们去到公司做的第一件事是啥?把项目clone(checkout)到本地来看。这就有两个区别:去到公司往往不是从零开发一个项目,项目的框架和代码都已经写好了。我们更多要做的就是迭代这个项目(或者说修复这个项目原本就有的Bug)一个项目往往都会有几个人进行开发,这就肯定要用到版本管理工具(SVN/Git)。所以,去公司做项目之前最好是提前去学学Git/SVN这些工具的使用比如说,我们使用Git的时候,要修改代码的时候会新建一个分支,改完了再合并到master分支上。好了,现在项目已经在本地上了,那我们要将项目在本地上启动起来吗?这得问同事。在公司做项目,一般都分了好几个环境线上(现在正在给用户用的)测试(写完功能,先看一下在测试环境下有没有问题,没问题才发布到线上环境)…等等(可能名字叫起来不一样,但不可能在本地上写完的代码直接就放到线上去跑了)有的时候,可能环境过于复杂(各种系统相互依赖),clone(checkout)下来的项目就很难在本地上启动起来,或者说在本地上的数据跟线上的数据差距太大了(比较难看出效果)。所以,有的时候可能就不用在本地将项目启动起来。那问题来了,我写完的代码怎么调试啊?我们可以这样干的:将本地写好的代码push到测试环境,然后本地远程连接测试环境,对其进行调试。还有,我们自己做项目的时候,try-catch完了之后,习惯可能就e.printStackTrace(); 万能的e.printxxxx但公司的项目不会有e.printStackTrace();这种代码的存在。因为这打印出来的错误信息没有日期、等级等等,分析起来不方便。 分析起来不方便在公司一般将错误的信息(或者有用的信息)写到log(日志)中。比如说:LOG.error("send java3y article failed, {}", e);于是,一般出现了问题,我们先去登上机器,查一下日志的信息是怎么样的。而登上线上的机器上,也不是直连的,会经过一层堡垒机。堡垒机是做啥的?来源:每次登录线上的机器都要ssh xxxxip 到堡垒机上,然后再到堡垒机上输入线上机器IP连接,着实麻烦。于是大佬们就会写自动登录堡垒机,直接输入IP到线上的脚本于是乎,我们就登录到堡垒机上,然后再连上线上机器就去查看日志了。查看日志怎么看?直接cat console.log吗,那得找到什么时候啊?vim console.log吗,我想根据某个关键字来查怎么在vim上操作啊?如果log文件太大了,vim打开太卡了怎么搞啊…所以,常用inux命令还是得学学的呀https://www.cnblogs.com/xiashan17/p/7059978.htmlhttps://blog.csdn.net/qq_31617637/article/details/71426904还有一点就是,在公司开发都要申请权限才能对数据库/机器/各种资源进行操作。而不像我们个人开发各种root权限,数据库各种DDL/DML随便玩。DML/DDL数据库操作都要申请权限,发布代码到线上也要申请权限,想要去线上的机器上查看日志也要权限…..最后 版本控制工具远程连接Debug调试不再使用e.printStackTrace();,而是log.error()来替代各种权限都需要申请和审批至于代码量来说的话,一般是公司的代码量比我们在学校做的项目要多得多的。技术的话就得看具体的公司了~
在我实习之前我就已经在看单点登录的是什么了,但是实习的时候一直在忙其他的事,所以有几个网站就一直躺在我的收藏夹里边:在前阵子有个读者来我这投稿,是使用JWT实现单点登录的(但是文章中并没有介绍什么是单点登录),所以我觉得是时候来整理一下了。简单代码实现JWT(json web token)完成SSO单点登录一、什么是单点登录?单点登录的英文名叫做:Single Sign On(简称SSO)。在初学/以前的时候,一般我们就单系统,所有的功能都在同一个系统上。后来,我们为了合理利用资源和降低耦合性,于是把单系统拆分成多个子系统。回顾:分布式基础知识比如阿里系的淘宝和天猫,很明显地我们可以知道这是两个系统,但是你在使用的时候,登录了天猫,淘宝也会自动登录。简单来说,单点登录就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。二、回顾单系统登录在我初学JavaWeb的时候,登录和注册是我做得最多的一个功能了(初学Servlet的时候做过、学SpringMVC的时候做过、跟着做项目的时候做过…),反正我也数不清我做了多少次登录和注册的功能了…这里简单讲述一下我们初学时是怎么做登录功能的。众所周知,HTTP是无状态的协议,这意味着服务器无法确认用户的信息。于是乎,W3C就提出了:给每一个用户都发一个通行证,无论谁访问的时候都需要携带通行证,这样服务器就可以从通行证上确认用户的信息。通行证就是Cookie。如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户明细表”。HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一个用户。于是乎:服务器向用户浏览器发送了一个名为JESSIONID的Cookie,它的值是Session的id值。其实Session是依据Cookie来识别是否是同一个用户。所以,一般我们单系统实现登录会这样做:登录:将用户信息保存在Session对象中如果在Session对象中能查到,说明已经登录如果在Session对象中查不到,说明没登录(或者已经退出了登录)注销(退出登录):从Session中删除用户的信息记住我(关闭掉浏览器后,重新打开浏览器还能保持登录状态):配合Cookie来用我之前Demo的代码,可以参考一下:/** * 用户登陆 */ @PostMapping(value = "/user/session", produces = {"application/json;charset=UTF-8"}) public Result login(String mobileNo, String password, String inputCaptcha, HttpSession session, HttpServletResponse response) { //判断验证码是否正确 if (WebUtils.validateCaptcha(inputCaptcha, "captcha", session)) { //判断有没有该用户 User user = userService.userLogin(mobileNo, password); if (user != null) { /*设置自动登陆,一个星期. 将token保存在数据库中*/ String loginToken = WebUtils.md5(new Date().toString() + session.getId()); user.setLoginToken(loginToken); User user1 = userService.userUpload(user); session.setAttribute("user", user1); CookieUtil.addCookie(response,"loginToken",loginToken,604800); return ResultUtil.success(user1); } else { return ResultUtil.error(ResultEnum.LOGIN_ERROR); } } else { return ResultUtil.error(ResultEnum.CAPTCHA_ERROR); } } /** * 用户退出 */ @DeleteMapping(value = "/session", produces = {"application/json;charset=UTF-8"}) public Result logout(HttpSession session,HttpServletRequest request,HttpServletResponse response ) { //删除session和cookie session.removeAttribute("user"); CookieUtil.clearCookie(request, response, "loginToken"); return ResultUtil.success(); } /** * @author ozc * @version 1.0 * <p> * 拦截器;实现自动登陆功能 */ public class UserInterceptor implements HandlerInterceptor { @Autowired private UserService userService; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception { User sessionUser = (User) request.getSession().getAttribute("user"); // 已经登陆了,放行 if (sessionUser != null) { return true; } else { //得到带过来cookie是否存在 String loginToken = CookieUtil.findCookieByName(request, "loginToken"); if (StringUtils.isNotBlank(loginToken)) { //到数据库查询有没有该Cookie User user = userService.findUserByLoginToken(loginToken); if (user != null) { request.getSession().setAttribute("user", user); return true; } else { //没有该Cookie与之对应的用户(Cookie不匹配) CookieUtil.clearCookie(request, response, "loginToken"); return false; } } else { //没有cookie、也没有登陆。是index请求获取用户信息,可以放行 if (request.getRequestURI().contains("session")) { return true; } //没有cookie凭证 response.sendRedirect("/login.html"); return false; } } } }总结一下上面代码的思路:用户登录时,验证用户的账户和密码生成一个Token保存在数据库中,将Token写到Cookie中将用户数据保存在Session中请求时都会带上Cookie,检查有没有登录,如果已经登录则放行如果没看懂的同学,建议回顾Session和Cookie和HTTP:介绍会话技术、Cookie的API、详解、应用Session介绍、API、生命周期、应用、与Cookie区别什么是HTTP三、多系统登录的问题与解决3.1 Session不共享问题单系统登录功能主要是用Session保存用户信息来实现的,但我们清楚的是:多系统即可能有多个Tomcat,而Session是依赖当前系统的Tomcat,所以系统A的Session和系统B的Session是不共享的。解决系统之间Session不共享问题有一下几种方案:Tomcat集群Session全局复制(集群内每个tomcat的session完全同步)【会影响集群的性能呢,不建议】根据请求的IP进行Hash映射到对应的机器上(这就相当于请求的IP一直会访问同一个服务器)【如果服务器宕机了,会丢失了一大部分Session的数据,不建议】把Session数据放在Redis中(使用Redis模拟Session)【建议】如果还不了解Redis的同学,建议移步(Redis合集)我们可以将登录功能单独抽取出来,做成一个子系统。SSO(登录系统)的逻辑如下:// 登录功能(SSO单独的服务) @Override public TaotaoResult login(String username, String password) throws Exception { //根据用户名查询用户信息 TbUserExample example = new TbUserExample(); Criteria criteria = example.createCriteria(); criteria.andUsernameEqualTo(username); List<TbUser> list = userMapper.selectByExample(example); if (null == list || list.isEmpty()) { return TaotaoResult.build(400, "用户不存在"); } //核对密码 TbUser user = list.get(0); if (!DigestUtils.md5DigestAsHex(password.getBytes()).equals(user.getPassword())) { return TaotaoResult.build(400, "密码错误"); } //登录成功,把用户信息写入redis //生成一个用户token String token = UUID.randomUUID().toString(); jedisCluster.set(USER_TOKEN_KEY + ":" + token, JsonUtils.objectToJson(user)); //设置session过期时间 jedisCluster.expire(USER_TOKEN_KEY + ":" + token, SESSION_EXPIRE_TIME); return TaotaoResult.ok(token); }其他子系统登录时,请求SSO(登录系统)进行登录,将返回的token写到Cookie中,下次访问时则把Cookie带上:public TaotaoResult login(String username, String password, HttpServletRequest request, HttpServletResponse response) { //请求参数 Map<String, String> param = new HashMap<>(); param.put("username", username); param.put("password", password); //登录处理 String stringResult = HttpClientUtil.doPost(REGISTER_USER_URL + USER_LOGIN_URL, param); TaotaoResult result = TaotaoResult.format(stringResult); //登录出错 if (result.getStatus() != 200) { return result; } //登录成功后把取token信息,并写入cookie String token = (String) result.getData(); //写入cookie CookieUtils.setCookie(request, response, "TT_TOKEN", token); //返回成功 return result; }总结:SSO系统生成一个token,并将用户信息存到Redis中,并设置过期时间其他系统请求SSO系统进行登录,得到SSO返回的token,写到Cookie中每次请求时,Cookie都会带上,拦截器得到token,判断是否已经登录到这里,其实我们会发现其实就两个变化:将登陆功能抽取为一个系统(SSO),其他系统请求SSO进行登录本来将用户信息存到Session,现在将用户信息存到Redis3.2 Cookie跨域的问题上面我们解决了Session不能共享的问题,但其实还有另一个问题。Cookie是不能跨域的比如说,我们请求<https://www.google.com/>时,浏览器会自动把google.com的Cookie带过去给google的服务器,而不会把<https://www.baidu.com/>的Cookie带过去给google的服务器。这就意味着,由于域名不同,用户向系统A登录后,系统A返回给浏览器的Cookie,用户再请求系统B的时候不会将系统A的Cookie带过去。针对Cookie存在跨域问题,有几种解决方案:服务端将Cookie写到客户端后,客户端对Cookie进行解析,将Token解析出来,此后请求都把这个Token带上就行了多个域名共享Cookie,在写到客户端的时候设置Cookie的domain。将Token保存在SessionStroage中(不依赖Cookie就没有跨域的问题了)到这里,我们已经可以实现单点登录了。3.3 CAS原理说到单点登录,就肯定会见到这个名词:CAS (Central Authentication Service),下面说说CAS是怎么搞的。如果已经将登录单独抽取成系统出来,我们还能这样玩。现在我们有两个系统,分别是www.java3y.com和www.java4y.com,一个SSOwww.sso.com首先,用户想要访问系统Awww.java3y.com受限的资源(比如说购物车功能,购物车功能需要登录后才能访问),系统Awww.java3y.com发现用户并没有登录,于是重定向到sso认证中心,并将自己的地址作为参数。请求的地址如下:www.sso.com?service=www.java3y.comsso认证中心发现用户未登录,将用户引导至登录页面,用户进行输入用户名和密码进行登录,用户与认证中心建立全局会话(生成一份Token,写到Cookie中,保存在浏览器上)随后,认证中心重定向回系统A,并把Token携带过去给系统A,重定向的地址如下:www.java3y.com?token=xxxxxxx接着,系统A去sso认证中心验证这个Token是否正确,如果正确,则系统A和用户建立局部会话(创建Session)。到此,系统A和用户已经是登录状态了。此时,用户想要访问系统Bwww.java4y.com受限的资源(比如说订单功能,订单功能需要登录后才能访问),系统Bwww.java4y.com发现用户并没有登录,于是重定向到sso认证中心,并将自己的地址作为参数。请求的地址如下:www.sso.com?service=www.java4y.com注意,因为之前用户与认证中心www.sso.com已经建立了全局会话(当时已经把Cookie保存到浏览器上了),所以这次系统B重定向到认证中心www.sso.com是可以带上Cookie的。认证中心根据带过来的Cookie发现已经与用户建立了全局会话了,认证中心重定向回系统B,并把Token携带过去给系统B,重定向的地址如下:www.java4y.com?token=xxxxxxx接着,系统B去sso认证中心验证这个Token是否正确,如果正确,则系统B和用户建立局部会话(创建Session)。到此,系统B和用户已经是登录状态了。看到这里,其实SSO认证中心就类似一个中转站。
一、理解axis如果你像我一样,发现API中有axis这个参数,但不知道是什么意思。可能就会搜搜axis到底代表的什么意思。于是可能会类似搜到下面的信息:使用0值表示沿着每一列或行标签\索引值向下执行方法(axis=0代表往跨行)使用1值表示沿着每一行或者列标签模向执行对应的方法(axis=1代表跨列)但我们又知道,我们的数组不单单只有二维的,还有三维、四维等等。一旦维数超过二维,就无法用简单的行和列来表示了。所以,可以用我下面的方式进行理解:axis=0将最开外头的括号去除,看成一个整体,在这个整体上进行运算axis=1将第二个括号去除,看成一个整体,在这个整体上进行运算…依次类推话不多说,下面以例子说明~1.1二维数组之concat首先,我们来看个concat的例子,concat第一个参数接收val,第二个参数接收的是axisdef learn_concat(): # 二维数组 t1 = tf.constant([[1, 2, 3], [4, 5, 6]]) t2 = tf.constant([[7, 8, 9], [10, 11, 12]]) with tf.Session() as sess: # 二维数组针对 axis 为0 和 1 的情况 print(sess.run(tf.concat([t1, t2], 0))) print(sess.run(tf.concat([t1, t2], 1)))ok,下面以图示的方式来说明。现在我们有两个数组,分别是t1和t2: 两个数组,t1和t2首先,我们先看axis=0的情况,也就是tf.concat([t1, t2], 0)。从上面的描述,我们知道,先把第一个括号去除,然后将其子内容看成一个整体,在这个整体下进行想对应的运算(这里我们就是concat)。 二维数组 axis=0的concat操作所以最终的结果是:[ [1 2 3], [4 5 6], [7 8 9], [10 11 12] ]接着,我们再看axis=1的情况,也就是tf.concat([t1, t2], 1)。从上面的描述,我们知道,先把第二个括号去除,然后将其子内容看成一个整体,在这个整体下进行想对应的运算(这里我们就是concat)。 axis=1理解 二维数组 concat所以最终的结果是:[ [1, 2, 3, 7, 8, 9] [4, 5, 6, 10, 11, 12] ]1.2三维数组之concat接下来我们看一下三维的情况def learn_concat(): # 三维数组 t3 = tf.constant([[[1, 2], [2, 3]], [[4, 4], [5, 3]]]) t4 = tf.constant([[[7, 4], [8, 4]], [[2, 10], [15, 11]]]) with tf.Session() as sess: # 三维数组针对 axis 为0 和 1 和 -1 的情况 print(sess.run(tf.concat([t3, t4], 0))) print(sess.run(tf.concat([t3, t4], 1))) print(sess.run(tf.concat([t3, t4], -1)))ok,下面也以图示的方式来说明。现在我们有两个数组,分别是t3和t4: 两个数组,t3和t4首先,我们先看axis=0的情况,也就是tf.concat([t3, t4], 0)。从上面的描述,我们知道,先把第一个括号去除,然后将其子内容看成一个整体,在这个整体下进行想对应的运算(这里我们就是concat)。 axis=0理解 三维数组 concat所以最终的结果是:[ [ [1 2] [2 3] ] [ [4 4] [5 3] ] [ [7 4] [8 4] ] [ [2 10] [15 11] ] ]接着,我们再看axis=1的情况,也就是tf.concat([t3, t4], 1)。从上面的描述,我们知道,先把第二个括号去除,然后将其子内容看成一个整体,在这个整体下进行想对应的运算(这里我们就是concat)。 axis=1理解 三维数组 concat所以最终的结果是:[ [ [1 2] [2 3] [7 4] [8 4] ] [ [4 4] [5 3] [2 10] [15 11] ] ]最后,我们来看一下axis=-1这种情况,在文档也有相关的介绍:As in Python, the axis could also be negative numbers. Negative axisare interpreted as counting from the end of the rank, i.e.,axis + rank(values)-th dimension所以,对于我们三维的数组而言,那axis=-1实际上就是axis=2,下面我们再来看一下这种情况: axis=-1理解 三维数组 concat最终的结果是:[ [ [1 2 7 4] [2 3 8 4] ] [ [4 4 2 10] [5 3 15 11] ] ]除了concat以外,其实很多函数都用到了axis这个参数,再举个例子:>>> item = np.array([[1,4,8],[2,3,5],[2,5,1],[1,10,7]]) >>> item array([[1, 4, 8], [2, 3, 5], [2, 5, 1], [1, 10, 7]]) >>> item.sum(axis = 1) array([13, 10, 8, 18]) >>> item.sum(axis = 0) array([ 6, 22, 21])
Service层代码:SQL代码(没有加悲观/乐观锁):用1000个线程跑代码:简单来说:多线程跑一个使用synchronized关键字修饰的方法,方法内操作的是数据库,按正常逻辑应该最终的值是1000,但经过多次测试,结果是低于1000。这是为什么呢?一、我的思考既然测试出来的结果是低于1000,那说明这段代码不是线程安全的。不是线程安全的,那问题出现在哪呢?众所周知,synchronized方法能够保证所修饰的代码块、方法保证有序性、原子性、可见性。讲道理,以上的代码跑起来,问题中Service层的increaseMoney()是有序的、原子的、可见的,所以断定跟synchronized应该没关系。(参考我之前写过的synchronize锁笔记:Java锁机制了解一下)既然Java层面上找不到原因,那分析一下数据库层面的吧(因为方法内操作的是数据库)。在increaseMoney()方法前加了@Transcational注解,说明这个方法是带有事务的。事务能保证同组的SQL要么同时成功,要么同时失败。讲道理,如果没有报错的话,应该每个线程都对money值进行+1。从理论上来说,结果应该是1000的才对。(参考我之前写过的Spring事务:一文带你看懂Spring事务!)根据上面的分析,我怀疑是提问者没测试好(hhhh,逃),于是我也跑去测试了一下,发现是以提问者的方式来使用是真的有问题。首先贴一下我的测试代码:@RestController public class EmployeeController { @Autowired private EmployeeService employeeService; @RequestMapping("/add") public void addEmployee() { for (int i = 0; i < 1000; i++) { new Thread(() -> employeeService.addEmployee()).start(); } } } @Service public class EmployeeService { @Autowired private EmployeeRepository employeeRepository; @Transactional public synchronized void addEmployee() { // 查出ID为8的记录,然后每次将年龄增加一 Employee employee = employeeRepository.getOne(8); System.out.println(employee); Integer age = employee.getAge(); employee.setAge(age + 1); employeeRepository.save(employee); } }简单地打印了每次拿到的employee值,并且拿到了SQL执行的顺序,如下(贴出小部分):从打印的情况我们可以得出:多线程情况下并没有串行执行addEmployee()方法。这就导致对同一个值做重复的修改,所以最终的数值比1000要少。二、图解出现的原因发现并不是同步执行的,于是我就怀疑synchronized关键字和Spring肯定有点冲突。于是根据这两个关键字搜了一下,找到了问题所在。我们知道Spring事务的底层是Spring AOP,而Spring AOP的底层是动态代理技术。跟大家一起回顾一下动态代理:public static void main(String[] args) { // 目标对象 Object target ; Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 但凡带有@Transcational注解的方法都会被拦截 // 1... 开启事务 method.invoke(target); // 2... 提交事务 return null; } }); }(详细请参考我之前写过的动态代理:给女朋友讲解什么是代理模式)实际上Spring做的处理跟以上的思路是一样的,我们可以看一下TransactionAspectSupport类中invokeWithinTransaction():调用方法前开启事务,调用方法后提交事务在多线程环境下,就可能会出现:方法执行完了(synchronized代码块执行完了),事务还没提交,别的线程可以进入被synchronized修饰的方法,再读取的时候,读到的是还没提交事务的数据,这个数据不是最新的,所以就出现了这个问题。三、解决问题从上面我们可以发现,问题所在是因为@Transcational注解和synchronized一起使用了,加锁的范围没有包括到整个事务。所以我们可以这样做:新建一个名叫SynchronizedService类,让其去调用addEmployee()方法,整个代码如下:@RestController public class EmployeeController { @Autowired private SynchronizedService synchronizedService ; @RequestMapping("/add") public void addEmployee() { for (int i = 0; i < 1000; i++) { new Thread(() -> synchronizedService.synchronizedAddEmployee()).start(); } } } // 新建的Service类 @Service public class SynchronizedService { @Autowired private EmployeeService employeeService ; // 同步 public synchronized void synchronizedAddEmployee() { employeeService.addEmployee(); } } @Service public class EmployeeService { @Autowired private EmployeeRepository employeeRepository; @Transactional public void addEmployee() { // 查出ID为8的记录,然后每次将年龄增加一 Employee employee = employeeRepository.getOne(8); System.out.println(Thread.currentThread().getName() + employee); Integer age = employee.getAge(); employee.setAge(age + 1); employeeRepository.save(employee); } }我们将synchronized锁的范围包含到整个Spring事务上,这就不会出现线程安全的问题了。在测试的时候,我们可以发现1000个线程跑起来比之前要慢得多,当然我们的数据是正确的:四、留下疑问现在我们知道为啥会出现线程安全问题了,也知道如何解决了。在我写文章的时候,我也从中发现一些问题,细心的你不知道注意到了没有。我测试的代码中synchronized是修饰在方法上的,按我的推断:应该是synchronized锁释放后,事务提交前这时间间隔内才会出现线程安全问题(别的线程偷偷跑进去了)。但从上面测试打印的SQL来看,并不完全是这样:应该不会出现一连串的查询,而是查询-更新,查询-更新,查询-更新这种情况才对的。总体来看,我认为思路是没有问题的,但出现上面的结果是我没考虑到的,如果知道为什么会出现这种情况的同学不妨在评论区留言告诉我。最后可以发现的是,虽然说Spring事务用起来我们是非常方便的,但如果不了解一些Spring事务的细节,很多时候出现Bug了就百思不得其解。
一、为什么需要Docker官方介绍(中文版):Docker 是世界领先的软件容器平台。开发人员利用 Docker 可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用 Docker 可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用 Docker 可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为 Linux 和 Windows Server 应用发布新功能。1.1环境(切换/配置)麻烦一般我们写程序的,能接触到好几个环境:自己写代码的环境叫做开发环境。给测试去跑的环境叫做测试环境。测试完可以对外使用的叫做生产环境。其实我们在学习编程中,很多时间都浪费在“环境”上:如果我现在重装了系统,我想要跑我的war/jar包,我得去安装一下JDK、Tomcat、MySQL等配置各种的环境变量才能跑起来。开开心心地跟着博主给出的步骤去写Demo,但总是有Bug。(这里我将版本/依赖也归纳在环境的范畴里边)。好不容易在测试环境下跑起来了,在生产环境就各种出错!跟着教学视频做分布式/集群的项目,跑一堆的虚拟机,每个虚拟机都要安装对应的环境。所以就有个笑话《千万不要跟程序员说,你的代码有bug》:他的第一反应是你的环境有问题,第二就是你是傻逼不会用吧。你要跟他这么说:“这个程序运行的怎么运行的跟预期不一样,是我操作有问题吗?”。这货就会第一反应“我擦,这是不是出bug了?”1.2应用之间需要隔离比如我写了两个应用(网站),这两个应用部署在同一台服务器上,那可能会出现什么问题?如果一个应用出现了问题,导致CPU占100%。那另一个应用也会受到关联,跟着一起凉凉了。这两个应用是完全不同技术栈的应用,比如一个PHP,一个.NET。这两个应用各种的依赖软件都安装在同一个服务器上,可能就会造成各种冲突/无法兼容,这可能调试就非常麻烦了。二、Docker是如何解决上述的问题的2.1解决环境(切换/配置)不知道大家有没有装过系统,比如说装Linux虚拟机,重装Windows系统,都是需要镜像的。有了这个镜像,我们就可以运行这个镜像,来进行安装系统的操作(此处省略N个下一步),于是我们的系统就装好了。一般来说,我们去官方渠道下载的镜像,都是纯净的。比如去官方下载Windows镜像,装完后之后桌面只有一个回收站。但有过了解装系统的同学可能就会知道,有的镜像装完可能还有360这些软件,但系统的的确确是变了。简单来说,就是这些镜像添加了其他的东西(比如360软件、腾讯、千千静听等等软件)。Docker也是这种思路,可以将我们的想要的环境构建(打包)成一个镜像,然后我们可以推送(发布)到网上去。想要用这个环境的时候,在网上拉取一份就好了。有了Docker,我们在搭环境的时候,跟以前的方式就不一样了。之前:在开发环境构建出了一个war包,想跑到Linux下运行。我们得先在Linux下载好Java、Tomcat、MySQL,配置好对应的环境变量,将war包丢到Tomcat的webapps文件夹下,才能跑起来。现在:在Linux下直接拉取一份镜像(各种环境都配好了),将镜像运行起来,把war包丢进去就好了。将Docker的镜像运行起来就是一两秒的事情而已,十分方便的。2.2解决应用之间隔离说到这里,就得提出一个大家可能不认识的概念:LXC(Linux Containers)--->Linux容器。2.2.1Linux容器在Linux内核中,提供了cgroups功能,来达成资源的区隔化。它同时也提供了名称空间(namespace)区隔化的功能,使应用程序看到的操作系统环境被区隔成独立区间,包括进程树,网络,用户id,以及挂载的文件系统。简单来说就是:LXC是一个为Linux内核包含特征的用户接口。通过强大的API和简单的工具,它可以让Linux用户轻松的创建和托管系统或者应用程序容器。2.2.2回到Docker我们在翻看Docker的官方文档的时候,也很容易看见cgroup和namespace这两个名词:来源维基百科:Early versions of Docker used LXC as the container execution driver, though LXC was made optional in v0.9 and support was dropped in Docker v1.10.lxc是早期版本docker的一个基础组件,docker 主要用到了它对 Cgroup 和 Namespace 两个内核特性的控制。新的Docker版本已经移除了对LXC的support。2.2.3Docker在Windows和Mac上面说了,Docker底层用的Linux的cgroup和namespace这两项技术来实现应用隔离,那Windows和Mac用户能用Docker吗?之前,Windows和Mac使用Docker实际上就是跑了一层Linux虚拟机。比如在Windows下安装的是Docker Toolbox,它需要Oracle Virtual Box来跑Docker现在,Windows和Mac都已经原生支持Docker了。但需要一些安装的条件,详情可以查看官网比如Windows:Docker for Windows requires 64bit Windows 10 Pro and Microsoft Hyper-V参考资料:Windows 原生 Docker 正式商用http://blog.daocloud.io/windows-docker/三、虚拟机和Docker说到应用隔离和镜像,我就想起了虚拟机。今年下半年(此处省略…..),文体两开花(此处省略…..),要是我写文章写得不好,我是需要向XX谢罪的。估计大家都用过虚拟机,虚拟机也能实现对应用的隔离,安装特定的镜像也能跑出我们想要的环境。虚拟机已经发展了很久了,为什么我们还需要Docker呢?这部分内容在官网也有相关的介绍:http://www.docker-cn.com/what-container#/virtual_machines一句话总结:Docker容器比虚拟机轻量多了!最后Docker可以干嘛?将一整套环境打包封装成镜像,无需重复配置环境,解决环境带来的种种问题。Docker容器间是进程隔离的,谁也不会影响谁。其实这篇文章主要是讲为什么我们需要Docker(在学习一项技术之前,必须要知道这项技术是用来干嘛的),Docker的一些概念和命令我还没介绍(留到下一篇啦)。
前言在读《Redis设计与实现》关于哈希表扩容的时候,发现这么一段话:执行BGSAVE命令或者BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率,所以在子进程存在期间,服务器会提高负载因子的阈值,从而避免在子进程存在期间进行哈希表扩展操作,避免不必要的内存写入操作,最大限度地节约内存。触及到知识的盲区了,于是就去搜了一下copy-on-write写时复制这个技术究竟是怎么样的。发现涉及的东西蛮多的,也挺难读懂的。于是就写下这篇笔记来记录一下我学习copy-on-write的过程。本文力求简单讲清copy-on-write这个知识点,希望大家看完能有所收获。一、Linux下的copy-on-write在说明Linux下的copy-on-write机制前,我们首先要知道两个函数:fork()和exec()。需要注意的是exec()并不是一个特定的函数, 它是一组函数的统称, 它包括了execl()、execlp()、execv()、execle()、execve()、execvp()。1.1简单来用用fork首先我们来看一下fork()函数是什么鬼:fork is an operation whereby a process creates a copy of itself.fork是类Unix操作系统上创建进程的主要方法。fork用于创建子进程(等同于当前进程的副本)。新的进程要通过老的进程复制自身得到,这就是fork!如果接触过Linux,我们会知道Linux下init进程是所有进程的爹(相当于Java中的Object对象)Linux的进程都通过init进程或init的子进程fork(vfork)出来的。下面以例子说明一下fork吧:#include <unistd.h> #include <stdio.h> int main () { pid_t fpid; //fpid表示fork函数返回的值 int count=0; // 调用fork,创建出子进程 fpid=fork(); // 所以下面的代码有两个进程执行! if (fpid < 0) printf("创建进程失败!/n"); else if (fpid == 0) { printf("我是子进程,由父进程fork出来/n"); count++; } else { printf("我是父进程/n"); count++; } printf("统计结果是: %d/n",count); return 0; }得到的结果输出为:我是子进程,由父进程fork出来 统计结果是: 1 我是父进程 统计结果是: 1解释一下:fork作为一个函数被调用。这个函数会有两次返回,将子进程的PID返回给父进程,0返回给子进程。(如果小于0,则说明创建子进程失败)。再次说明:当前进程调用fork(),会创建一个跟当前进程完全相同的子进程(除了pid),所以子进程同样是会执行fork()之后的代码。所以说:父进程在执行if代码块的时候,fpid变量的值是子进程的pid子进程在执行if代码块的时候,fpid变量的值是01.2再来看看exec()函数从上面我们已经知道了fork会创建一个子进程。子进程的是父进程的副本。exec函数的作用就是:装载一个新的程序(可执行映像)覆盖当前进程内存空间中的映像,从而执行不同的任务。exec系列函数在执行时会直接替换掉当前进程的地址空间。我去画张图来理解一下: exec函数的作用参考资料:程序员必备知识——fork和exec函数详解https://blog.csdn.net/bad_good_man/article/details/49364947linux中fork()函数详解(原创!!实例讲解):https://blog.csdn.net/jason314/article/details/5640969linux c语言 fork() 和 exec 函数的简介和用法:https://blog.csdn.net/nvd11/article/details/8856278Linux下Fork与Exec使用:https://www.cnblogs.com/hicjiajia/archive/2011/01/20/1940154.htmlLinux 系统调用 —— fork()内核源码剖析:https://blog.csdn.net/chen892704067/article/details/765962253回头来看Linux下的COW是怎么一回事fork()会产生一个和父进程完全相同的子进程(除了pid)如果按传统的做法,会直接将父进程的数据拷贝到子进程中,拷贝完之后,父进程和子进程之间的数据段和堆栈是相互独立的。 父进程的数据拷贝到子进程中但是,以我们的使用经验来说:往往子进程都会执行exec()来做自己想要实现的功能。所以,如果按照上面的做法的话,创建子进程时复制过去的数据是没用的(因为子进程执行exec(),原有的数据会被清空)既然很多时候复制给子进程的数据是无效的,于是就有了Copy On Write这项技术了,原理也很简单:fork创建出的子进程,与父进程共享内存空间。也就是说,如果子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程,这样创建子进程的速度就很快了!(不用复制,直接引用父进程的物理空间)。并且如果在fork函数返回之后,子进程第一时间exec一个新的可执行映像,那么也不会浪费时间和内存空间了。另外的表达方式:在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。Copy On Write技术实现原理:fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。Copy On Write技术好处是什么?COW技术可减少分配和复制大量资源时带来的瞬间延时。COW技术可减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。Copy On Write技术缺点是什么?如果在fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断page-fault),这样就得不偿失。几句话总结Linux的Copy On Write技术:fork出的子进程共享父进程的物理空间,当父子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享父进程的)。fork出的子进程功能实现和父进程是一样的。如果有需要,我们会用exec()把当前进程映像替换成新的进程文件,完成自己想要实现的功能。参考资料:Linux进程基础:http://www.cnblogs.com/vamei/archive/2012/09/20/2694466.htmlLinux写时拷贝技术(copy-on-write)http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html当你在 Linux 上启动一个进程时会发生什么?https://zhuanlan.zhihu.com/p/33159508Linux fork()所谓的写时复制(COW)到最后还是要先复制再写吗?https://www.zhihu.com/question/265400460写时拷贝(copy-on-write) COW技术https://blog.csdn.net/u012333003/article/details/25117457Copy-On-Write 写时复制原理https://blog.csdn.net/ppppppppp2009/article/details/22750939二、解释一下Redis的COW基于上面的基础,我们应该已经了解COW这么一项技术了。下面我来说一下我对《Redis设计与实现》那段话的理解:Redis在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis会fork出一个子进程来读取数据,从而写到磁盘中。总体来看,Redis还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断page-fault),这样就得耗费不少性能在复制上。而在rehash阶段上,写操作是无法避免的。所以Redis在fork出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。参考资料:fork()后copy on write的一些特性:https://zhoujianshi.github.io/articles/2017/fork()%E5%90%8Ecopy%20on%20write%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/index.html写时复制:https://miao1007.github.io/gitbook/java/juc/cow/三、文件系统的COW下面来看看文件系统中的COW是啥意思:Copy-on-write在对数据进行修改的时候,不会直接在原来的数据位置上进行操作,而是重新找个位置修改,这样的好处是一旦系统突然断电,重启之后不需要做Fsck。好处就是能保证数据的完整性,掉电的话容易恢复。比如说:要修改数据块A的内容,先把A读出来,写到B块里面去。如果这时候断电了,原来A的内容还在!参考资料:文件系统中的 copy-on-write 模式有什么具体的好处?https://www.zhihu.com/question/19782224/answers/created新一代 Linux 文件系统 btrfs 简介:https://www.ibm.com/developerworks/cn/linux/l-cn-btrfs/最后最后我们再来看一下写时复制的思想(摘录自维基百科):写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。至少从本文我们可以总结出:Linux通过Copy On Write技术极大地减少了Fork的开销。文件系统通过Copy On Write技术一定程度上保证数据的完整性。其实在Java里边,也有Copy On Write技术。
退出root账户,用jkXX账户登录。退出jkXX账户,返回root账户,观察/etc/shadow文件;用passwd命令锁定用户jkXX,观察/etc/shadow文件变化;然后退出root账户,用jkXX账户登录,是否成功?用chage命令查看peter账户的时间设置;重新设置peter账户的时间,要求两天内不能更改口令,且口令最长的存活期为 90 天,并在口令过期前 5 天通知用户,口令超期7天密码失效;用chage命令再次查看peter账户的时间设置用root账户登录;用su切换到jason账户;用cd进入用户主目录;创建一个新文件abc,用长格式列出abc文件;观察文件的用户和组的属性锁定账户后,shadow文件发生了什么变化?答:锁定账户的密码之前会锁定标志!!用su切换用户后,建立的新文件文件属于哪个用户?答:新文件属于切换之后的用户。两次执行chpasswd命令,结果是否相同?加密算法md5和sha512哪个更安全?答:两次执行chpasswd命令结果不同,默认情况采用sha512加密算法;-m选项时,采用md5加密算法;sha512更安全,因为加密信息长度更长,破解计算量大。建立三个普通用户账户,要求如下:用户名分别为jkXX(XX为学生学号末两位),peter,jason,其中jkXX和jason为相同普通组成员;观察/etc/passwd文件的变化。为jkXX账户添加root组;分别练习id,groups,whoami,who命令,显示当前账户的信息;用su命令切换到jkXX账户,分别练习id,groups,whoami,who命令,显示当前账户的信息。用newgrp切换jkXX账户的组,分别练习id,groups,whoami,who命令,显示当前账户的信息二、权限管理Linux是多用户的操作系统,允许多个用户同时在系统上登录和工作。 为了确保系统和用户的安全,Linux自然就有自己一套的权限管理机制了!相信用过Linux的同学在检索文件夹文件的时候常常用到ls -l的命令,会出来一大串的数据。这些数据你能读懂了吗?例如:drwxr-xr-x 3 osmond osmond 4096 05-16 13:32 nobp其实很简单:其实我们看权限就是看drwxr-xr-x这么一串东西,看起来很复杂,但不是的,一下就可以理解了。我们来分解一下:这9个字符每3个一组,组成 3 套 权限控制第一套控制文件所有者的访问权限第二套控制所有者所在用户组的其他成员的访问权限第三套控制系统其他用户的访问权限rwx分别代表的意思:看到这里来,如果前面的你看懂了,那drwxr-xr-x这么一串东西我觉得你很容易就能理解了:d是文件夹,后面还有9个字母,每3个分成一组,-号表示没有。那么这个文件夹的权限就是:对当前用户是可读可写可执行,对同组的用户是可读可执行,对其他的用户是可读可执行是不是很简单??r-read,w-write,x-execute,很好理解的。对于这些rwx命令为了方便还可以换成八进制的数据来表示,我相信大家看完下面的demo也知道其实就这么一回事了:权限的优先顺序:如果UID匹配,就应用用户属主(user)权限否则,如果GID匹配,就应用组(group)权限如果都不匹配,就应用其它用户(other)权限超级用户root具有一切权限,无需特殊说明2.1管理Linux权限的常用命令chmod改变文件或目录的权限chown改变文件或目录的属主(所有者)chgrp改变文件或目录所属的组umask设置文件的缺省生成掩码例子:2.2权限扩展知识上面提到了umask属性,它用来做这样的东西的:默认生成掩码告诉系统当创建一个文件或目录时不应该赋予其哪些权限。默认的umask的值是022,我们看一下下面的例子应该就能懂了:除了上面所说的权限之外,Linux还提供了三种特殊的权限:SUID:使用命令的所属用户的权限来运行,而不是命令执行者的权限SGID:使用命令的组权限来运行。Sticky-bit:目录中的文件只能被文件的所属用户和root用户删除。它们是这样表示的:SUID和SGID用s表示;Sticky-bit用t表示SUID是占用属主的x位置来表示SGID是占用组的x位置来表示sticky-bit是占用其他人的x位置来表示例如:drwxrwxrwt 5 root root 4096 06-18 01:01 /tmp它就拥有sticky-bit权限。-rwsr-xr-x 1 root root 23420 2010-08-11 /usr/bin/passwd它就拥有SUID权限SUID,SGID,sticky-bit同样也有数字的表示法:使用的例子:Linux内核中有大量安全特征。EXT2/3/4文件系统的扩展属性(Extended Attributes)可以在某种程度上保护系统的安全常见的扩展属性:A(Atime):告诉系统不要修改对这个文件的最后访问时间。使用A属性可以提高一定的性能。S(Sync):一旦应用程序对这个文件执行了写操作,使系统立刻把修改的结果写到磁盘。使用S属性能够最大限度的保障文件的完整性。a(Append Only):系统只允许在这个文件之后追加数据,不允许任何进程覆盖或者截断这个文件。如果目录具有这个属性,系统将 只允许在这个目录下建立和修改文件,而不允许删除任何文件。i(Immutable):系统不允许对这个文件进行任何的修改。如果目录具有这个属性,那么任何的进程只能修改目录之下的文件,不允许建立和删除文件。a属性和i属性对于提高文件系统的安全性和保障文件系统的完整性有很大的好处。常用命令:显示扩展属性:lsattr [-adR] [文件|目录]修改扩展属性:chattr [-R] [[-+=][属性]] <文件|目录>2.3权限管理练习题用root账户登录,创建一个文件aaaXX(XX为学生学号末两位),用长格式查看文件权限;用chmod命令,文字设定法,给aaaXX文件同组增加写属性,观察结果;用chmod命令,数字设定法,给aaaXX文件设置权限为766,观察结果;切换到peter账户,查看当前umask是多少,观察结果;创建一个目录foldXX(XX为学生学号末两位),查看其权限;创建一个新文件bbb,查看其权限;改变unmask为066,创建一个新文件ccc,查看其权限切换到jkXX账户;创建一个文件myfile,观察其属性;用chgrp改变文件myfile组属性为root;试着去改变文件myfile主属性为root,可以吗?切换到root账户,改变文件myfile主属性为root,观察结果数字设定766代表文件权限是什么?答:766代表文件权限为rwx-rw-rw-为什么用jkXX账户改变文件myfile的属主失败?答:因为chown只有root账户才可以使用Umask为022和066对新创建的文件属性影响一样吗?为什么?答:影响当然不一样,umask定义的是默认不应该获得的权限,066比022转换成为二进制数后,多了两个限制比特位。以root账户登录,复制/usr/bin/dir文件到用户主目录,用长格式列出,设置文件的suid和sguid为1,用长格式列出;切换帐号为jkXX,运行复制过来的文件dir(注意运行当前路径下的文件要带上路径,例如./dir);切换到jkXX账户,进入/tmp目录,建立文件夹myfold,设置文件夹myfold权限为777,并且sgid和sticky-bit为1,用长格式列出,观察myfold的属性;进入myfold,创建新文件aaa,设置属性为任何人可读可写,用长格式列出;切换到jason账户,进入/tmp/myfold目录,删除aaa文件,是否可以删除?root账户,进入用户主目录;创建一个文件bbb文件,查看文件的扩展属性;给文件bbb添加扩展属性i,然后试着删除该文件,是否成功,怎样才能删除;创建一个ccc文件,给文件ccc添加扩展属性a,用长格式列表/bin目录并重定向输出到ccc文件,观察ccc文件长度的变化,用长格式列表/etc目录,并重定向输出到ccc文件,是否成功切换到jkXX账户,在/tmp目录下创建一个目录myshare,用getfacl查看myshare目录文件访问控制表;设置myshare文件夹对于jason用户权限为rwx,查看文件访问控制表的变化;切换到jason账户,进入myshare文件创建文件yyy,是否成功;切换到peter账户,进入myshare文件创建文件zzz,是否成功,为什么?myfold目录下,为什么jason账户不能删除一个任何人都可读可写的文件?答:因为文件所在的文件夹myfold被它的所属者jk08设置了stickybit位,该文件夹下面的所有文件,只有文件所属,以及root用户才能删除。为什么peter账户在在myshare文件夹里面不能创建文件?答:因为myshare文件夹,属于jk08用户,只有jk08对该目录具备rwx权限。此外,采用facl的方式,给jason用户开放了该目录的rwx访问权限;peter既不是文件夹的拥有者,也没有在facl中开放rwx权限;依据权限设置情况,peter只有该文件夹的rx权限。因此,不能创建文件。添加扩展属性a后,用重定向将输出内容给ccc文件,可能会失败,怎样才能输出成功?答:应该采用追加方式的重定向>>,可以在文件末尾添加内容,这样才符合文件扩展属性a的安全规定。三、总结本文主要是总结了Linux下操作用户和权限的知识~~~这两个知识点在Linux下也是很重要的,是学习Linux的基础~
进程的概述首先,我们要明白一点:程序不能独立运行,作为资源分配和独立运行的单位是进程。操作系统所具有的四大特征也都是基于进程而形成的。学习进程的前提:前面也讲了操作系统的发展历史,我们知道未配置操作系统和单批到处理系统的程序是按照顺序执行的。只有前边的程序执行完了,后边的程序才能执行。因此,CPU的利用率是非常低下的。、为了能更好地描述程序的顺序和并发执行情况,我们先介绍用于描述程序执行先后顺序的前趋图。所谓前趋图(Precedence Graph),是指一个有向无循环图,可记为DAG(Directed Acyclic Graph),它用于描述进程之间执行的先后顺序。根据前趋图,我们就可以发现程序有以下的三个特性:① 顺序性:指处理机严格地按照程序所规定的顺序执行,即每一操作必须在下一个操作开始之前结束;② 封闭性:指程序在封闭的环境下运行,即程序运行时独占全机资源,资源的状态(除初始状态外)只有本程序才能改变它,程序一旦开始执行,其执行结果不受外界因素影响;③ 可再现性:指只要程序执行时的环境和初始条件相同,当程序重复执行时,不论它是从头到尾不停顿地执行,还是“停停走走”地执行,都可获得相同的结果。但是,当程序中引入了并发这么一个概念的时候,会给程序带来新的特征:(1) 间断性。(2) 失去封闭性。(3) 不可再现性。为什么要引入进程:在多道程序环境下,程序的执行属于并发执行,此时它们将失去其封闭性,并具有间断性,以及其运行结果不可再现性的特征。由此,决定了通常的程序是不能参与并发执行的,否则,程序的运行也就失去了意义。为了能使程序并发执行,并且可以对并发执行的程序加以描述和控制,人们引入了“进程”的概念。小总结:引入进程就是为了能够让程序能够并发执行!进程的定义:(1) 进程是程序的一次执行。(2) 进程是一个程序及其数据在处理机上顺序执行时所发生的活动。(3) 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。进程的特征:除了进程具有程序所没有的PCB结构外,还具有下面一些特征:(1) 动态性。(2) 并发性。(3) 独立性。(4) 异步性。进程的基本状态和装换一般而言,每一个进程至少应处于以下三种基本状态之一:**(1) 就绪(Ready)状态。(2) 执行(Running)状态。(3) 阻塞(Block)状态。 **进程控制进程控制是指系统使用一些具有特定功能的程序段来创建、撤消进程以及完成进程各状态间的转换,从而达到多进程高效率并发执行和协调、实现资源共享的目的。进程的控制是通过原语实现的。用于进程控制的原语有:创建原语、撤消原语、阻塞原语、唤醒原语、挂起原语和激活原语等。一般,把系统态下执行的某些具有特定功能的程序段称为原语。原语可分为机器指令级原语和功能级原语。前者是一条语句,后者是一段代码。机器指令级原语的特点是执行期间不允许中断,它是一个不可分割的基本单位。功能级原语的特点是作为原语的程序段不允许并发执行。1)活动就绪Readya→静止就绪Readys:Suspend原语2)活动阻塞Blockeda →静止阻塞Blockeds:Suspend原语3)静止就绪Readys→活动就绪Readya:Active原语4)静止阻塞Blockeds→活动阻塞Blockeda:Active原语引起挂起状态的原因:1)终端用户的请求。2)父进程的请求。3)负荷调节的需要。4)操作系统的需要。5)对换的需要。进程与进程之间的关系在进程有关系的时候有两种关系互斥(间接制约)同步(直接制约)信号量机制信号量机制进程同步的一个工具,使用它能够合理地分配CPU资源,管理进程一般地,我们对资源分配的时候,我们有以下的原则:同步时将信号量设置为0互斥时将信号量设置为1有几个进程设置几个信号量当等于0的时候,表示资源与进程数平衡当大于0的时候,表示资源多,临界资源有余当小于0的时候,表示资源少。记录型信号量机制wait()(P操作)申请资源,减少signal()(V操作)释放资源,增加信号量的应用利用信号量实现进程互斥为使多个进程能互斥地访问某临界资源,只需为该资源设置一互斥信号量mutex,并设其初始值为1,然后将各进程访问该资源的临界区CS置于wait(mutex)和signal(mutex)操作之间即可。利用信号量实现前趋关系还可利用信号量来描述程序或语句之间的前趋关系。设有两个并发执行的进程P1和P2。P1中有语句S1;P2中有语句S2。我们希望在S1执行后再执行S2。为实现这种前趋关系,只需使进程P1和P2共享一个公用信号量S,并赋予其初值为0,将signal(S)操作放在语句S1后面,而在S2语句前面插入wait(S)操作,即在进程P1中,用S1;signal(S); 在进程P2中,用wait(S);S2;进程通信进程通信,是指进程之间的信息交换,其所交换的信息量大小不一。信号量机制在通信方面的缺点:1) 效率低;2) 通信对用户不透明。高级进程通信是指用户可直接利用操作系统所提供的一组通信命令,高效地传送大量数据的一种通信方式。(往往我们都是使用高级进程通信的)进程的通信类型在进程通信的时候还有几种类型可分:共享存储器系统在共享存储器系统中,相互通信的进程共享某些数据结构或共享存储区,进程之间能够通过这些空间进行通信。消息传递系统消息传递机制是使用最广泛的一种进程间通信的机制(这种用的多)操作系统隐藏了通信的细节,简化了通信程序的编制。管道通信管道是指用于连接一个读进程和一个写进程以实现他们之间通信的一个共享文件,又名pipe文件。消息传递系统的实现方法又可以分几种:直接通信方式1) 发送原语:Send(Receiver,message);发送一个消息给接收进程2) 接收原语:Receive(Sender,message);接收Sender发来的消息间接通信方式通过信箱:指进程之间的通信,需要通过作为共享数据结构的实体。在间接通信的方式中,信箱作为的是一个实体私用信箱公有信箱共享信箱对于信箱而言也有几种关系:一对一关系多对一关系一对多关系多对多关系
可重定位分区分配动态分区很完美地在内存初次分配阶段解决了内存空间浪费的问题。但是,内存是需要重复利用的。随着任务的不断运行完毕,内存空间会被回收;同时,操作系统又会不断接收新的任务,内存空间会被分配。于是,当运行一个新任务时,只能从回收回来的分区上分配内存,一方面动态分区技术在一定程度上退化为固定分区,另一方面分配后余下的碎片(小块的内存空间)就被保留了下来,较小的内存空间被分配出去的可能性很小,造成了浪费(碎片)。最终,内存空间会存在很多小块的空闲空间,而不再有大块的空闲空间,于是当较大型的任务到达时,没有足够的空间供分配(尽管这些小块的空闲内存空间之和比任务所需的空间大)针对动态分区中碎片之和大于任务所需空间的情况,考虑对内存空间采用紧凑技术进行整理,将已进入内存的任务所占有的内存空间尽量搬到较低的地址,相对的,空闲碎片的会被换到了高地址空间。当所有进入内存的任务都被搬到较低的地址后,空闲碎片都被移动到了内存空间的高地址空间。于是,所有的碎片被整合成了一个大块,从而可以装载任务。这就是可重定位的动态分区。将碎片合成一大块---可重定位的动态分区实现1)动态重定位依靠硬件地址变换机构完成。2)地址重定位机构需要基地址寄存器(BR)和程序虚地址寄存器(VR)。指令或数据的虚地址(VA),也称为逻辑地址(LA)。内存地址MA(MA),也称物理地址(PA)实现逻辑地址到物理地址的转换,可以通过公式MA=(BR)十(VR)完成。图5-10所示为指令或数据的逻辑地址到物理地址的转换过程优点1)可以对内存进行非连续分配。2)动态重定位提供了实现虚拟存储器的基础。3)有利于程序段的共享。4 动态重定位分区的分配算法1)主干是动态分区的分配算法。2)在动态分区的基础上增加了紧凑技术。3)内存分配算法,如图5-11所示。基本分页存储管理上面的存储都是连续的内存分配技术,我们的分页存储是离散的与其花费巨大的代价搬家,不如离散地存储在这些碎片中一种离散存储的方法是面向系统的,将内存用户空间划分为大小相等(2nB,如4KB)的物理块;另一种方法是面向用户的,将内存空间划分为物理段,32位系统中,以高16位表示段号,低16位表示段内地址。这就是汇编语言中定义一个段时大小不可以超过64KB的根本原因。离散存储思想产生的原因:(1)紧凑技术的弊端尽管可重定位分区方式下的紧凑技术使得碎片得以利用,提高了内存空间的利用率。但这是以牺牲时间进行搬家而获得的,代价是很高的。(2)求变创新从内存连续分配的思想上突破、创新,提出离散的内存分配方式,从而使存在大量碎片和无足够大的连续空间供分配这一矛盾得以缓解。离散存储的基本概念将一个进程直接分散地装入到许多不相邻的分区中,而无须“紧凑”的分配方式,称为离散分配。离散分配的种类包括分页存储管理、分段存储管理和段页式存储管理。不具备页面对换功能的分页存储管理方式称为基本分页存储管理方式。分页存储管理分页存储管理的基本方法页面和物理块页面。页面大小。将用户作业的地址空间分成若干个大小相同的区域,称为页面或页,并为每个页从“0”开始编号;相应地,主存空间也分成与页大小相同的若干个存储块,或称为物理块或页框(frame),并且采用同样的方式为它们进行编号,从0开始:0块,1块,…,n-1块分页地址中的地址结构如下:对某特定机器,其地址结构是一定的。若给定一个逻辑地址空间中的地址为A,页面的大小为L,则页号P和页内地址d可按下式求得:页表在分页系统中,允许将进程的各个页离散地存储在内存的任一物理块中,为保证进程仍然能够正确地运行,即能在内存中找到每个页面所对应的物理块,系统又为每个进程建立了一张页面映像表,简称页表进程在运行期间,需要对程序和数据的地址进行变换,即将用户地址空间中的逻辑地址变换为内存空间中的物理地址,由于它执行的频率非常高,每条指令的地址都需要进行变换,因此需要采用硬件来实现。页表功能是由一组专门的寄存器来实现的。一个页表项用一个寄存器。由于页表是存放在内存中的,这使CPU在每存取一个数据时,都要两次访问内存。第一次是访问内存中的页表,从中找到指定页的物理块号,再将块号与页内偏移量W拼接,以形成物理地址。第二次访问内存时,才是从第一次所得地址中获得所需数据(或向此地址中写入数据)。因此,采用这种方式将使计算机的处理速度降低近1/2。可见,以此高昂代价来换取存储器空间利用率的提高,是得不偿失的。因此,我们采用:具有快表的地址变换机分段存储管理用户把自己的作业按照逻辑关系划分为若干个段,每个段都从0开始编址,并有自己的名字和长度。因此,程序员们都迫切地需要访问的逻辑地址是由段名(段号)和段内偏移量(段内地址)决定的,这不仅可以方便程序员编程,也可使程序非常直观,更具可读性。在实现对程序和数据的共享时,是以信息的逻辑单位为基础的。分页系统中的“页”只是存放信息的物理单位(块),并无完整的逻辑意义,这样,一个可被共享的过程往往可能需要占用数十个页面,这为实现共享增加了困难。信息保护同样是以信息的逻辑单位为基础的,而且经常是以一个过程、函数或文件为基本单位进行保护的。在实际应用中,往往存在着一些段,尤其是数据段,在它们的使用过程中,由于数据量的不断增加,而使数据段动态增长,相应地它所需要的存储空间也会动态增加。然而,对于数据段究竟会增长到多大,事先又很难确切地知道。对此,很难采取预先多分配的方法进行解决。
存储器的基础知识首先,一般的存储器我们就会认为它包含着三部分:寄存器速度最快,但是造价高主存储器速度次之,被通俗称为内存外存速度最慢,用于存储文件数据,因为上边两种一旦断电,数据就会丢失。这个用来做持久化存储的。因此,我们的存储器往往是使用三层结构的。程序的装入和链接在操作系统的角度而言,我们面对存储器就是面对程序的装入和连接一般地,用户程序向要在系统上运行,就要经历下面几个步骤:编译:对用户源程序进行遍历,形成若干个目标模块链接:将目标模块以及他们所需要的库函数链接在一起,形成完整的模块。装入:将模块装入内存绝对装入(麻烦)用户程序中使用的地址称为相对地址或逻辑地址或虚拟地址。在早期,当程序装入内存时,指令存储在内存中的物理地址与其逻辑地址完全相同.这种程序的装入方式称为绝对装入方式(Absolute Loading Mode)。可重定位装入方式(避免地址叠加)采用可重定位的装入方式(Relocation loading Mode)时,如果能够将不同的的程序装入到地址范围不同的物理内存空间,避免分配给各个程序的内存空间叠加(相交或“撞车”),就能实现将多个程序安全装入内存,使它们共享内存空间,从而支持多任务系统如果使用可重定位装入方式,就必须要解决:逻辑地址对物理地址之间的转换静态装入静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。在程序运行前一次性装入动态装入随着内存技术的发展,为了让更多的程序投入有限的内存并发运行,操作系统只将运行程序所必须的模块装入内存,其他诸如帮助系统等不常用的程序模块不装入内存,进而只装入能够使程序运行起来的那部分模块,更加节省了空间。在程序运行时,分批装入到内存中静态链接静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。和静态装入是一样的。动态链接载入时动态链接是在将功能模块读入内存时把动态库中调用到的相关模块的内容载入内存。载入时动态链接是分别载入,当把一个模块载入内存时检查有调用关系的模块并将其载入内存,比静态链接节省了许多开销。运行时动态链接则是把当前模块调用的模块推迟到调用的时候再载入。在运行时和运行前进行链接。连续分配存储管理方式操作系统将内存分为系统区和用户区两部分系统区仅提供给OS使用,通常是放在内存的低址部分,用户区指系统区以外的全部内存空间,提供给用户使用,通常在高地址部分。内存会被分成为系统区和用户区,比例为1:3。内存分配方式指的是对用户区的分配方式。原因是用户提交的任务只能在用户区运行。固定分区方式为了支持多道程序系统和分时系统,支持多个程序并发执行,引入了分区式存储管理。分区式存储管理是把内存分为一些大小相等或不等的分区,操作系统占用其中一个分区,其余的分区由应用程序使用,每个应用程序占用一个或几个分区。1 划分内存分区的方法1)分区大小相等内存分区大小一致,可能浪费空间;也可能空间不够。2)分区大小不等含较多的较小分区、适量的中等分区和少量的大分区。系统对内存的管理和控制通过数据结构—分区说明表进行。分区说明表说明各分区号、分区大小、起始地址和是否是空闲区(分区状态)。内存的分配释放、存储保护以及地址变换等都通过分区说明表进行。内存分区的分配:1)为了便于内存分配,通常将分区按大小进行排队,并为之建立一张分区表。2)分配3)回收动态分区固定分区的重大意义在于操作系统开始支持多任务。但是仍然存在内存浪费的问题。原因是内存分区在先,而运行程序在后,对于每个待运行的程序而言内存分区的大小并非量身定做。于是就出现了当运行某个任务时只能将该任务装载到内存空间中更大的分区。尽管浪费没有单一连续分区严重,但是内存的使用率仍然很低。如果内存分区的划分不是预先划定,而是根据所要运行的程序的大小分配内存,在内存分配阶段就不会出现内存碎片。于是,动态分区技术应运而生。1 基本问题:1)动态分区的基本思想:在作业执行前不直接建立分区,分区的建立是在作业的处理过程中进行的。且其大小可随作业或进程对内存的要求而改变。2)动态创建分区:在装入程序时按其初始要求分配,或在其执行过程中通过系统调用进行分配或改变分区大小,按需分配。3)采用的数据结构:内存分配表,由两个表格组成。一个是已分配区表,另一张是空闲区表.动态分区就有两张表来进行说明了。动态分区分配内存时从可用表或自由链中寻找空闲区的常用方法1)首次适应算法(First Ft Algorithm, FFA)首次适应法要求可用表或自由链按起始地址递增的次序排列。2)最佳适应算法(Best Fit Algorithm, BFA)要求按空闲区大小从小到大的次序组成空闲区可用表或自由链。3)最坏适应算法(Worst Fit Algorithm, WFA):要求空闲区按其大小递减的顺序组成空闲区可用表或自由链。碎片问题4)碎片问题经过一段时间的分配回收后,内存中存在很多很小的空闲块。它们每一个都很小,不足以满足分配要求; 但其总和满足分配要求。这些空闲块被称为碎片。5)碎片问题的解决通过在内存移动程序的技术将所有小的空闲区域合并为大的空闲区域,这种技术称为紧凑技术,也称为紧缩技术、紧致技术、浮动技术、搬家技术。
设备管理概述计算机系统的一个重要组成部分是I/O系统,在该系统中包括用于实现信息输入、输出和存储功能的设备和相应的设备控制器,在有些大型机中,还有I/O通道或I/O处理机。I/O设备是计算机系统中重要的资源,并且品种繁多,功能各异,因此设备管理是操作系统中最繁杂而且硬件紧密相关的部分。设备管理的对象是I/O设备,设备控制器和I/O通道。设备管理的基本任务是完成用户提出的I/O请求,提高I/O速度,改善I/O设备的利用率。设备管理的功能包括缓冲区管理、设备分配、设备处理、虚拟设备以及实现设备独立性等。IO系统计算机的I/O系统是主机和外设之间的数据传送系统,目前主要有总线型结构和通道型结构两种类型。与计算机相连接的外部设备有字符设备和块设备两种类型。前者一次只能传送一个字符,传送速度比较低;后者一次可以传送一个字符块,传送速度快,效率高如图6-1所示为总线型IO系统结构示意。1 I/O设备的类型1) 按传输速率分类,2) 按信息交换的单位分类,3) 按设备的共享属性分类,设备控制器设备控制器是计算机中的一个实体,其主要职责是控制一个或多个I/O设备,以实现I/O设备和计算机之间的数据交换。它是CPU与I/O设备之间的接口,它接收从CPU发来的命令,并去控制I/O设备工作,以使处理机从繁杂的设备控制事务中解脱出来。设备控制器是一个可编址的设备,当它仅控制一个设备时,它只有一个唯一的设备地址;若控制可连接多个设备时,则应含有多个设备地址,并使每一个设备地址对应一个设备。设备控制器的复杂性因不同设备而异,相差甚大,于是可把设备控制器分成两类:一类是用于控制字符设备的控制器,另一类是用于控制块设备的控制器。设备控制器的基本功能:(1)接收和识别命令(2)数据交换(3)标识和报告设备的状态(4)地址识别(5)数据缓冲(6)差错控制设备控制器的组成:(1)设备控制器与处理机的接口(2)设备控制器与设备的接口(3)I/O逻辑通道设备控制器减少了CPU对I/O的操作,但是当I/O设备较多时,设备控制器的数量也会增加,于是又退化到了类似早期设备较少时的情形,因而CPU的负担仍然很重。改进措施:为了实现速度匹配,并使CPU与I/O操作尽可能地并行工作,以提高CPU的利用率,IBM公司提出了“通道”的概念。-** 通道是一种通过执行通道程序管理I/O操作的控制器,它使CPU与I/O操作达到更高的并行度**。在采用通道的系统中,除了一般的机器指令系统外,系统还设置了供通道专用的一组通道指令,用通道指令编制成通道程序。通道的任务:**由CPU处理的I/O任务转由通道承担,从而把CPU从繁杂的I/O任务中解脱出来。 **具体地,使数据的传送独立于CPU,使有关对I/O操作的组织、管理及其结束处理尽量独立,以保证CPU有更多的时间去进行数据处理,从而建立独立的I/O操作。数据传输的过程:CPU↔内存↔主通道↔子通道↔设备控制器↔设备通道类型 :(1)字节多路通道字节多路通道可以连接多台慢速I/O设备,以交叉方式传送数据,即各设备轮流使用通道与主存进行数据传送,且每次只传送一个字节。因为每次数据传送仅占用了不同的设备各自分得的很短的时间片,所以大大提高了通道的利用率。子通道采用时间片轮转法调度,低速(2)数组选择通道数组选择通道可以连接多台快速I/O设备,但每次只能从中选择一台设备执行通道程序,进行主存与该设备之间的数据传送。当数据传送完后,才能选择另一台设备。在这种工作方式中,数据传送以成组方式进行,传送速率很高,多用于连接快速I/O设备。但因连接在选择通道上的多台设备,只能依次使用通道与主存传送数据,故设备之间不能并行工作,且整个通道的利用率不高**(3)数组多路通道 **数组多路通道综合了选择通道和字节多路通道的优点,它有多个子通道。结构上采用字节多路通道方式,可以像字节多路通道那样,执行多路通道程序,使所有子通道分时共享总通道;传输方式采用数组方式,可以像选择通道那样进行成组数据的传送,因而高速,利用率高。控制方式在早期的计算机系统中,由于无中断机构,处理机对I/O设备的控制,采取程序I/O方式(Programmed I/O方式)。在程序I/O方式中,由于CPU的高速性和I/O设备的低速性,致使CPU 的绝大部分时间都处于等待I/O设备完成数据I/O的循环测试中,造成对CPU的极大浪费。这种轮询的方式中,因为在CPU中无中断机构,使 I/O设备无法向CPU报告它已完成了一个字符的输入操作,所以需要CPU不断地测试I/O设备的状态。效率非常低,一次可能读取不到一个字节,如图6-7所示。中断驱动I/O控制方式程序I/O方式方式中,CPU主动挨个端口轮询是否有数据要处理,极大地浪费了CPU资源,从而使得整个系统的效率底下中断驱动I/O控制方式将CPU主动轮询的方式做出了改进,CPU处于被动的位置,等到有设备请求进行输入数据的时候,才去响应,从而提高了CPU的利用率。也就是说,引入中断之后,每当设备完成I/O操作,便以中断请求方式通知CPU,然后进行相应处理。但由于CPU直接控制输入输出操作,每传达一个单位信息。相对程序IO方式,CPU资源得以有效地利用。但是每传达一个单位信息都要发生一次中断,仍然消耗大量CPU时间,因此速度较低。直接存储器访问DMA控制方式DMA控制器(DMAC)是一种在系统内部转移数据的独特外设可以将其视为一种能够通过一组专用总线将内部和外部存储器与每个具有DMA能力的外设连接起来的控制器,如图6-8所示。它之所以属于外设,是因为它是在处理器的编程控制下来执行传输的。值得注意的是,通常只有数据流量较大(KBps或者更高)的外设才需要支持DMA能力,这些应用方面典型的例子包括视频、音频和网络接口DMA方式,在一定程度上提升了IO速度,把CPU从低效地IO操作中解脱出来,提高了CPU的利用率,从而提高了整个系统的效率。特点:1)数据传送的单位:数据块2)数据传输的方向:设备(控制器)←→内存2 DMA控制器的组成 :DMA控制器由命令/状态寄存器CR、内存地址寄存器MAR、数据寄存器DR和数据计数器DC组成。缓冲管理在设备管理部分,存在的主要矛盾是高速的CPU和低速I/O设备之间速度不匹配的问题。对于这种问题,处理的方法一般是增加缓冲。类似的,在计算机网络通信中,高速发送设备和低速接收设备之间为了防止数据的丢失,也会增加缓冲区。缓和CPU和I/O设备速度不匹配的矛盾;减少对CPU的中断频率,放宽对中断响应时间的限制;提高CPU和I/O设备之间的并行性。单缓冲双缓冲双缓冲,也成缓冲对换。在IO设备和CPU之间设置了两个缓冲区,使得它们能够交替访问不同的缓冲区,从而提高数据处理的效率。在输入与输出的速度基本匹配时可得到较好的效果,否则,由于缓冲区太少,不能缓解IO设备和CPU之间的速度压力。双缓冲退化为单缓冲。存在问题:当速度不匹配时效果退化到单缓冲机制的程度。解决办法:增加缓冲个数,按照循环链的方式组织缓冲区(3)进程同步在循环缓冲机制中,如果输入数据和读取数据的速度相当,则运行平稳;如果速度差异较大,则最出现所有缓冲区均为空而无数据可提取或者所有缓冲区均为满而无法输入数据的情况。因此,需要控制数据提取进程和数据输入进程的同步,防止出现与时间相关的错误。与时间相关的错误见第2章2.3.1节P28。1) Nexti指针追上Nextg指针输入速度>计算速度,系统受计算限制。2) Nextg指针追上Nexti指针输入速度<计算速度,系统受I/O限制。
NginxF5硬件:也是做负载均衡的Nginx:作用:静态文件处理动态负载均衡Nginx作为一个Web服务器,将静态文件由自己处理,动态文件转发为其他的服务器进行处理(Tomcat),这样就提高了效率。Nginx不能处理动态文件的。Nginx常见的问题:http://bbs.csdn.net/topics/390276707下载配置Nginx去官网上下载Nginx,我选择的是稳定版:http://nginx.org/en/download.html安装Nginx需要的依赖项:yum -y install gcc pcre pcre-devel zlib zlib-devel openssl openssl-devel执行configure要设置安装的路径是啥:make[1]: Leaving directory `/opt/nginx-1.10.3',出现这样的命令可以不用管他./configure --prefix=/opt/nginx我安装到了root下了。执行make&make installmake && make install启动报错的情况:/usr/local/nginx/sbin/nginx 启动nginx报错,信息如下: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) ….. 使用ps -ef|grep nginx,并未发现有nginx进程,有可能被其他进程占用,这时可以采用如下方式处理: 1. 查看80端口占用 netstat -ntpl 2. 杀掉占用80端口的进程 kill -9 $pid最终可以启动:Nginx简单配置内容来自于:http://www.jikexueyuan.com/course/1876_1.html?ss=1还有相关配置详解的博文:http://blog.csdn.net/xmtblog/article/details/42295181Nginx拦截请求的配置:# server 表示一个虚拟主机,一台服务器可配置多个虚拟主机 server { # 监听端口 listen 80; # 识别的域名 server_name localhost; # 一个关键设置,与url参数乱码问题有关 charset utf-8; #access_log logs/host.access.log main; #location表达式: #syntax: location [=|~|~*|^~|@] /uri/ { … } #分为两种匹配模式,普通字符串匹配,正则匹配 #无开头引导字符或以=开头表示普通字符串匹配 #以~或~* 开头表示正则匹配,~*表示不区分大小写 #多个location时匹配规则 #总体是先普通后正则原则,只识别URI部分,例如请求为/test/1/abc.do?arg=xxx #1. 先查找是否有=开头的精确匹配,即location = /test/1/abc.do {...} #2. 再查找普通匹配,以 最大前缀 为规则,如有以下两个location # location /test/ {...} # location /test/1/ {...} # 则匹配后一项 #3. 匹配到一个普通格式后,搜索并未结束,而是暂存当前结果,并继续再搜索正则模式 #4. 在所有正则模式location中找到第一个匹配项后,以此匹配项为最终结果 # 所以正则匹配项匹配规则受定义前后顺序影响,但普通匹配不会 #5. 如果未找到正则匹配项,则以3中缓存的结果为最终结果 #6. 如果一个匹配都没有,返回404 #location =/ {...} 与 location / {...} 的差别 #前一个是精确匹配,只响应/请求,所有/xxx类请求不会以前缀匹配形式匹配到它 #而后一个正相反,所有请求必然都是以/开头,所以没有其它匹配结果时一定会执行到它 #location ^~ / {...} ^~意思是非正则,表示匹配到此模式后不再继续正则搜索 #所有如果这样配置,相当于关闭了正则匹配功能 #因为一个请求在普通匹配规则下没得到其它普通匹配结果时,最终匹配到这里 #而这个^~指令又相当于不允许正则,相当于匹配到此为止 location / { root html; index index.html index.htm; # deny all; 拒绝请求,返回403 # allow all; 允许请求 } location /test/ { deny all; } location ~ /test/.+\.jsp$ { proxy_pass http://192.168.1.62:8080; } location ~ \.jsp$ { proxy_pass http://192.168.1.61:8080; } # 定义各类错误页 error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } # @类似于变量定义 # error_page 403 http://www.jikexueyuan.com这种定义不允许,所以利用@实现 error_page 403 @page403; location @page403 { proxy_pass http://http://www.jikexueyuan.com; } }80端口和Tomcat8080端口的问题:http://www.oschina.net/question/922543_89331http://blog.csdn.net/juan0728juan/article/details/53019997#我的简单配置修改如下:server { listen 80; server_name localhost; charset utf-8; #access_log logs/host.access.log main; #反向代理到8080端口,而页面不显示! proxy_set_header Host $host:$server_port; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #直接匹配网站根,通过域名访问网站首页比较频繁,使用这个会加速处理,官网如是说。 #这里是直接转发给后端应用服务器了,也可以是一个静态首页 # 第一个必选规则 location = / { proxy_pass http://www.zhongfucheng.site:8080/index.html; } # 第二个必选规则是处理静态文件请求,这是nginx作为http服务器的强项 # 有两种配置模式,目录匹配或后缀匹配,任选其一或搭配使用 #后缀匹配 location ~* \.(css|js|html|ico)$ { root /opt/apache-tomcat-7.0.82/webapps/zhongfucheng; } #第三个规则就是通用规则,用来转发动态请求到后端应用服务器 #非静态文件请求就默认是动态请求,自己根据实际把握 #毕竟目前的一些框架的流行,带.php,.jsp后缀的情况很少了 location / { proxy_pass http://www.zhongfucheng.site:8080/; }Nginx优化配置# nginx不同于apache服务器,当进行了大量优化设置后会魔术般的明显性能提升效果 # nginx在安装完成后,大部分参数就已经是最优化了,我们需要管理的东西并不多 #user nobody; #阻塞和非阻塞网络模型: #同步阻塞模型,一请求一进(线)程,当进(线)程增加到一定程度后 #更多CPU时间浪费到切换一,性能急剧下降,所以负载率不高 #Nginx基于事件的非阻塞多路复用(epoll或kquene)模型 #一个进程在短时间内可以响应大量的请求 #建议值 <= cpu核心数量,一般高于cpu数量不会带好处,也许还有进程切换开销的负面影响 worker_processes 4; #将work process绑定到特定cpu上,避免进程在cpu间切换的开销 worker_cpu_affinity 0001 0010 0100 1000 #8内核4进程时的设置方法 worker_cpu_affinity 00000001 00000010 00000100 10000000 # 每进程最大可打开文件描述符数量(linux上文件描述符比较广义,网络端口、设备、磁盘文件都是) # 文件描述符用完了,新的连接会被拒绝,产生502类错误 # linux最大可打开文件数可通过ulimit -n FILECNT或 /etc/security/limits.conf配置 # 理论值 系统最大数量 / 进程数。但进程间工作量并不是平均分配的,所以可以设置的大一些 worker_rlimit_nofile 655350 #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { # 并发响应能力的关键配置值 # 每个进程允许的最大同时连接数,work_connectins * worker_processes = maxConnection; # 要注意maxConnections不等同于可响应的用户数量, # 因为一般一个浏览器会同时开两条连接,如果反向代理,nginx到后端服务器的连接也要占用连接数 # 所以,做静态服务器时,一般 maxClient = work_connectins * worker_processes / 2 # 做反向代理服务器时 maxClient = work_connectins * worker_processes / 4 # 这个值理论上越大越好,但最多可承受多少请求与配件和网络相关,也可最大可打开文件,最大可用sockets数量(约64K)有关 worker_connections 500; # 指明使用epoll 或 kquene (*BSD) use epoll # 备注:要达到超高负载下最好的网络响应能力,还有必要优化与网络相关的linux内核参数 } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; # 关闭此项可减少IO开销,但也无法记录访问信息,不利用业务分析,一般运维情况不建议使用 access_log off # 只记录更为严重的错误日志,可减少IO压力 error_log logs/error.log crit; #access_log logs/access.log main; # 启用内核复制模式,应该保持开启达到最快IO效率 sendfile on; # 简单说,启动如下两项配置,会在数据包达到一定大小后再发送数据 # 这样会减少网络通信次数,降低阻塞概率,但也会影响响应及时性 # 比较适合于文件下载这类的大数据包通信场景 #tcp_nopush on; 在 #tcp_nodelay on|off on禁用Nagle算法 #keepalive_timeout 0; # HTTP1.1支持持久连接alive # 降低每个连接的alive时间可在一定程度上提高可响应连接数量,所以一般可适当降低此值 keepalive_timeout 30s; # 启动内容压缩,有效降低网络流量 gzip on; # 过短的内容压缩效果不佳,压缩过程还会浪费系统资源 gzip_min_length 1000; # 可选值1~9,压缩级别越高压缩率越高,但对系统性能要求越高 gzip_comp_level 4; # 压缩的内容类别 gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; # 静态文件缓存 # 最大缓存数量,文件未使用存活期 open_file_cache max=655350 inactive=20s; # 验证缓存有效期时间间隔 open_file_cache_valid 30s; # 有效期内文件最少使用次数 open_file_cache_min_uses 2; server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root html; index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} } # another virtual host using mix of IP-, name-, and port-based configuration # #server { # listen 8000; # listen somename:8080; # server_name somename alias another.alias; # location / { # root html; # index index.html index.htm; # } #} # HTTPS server # #server { # listen 443 ssl; # server_name localhost; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # ssl_session_cache shared:SSL:1m; # ssl_session_timeout 5m; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; # location / { # root html; # index index.html index.htm; # } #} include /usr/local/nginx/conf/vhosts/*.conf; }此部分我暂时没有进行修改!以后用到再来看把!Tomcat优化<?xml version='1.0' encoding='utf-8'?> <Server port="8005" shutdown="SHUTDOWN"> <Listener className="org.apache.catalina.startup.VersionLoggerListener" /> <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /> <Listener className="org.apache.catalina.core.JasperListener" /> <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" /> <GlobalNamingResources> <Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml" /> </GlobalNamingResources> <Service name="Catalina"> <!-- <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> --> <!-- protocol 启用 nio模式,(tomcat8默认使用的是nio)(apr模式利用系统级异步io) --> <!-- minProcessors最小空闲连接线程数--> <!-- maxProcessors最大连接线程数--> <!-- acceptCount允许的最大连接数,应大于等于maxProcessors--> <!-- enableLookups 如果为true,requst.getRemoteHost会执行DNS查找,反向解析ip对应域名或主机名--> <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" redirectPort="8443 maxThreads=“500” minSpareThreads=“100” maxSpareThreads=“200” acceptCount="200" enableLookups="false" /> <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> <Engine name="Catalina" defaultHost="localhost"> <Realm className="org.apache.catalina.realm.LockOutRealm"> <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> </Realm> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log." suffix=".txt" pattern="%h %l %u %t &quot;%r&quot; %s %b" /> </Host> </Engine> </Service> </Server>这部分也没有用到,等需要用到的时候再回来!配置成功在配置完之后,我总有一个疑问,Nginx是否已经帮我们处理了静态文件???于是我在网上找相关的问题,可没有相关的资料。。最后我就在想:Tomcat的端口我配置的是8080,静态资源在http://zhongfucheng.site:8080,在tomcat下肯定是可以获取得到的。如果静态资源在http://zhongfucheng.site就能够获取得到了,那么静态资源就已经被Nginx处理了!最后在网页上看源码果然是配置成功了!
使用SSH连接Linux环境经过十多天的时间,我的网站备案终于完成了…接下来我就收到了阿里云的邮件。它让我在网站首页的尾部添加备案号,貌似还需要去公安网站中再备案什么资料的。2017年11月20日19:06:26在图书馆并没有带身份证、于是就得放一下了。接下来,我就是要把我写的东西放在Linux下了。首先,我得连接Linux系统,通过阿里云的远程服务可以连接得到。密码可以在阿里云中设置,用户名是root,开始的时候我并不知道用户名是root,看了一下子文档才知道…然后阿里云文档中还说了可是使用ssh来连接,可是我根据它的教程怎么都连不上,我还以为是什么地方错误了。后来在ping一下公网的时候,发现根本ping不通…原来在使用SSH连接Linux的时候还需要配置安全组!不得不说,我的linux还真是不熟练,以此机会多接触一下linux才行。配置完安全组以后,我就可以带putty中使用SSH连接Linux了。2017年11月21日10:15:18 花了点时间去回顾了一下Linux的命令了,现在来搭建JavaEE环境了下载开发环境用到的tar包下载JDK去oracle官网找了一下,我的开发环境使用的是JDK1.7版本的,但是oracle官网找jdk1.7起来有点麻烦,我找到了教程:https://jingyan.baidu.com/album/9989c746064d46f648ecfe9a.html?picindex=5于是我就在http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html中找到了链接下载直接复制那个链接到迅雷下载就行了,那么就不用登陆了。我是下载了tar包..下载Mysql同样是在oracle官网中需找,找到对应的链接:https://dev.mysql.com/downloads/mysql/5.6.html#downloads下载Tomcat对于Tomcat下载就非常方便了,可以直接找到我开发环境使用的Tomcat7https://tomcat.apache.org/download-70.cgi也是同样下载tar包下载ElasticsearchElasticserach的下载还是非常方便的,提供搜索来进行下载。这里我就不贴链接了。直接去官网找就行了。或者去我的Elasticsearch学习记录中找。下载了2.3.3版本,因为我在windows开发的时候也是下载2.3.3版本的,就为了保持一致吧。解压并配置环境安装Java安装Java还是顺利的tar -zxvf jdk1.7.tar.gz 编辑配置文件 vim /etc/profile 在配置文件后添加下面的内容 export JAVA_HOME="/opt/jdk1.7.0_80" export PATH="$JAVA_HOME/bin:$PATH" 刷新配置文件 source /etc/profile测试:java -version安装TomcatTomcat版本是7安装Tomcat也是非常方便的,也是直接解压。在其中遇到了一个问题,启动tomcat时,一直卡在Deploying web application directory最后找到了解决方案:http://www.cnblogs.com/jtlgb/p/7063863.html开启和关闭Tomcat./startup.sh ./shutdown.sh查看Tomcat是否开启了的方法启动linux进入到tomcat安装目录 /apache-tomcat-6.0.26/bin下运行 #./startup.sh start 停止tomcat时运行命令: #./shutdown.sh start 远程查看tomcat的控制台 进入tomcat/logs/文件夹下 键入指令:tail -f catalina.out 就可以查看控制台了linux或者部分unix系统提供随机数设备是/dev/random 和/dev/urandom , 两个有区别,urandom安全性没有random高,但random需要时间间隔生成随机数。jdk默认调用random。 然后就很简单啦,找到对应的配置文件去修改就好了 找到jdk1.x.x_xx/jre/lib/security/Java.security文件,在文件中找到securerandom.source这个设置项,将其改为: securerandom.source=file:/dev/./urandom再次将Tomcat启动的时候,就可以顺利启动了。在windows下访问linux下的Tomcat安装MysqlMysql的版本是5.6.38摘要自http://blog.csdn.net/1099564863/article/details/51622709和https://www.cnblogs.com/idnf/p/4590818.html这篇是最后成功的:http://blog.csdn.net/wplblog/article/details/52179299安装Mysql就用了我非常多的时间、有的博客前面和后面的目录结构是对不上的、装了我好久….哎。下面就从各个博客中摘抄我成功安装Mysql的记录吧:安装 所需小环境 (此部分我不知道有什么用,以后知道了再来补吧)[root@localhost ~]# yum -y install make bison-devel ncures-devel libaio [root@localhost ~]# yum -y install libaio libaio-devel [root@localhost ~]# yum -y install perl-Data-Dumper [root@localhost ~]# yum -y install net-tools安装bison(这个我也安装了,感觉没什么用处)bison下载地址:http://www.gnu.org/software/bison/ [root@localhost ~]# tar zxvf bison-2.5.tar.gz [root@localhost ~]# cd bison-2.5 [root@localhost ~]# ./configure [root@localhost ~]# make [root@localhost ~]# make install解压刚刚下载的Mysql安装包(我是按照它的指示就在root的目录下安装) [root@localhost ~]#tar -zxvf mysql-5.6.38.tar.gz使用cmake安装,在博文中的目录被它变了,后面又不是一致的。后来我自己修改了才解决了问题。复制下面的内容cmake \-DCMAKE_INSTALL_PREFIX=/usr/local/mysql -DMYSQL_DATADIR=/usr/local/mysql/data -DSYSCONFDIR=/etc/my.cnf -DWITH_MYISAM_STORAGE_ENGINE=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_MEMORY_STORAGE_ENGINE=1 -DWITH_READLINE=1 -DMYSQL_UNIX_ADDR=/tmp/mysqld.sock -DMYSQL_TCP_PORT=3306 -DENABLED_LOCAL_INFILE=1 -DWITH_PARTITION_STORAGE_ENGINE=1 -DEXTRA_CHARSETS=all -DDEFAULT_CHARSET=utf8 -DDEFAULT_COLLATION=utf8_general_cimake 和安装make && make install配置mysql 检查系统是否已经有mysql用户,如果没有则创建 [root@localhost mysql-5.6.38]# cat /etc/passwd | grep mysql [root@localhost mysql-5.6.38]# cat /etc/group | grep mysql创建mysql用户(但是不能使用mysql账号登陆系统)[root@localhost mysql-5.6.32]# groupadd mysql -s /sbin/nologin [root@localhost mysql-5.6.32]# useradd -g mysql mysql修改权限[root@localhost mysql-5.6.32]# chown -R mysql:mysql /usr/local/mysql切换到mysql目录cd /usr/local/mysql设置权限等东西chown -R mysql:mysql . (#这里最后是有个.的大家要注意# 为了安全安装完成后请修改权限给root用户) scripts/mysql_install_db --user=mysql (先进行这一步再做如下权限的修改) chown -R root:mysql . (将权限设置给root用户,并设置给mysql组, 取消其他用户的读写执行权限,仅留给mysql "rx"读执行权限,其他用户无任何权限) chown -R mysql:mysql ./data (数据库存放目录设置成mysql用户mysql组) chmod -R ug+rwx . (赋予读写执行权限,其他用户权限一律删除仅给mysql用户权限)将mysql的配置文件拷贝到/etc cp support-files/my-default.cnf /etc/my.cnf修改my.cnf# vi /etc/my.cnf[mysqld] 下面添加:datadir也是被我修改过的。不修改就启动不了内容如下:user=mysql datadir=/usr/local/mysql/data default-storage-engine=MyISAM启动mysql(还是在mysql的目录下进行的)cp support-files/mysql.server /etc/init.d/mysql service mysql start修改root的密码chkconfig --add mysql 修改密码 cd 切换到mysql所在目录 # cd /usr/local/mysql ./bin/mysqladmin -u root password最后设置新的密码即可!
url自动携带jsessionid在我使用浏览器收藏了我写的网站的时候,有的时候会访问不了页面。看了一下原因,是由于url携带了jsessionId,我就奇怪为啥会自动携带jsession了。我分析是由“记住我“功能引起的这个bug,于是我就去查找了一下Shiro的相关资料。找到了解决方案:http://blog.csdn.net/yyf314922957/article/details/51038322我把Shiro的版本升级了,加入了配置文件信息:<!--Shiro与Spring整合--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency> <!--Shiro与ehcache整合--> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache-core</artifactId> <version>2.5.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.3.2</version> </dependency> <!--javaEE基本API--> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> </dependency><!-- 会话管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!--不允许url重写sessionId--> <property name="sessionIdUrlRewritingEnabled" value="false" /> <!-- session的失效时长,单位毫秒 --> <property name="globalSessionTimeout" value="800000"/> <!-- 删除失效的session --> <property name="deleteInvalidSessions" value="true"/> </bean>Shiro拦截器比SpringMVC拦截器先要执行经过一段时间的使用,发现自动登陆的功能还是没有做好。打了debug才发现原来Shiro拦截器比SpringMVC拦截器先要执行,这意味着我的SpringMVC实现自动登陆的代码是没用的!!后来自己重新定义一个Filter放在Shiro拦截器前面,在访问的时候就报错了。也就是说Shiro拦截器的优先级是最高的!那也就是说:我的自动登陆逻辑不能放在拦截器或者过滤器中!最后,我的自动登陆逻辑就写在了表单认证器上修复在线聊天名字冲突问题在测试的时候,发现获取了当前用户的名字,而不是发送者的名字。原来在逻辑上出错了,当时获取的时候是这样子:info: $("#userNickname").val()+":"+ txt, //文字这样是不合理的,因为无论是谁发的,都是获取得到自己的名字。最后修改成:在发送端就指明用户了,在获取的时候直接把发送的数据获取就好了。message: $("#userNickname").val() + ":" + $("#inputText").val() info: txt, //文字Mysql连接失效问题2017年12月29日20:48:42其实这个问题在前一段时间我已经知道了,只是我刷新页面,它就不会出现这个问题了。我前一段时间又忙着期末考,于是就没管他了。今天考完期末考了,我又回来了….上网上查找了一些资源,出现这个异常的主要原因是:“我的Mysql有效时间少于我配置C3P0连接池的时间”,由于连接池的连接是有效的,但是Mysql已经失效了“在wait_timeout时间里,mysql的connection处于等待状态,过了这时间mysql5就关闭了,但是java application的连接池仍然有合法的connection,当你再操作数据库时,就会出现这样的问题。因此就抛出了这个异常…参考资料:https://www.cnblogs.com/chihirotan/p/6253175.html主要是修改C3P0的配置
结果:六、猴子吃桃子问题猴子摘下了n个桃子,当天吃掉一半多一个,第二天也是吃掉剩下桃子的一半多一个,到了第十天,桃子只剩下了1个。问:猴子第一天摘了多少个桃子思路:假设当天有n个桃子,它是前一天桃子的一半少1个,f(n - 1) = f(n)/2 - 1,我们就可以推出当天桃子的个数:根据递推公式:f(n) = 2 * f(n - 1) + 2用递归和循环都可解决:递归方式:/** * 猴子吃桃问题 * @param x 天数 */ public static int monkeyQue(int x) { if (x <= 0) { return 0; } else if (x == 1) { return 1; } else { return 2 * monkeyQue(x - 1) + 2; } }循环方式:int x = 1; for (int i = 1; i <= 9; i++) { x = (x + 1) * 2; }结果:七、计算单词的个数输入一段字符,计算出里面单词的个数,单词之间用空格隔开 ,一个空格隔开,就代表着一个单词了思路:把字符遍历一遍,累计由空格串转换为非空格串的次数,次数就是单词的个数定义一个标志性变量flag,0表示的是空格状态,1表示的是非空格状态/** * 输入一段字符,计算出里面单词的个数 * * @param str 一段文字 */ public static int countWord(String str) { // 0 表示空格状态,1 表示非空格状态 int flag = 0; // 单词次数 int num = 0; for (int i = 0; i < str.length(); i++) { if (String.valueOf(str.charAt(i)).equals(" ") ) { flag = 0; } else if (flag == 0) { num++; flag = 1; } } return num ; }结果:八、判断字母是否完全一样给定两个字符串s和t,判断这两个字符串中的字母是不是完全一样(顺序可以不一样)思路:遍历这两个字符串,用每个字符减去'a',将其分别存入到数组中去,随后看这两个数组是否相等即可要点:'c'-'a'=2即可计算出存储的位置,如果有多个,则+1即可,后面我们来比较数组大小代码实现:/** * 给定两个字符串s和t,判断这两个字符串中的字母是不是完全一样(顺序可以不一样) */ public static void isAnagram() { //分别存储字符串的字符 char[] array1 = new char[26]; char[] array2 = new char[26]; String s1 = "pleasefollowthewechatpublicnumber"; String s2 = "pleowcnumberthewechatpubliasefoll"; for (int i = 0; i < s1.length(); i++) { char value = s1.charAt(i); // 算出要存储的位置 int index = value - 'a'; array1[index]++; } for (int i = 0; i < s2.length(); i++) { char value = s2.charAt(i); // 算出要存储的位置 int index = value - 'a'; array2[index]++; } for (int i = 0; i < 26; i++) { if (array1[i] != array2[i]) { System.out.println("不相同"); return; } } System.out.println("相同"); }结果:九、判断一个数是不是2的某次方判断一个数是不是2的某次方思路:除2取余数,直至余数不为0【针对2的倍数这种情况】,看是不是等于1就可以判断是不是2的某次方了/** * 判断是否是2的某次方 */ public static void isPowerOfTwo() { int num = 3; if (num == 0) { System.out.println("不是"); } while (num % 2 == 0) { num = num / 2; } if (num == 1) { System.out.println("是"); } else { System.out.println("不是"); } }结果:这题还有另一种解决方式,就是位运算:2的n次方都有一个特点,二进制都是1000000如果 **2的n次方的二进制-1和2的n次方二进制做按位与运算,那么得出的结果肯定是0 **if(num <= 0){ System.out.println("不是"); } else if(num == 1){ System.out.println("是"); } else{ if( (num & (num-1) ) == 0){ System.out.println("是"); } else{ System.out.println("不是"); } }十、判断一个数字是不是ugly number判断一个数字是不是ugly number(分解出来的质因数只有2、3、5这3个数字)思路:如果是由2,3,5组成的,那么这个数不断除以2,3,5,最后得出的是1,这个数就是纯粹用2,3,5组成的跟之前判断该数是否2的某次方是一样的思路~代码:/** * 判断一个数字是不是ugly number(分解出来的质因数只有2、3、5这3个数字) * @param num */ public static void isUgly(int num) { if (num <= 0) { System.out.println("不是"); } else { while (num % 2 == 0) { num = num / 2; } while (num % 3 == 0) { num = num / 3; } while (num % 5 == 0) { num = num / 5; } if (num == 1) { System.out.println("是"); } else { System.out.println("是"); } } }结果:总结没错,你没看错,简单的小算法也要总结!其实我觉得这些比较简单的算法是有"套路"可言的,你如果知道它的套路,你就很容易想得出来,如果你不知道它的套路,那么很可能就不会做了(没思路)。积累了一定的"套路"以后,我们就可以根据经验来推断,揣摩算法题怎么做了。举个很简单的例子:乘法是在加法的基础之上的,那乘法我们是怎么学的?背(积累)出来的,9*9乘法表谁没背过?比如看到2+2+2+2+2,会了乘法(套路)以后,谁还会慢慢加上去。看见了5个2,就直接得出2*5了1-n阶乘之和求n的阶乘就用1*2*3*4*...n,实际上就是一个循环的过程,求和就套个sum变量即可!获取二维数组每列最小的值外层循环控制列数,内层循环控制行数,这就是遍历每列的方法~求"1!+4!(2的平方)+9!(3的平方)+…+n的值先求平方,再求阶乘,最后套个sum变量数组对角线元素之和行和列的位置相等,即是对角线上的元素打印杨辉三角形找出杨辉三角形的规律:第一行、第一列和列值等于行值时上的元素都是1,其余的都是头上的值加头上的左边的值猴子吃桃子问题根据条件,我们可以推算出前一天桃子,进而推出当天桃子(规律)。猴子都是在相等的条件(剩下桃子的一半多一个),因此就应该想到循环或者递归计算单词的个数利用每个单词间会有个空格的规律,用变量来记住这个状态(字母与空格)的转换,即可计算出单词的个数!判断字母是否完全一样将每个字母都分别装载到数组里面去,'c-a'就是字母c在数组的位置了(也就是2)。由于字母出现的次数不唯一,因此我们比较的是数组的值(如果出现了两次,那么值为2,如果出现了3次,那么值为3)。只要用于装载两个数组的值都吻合,那么字母就是一样!判断一个数是不是2的某次方最佳方案:2的某次方在二进制都有个特点:10000(n个0)--->ps:程序员的整数~……….那么比这个数少一位的二进制肯定是01111,它俩做&运算,那么肯定为0。用这个特性就非常好判断该数是否是2的某次方了次方案:2的某次方的数不断缩小(只要number % 2 == 0就可以缩小,每次number / 2),最后的商必然是1。判断一个数字是不是ugly number分解出来的质因数只有2、3、5这3个数字,这题其实就是判断该数是否为2的某次方的升级版。将这个数不断缩小(只要number%2||%3||%5==0,每次number / 2 | / 3 /5),最后的商必然是1。
response、request对象Tomcat收到客户端的http请求,会针对每一次请求,分别创建一个代表请求的request对象、和代表响应的response对象既然request对象代表http请求,那么我们获取浏览器提交过来的数据,找request对象即可。response对象代表http响应,那么我们向浏览器输出数据,找response对象即可。什么是HttpServletResponse对象?http响应由状态行、实体内容、消息头、一个空行组成。HttpServletResponse对象就封装了http响应的信息。HttpServletResponse的应用调用getOutputStream()方法向浏览器输出数据调用getOutputStream()方法向浏览器输出数据,getOutputStream()方法可以使用print()也可以使用write(),它们有什么区别呢?我们试验一下。代码如下//获取到OutputStream流 ServletOutputStream servletOutputStream = response.getOutputStream(); //向浏览器输出数据 servletOutputStream.print("aaaa");成功输出,好像没什么毛病。我们试着输出中文试试//获取到OutputStream流 ServletOutputStream servletOutputStream = response.getOutputStream(); //向浏览器输出数据 servletOutputStream.print("中国!");出异常了!!!为什么会出现异常呢?在io中我们学过,outputStream是输出二进制数据的,print()方法接收了一个字符串,print()方法要把“中国”改成二进制数据,Tomcat使用IOS 8859-1编码对其进行转换,“中国”根本对ISO 8859-1编码不支持。所以出现了异常我们再看看write()方法,先向浏览器输出英文数据response.getOutputStream().write("aaa".getBytes());没有问题再试试输出中文数据response.getOutputStream().write("你好呀我是中国".getBytes());貌似也没有问题。为什么使用write()方法能够正常向浏览器输出中文呢?"你好呀我是中国".getBytes()这句代码在转成byte[]数组的时候默认查的是gb2312编码,而"你好呀我是中国"支持gb2312编码,所以可以正常显示出来。但是,程序要实现通用性,应该使用的是UTF-8编码,我们在字符串转换成字节数组时指定UTF-8编码,看看会怎么样。response.getOutputStream().write("你好呀我是中国".getBytes("UTF-8"));好的,成功把它搞成乱码了!!!为什么它变成了乱码呢?原因是这样的:我在向服务器输出的中文是UTF-8编码的,而浏览器采用的是GBK,GBK想显示UTF-8的中文数据,不乱码才怪呢!既然如此,我将浏览器的编码改成UTF-8试试。乱码问题又解决了。可是,每次编写UTF-8程序时都要去网页上改编码格式吗?这样明显不可能的。既然HTTP响应有对浏览器说明回送数据是什么类型的消息头,那么HttpServletResponse对象就应该有相对应的方法告诉浏览器回送的数据编码格式是什么于是乎就去查找Servlet API,找到了设置消息头的方法//设置头信息,告诉浏览器我回送的数据编码是utf-8的 response.setHeader("Content-Type", "text/html;charset=UTF-8"); response.getOutputStream().write("你好呀我是中国".getBytes("UTF-8"));浏览器在显示数据时,自动把页面的编码格式置换成UTF-8,乱码问题也解决了另外,除了使用HttpServletResponse对象设置消息头的方法,我可以使用html的标签模拟一个http消息头下面是代码://获取到servletOutputStream对象 ServletOutputStream servletOutputStream = response.getOutputStream(); //使用meta标签模拟http消息头,告诉浏览器回送数据的编码和格式 servletOutputStream.write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'>".getBytes()); servletOutputStream.write("我是中国".getBytes("UTF-8"));乱码问题也可以解决调用getWriter()方法向浏览器输出数据对于getWriter()方法而言,是Writer的子类,那么只能向浏览器输出字符数据,不能输出二进制数据使用getWriter()方法输出中文数据,代码如下://获取到printWriter对象 PrintWriter printWriter = response.getWriter(); printWriter.write("看完博客点赞!");喜闻可见的事又出现了,我又出现乱码了。为什么出现乱码了呢?由于Tomcat是外国人的写,Tomcat默认的编码是ISO 8859-1,当我们输出中文数据的时候,Tomcat会依据ISO 8859-1码表给我们的数据编码,中文不支持这个码表呀,所以出现了乱码既然如此,我设置一下编码不就好了吗,代码如下://原本是ISO 8859-1的编码,我设置成UTF-8 response.setCharacterEncoding("UTF-8"); //获取到printWriter对象 PrintWriter printWriter = response.getWriter(); printWriter.write("看完博客点赞!");我再访问了一下,我的天!看起来更乱了!为什么乱码问题还没有解决?细心的朋友会发现,我只是在中文转换的时候把码表设置成UTF-8,但是浏览器未必是使用UTF-8码表来显示数据的呀好的,我们来看看浏览器的编码格式,果然,浏览器使用GB2312显示UTF-8的数据,不乱码才怪呢这个问题我们在上面已经是有两种方法解决了【使用标签模拟消息头、设置消息头】,Servlet还提供了一个方法给我们//设置浏览器用UTF-8编码显示数据 response.setContentType("text/html;charset=UTF-8");好的,我们再来访问一下既然Servlet有那么多方法解决乱码问题,是不是有一种是最简便的呢?没错!下面这个方法是最简便的,它不仅设置浏览器用UTF-8显示数据,内部还把中文转码的码表设置成UTF-8了,也就是说,response.setContentType("text/html;charset=UTF-8");把response.setCharacterEncoding("UTF-8")的事情也干了!使用getWriter()显示中文数据,只需要一个方法就搞掂了!//设置浏览器用UTF-8编码显示数据, response.setContentType("text/html;charset=UTF-8"); //获取到printWriter对象 PrintWriter printWriter = response.getWriter(); printWriter.write("看完博客点赞!");
前言本章节主要讲解Druid数据库连接池,为什么要学Druid数据库连接池呢??我的知识储备数据库连接池有两种->C3P0,DBCP,可是现在看起来并不够用阿~当时学习C3P0的时候,觉得这个数据库连接池是挺强大的。看过的一些书上也是多数介绍了这两种数据库连接池,自己做的Demo也是使用C3P0。可是现在看起来这两种都不够了~业界发展得真快呀上面的我就没有打码了,都是一些热心的开发者评论,正因为他们的评论才促使我会去学更好的东西,也希望大家多多指点~于是乎,我就花一点时间去学习Druid数据库连接池了…如果有错的地方往指正~~Druid数据库连接池是阿里的,因此文档是有中文版本的,英语不好学起来也不用那么头疼.一、Druid介绍Druid一般的用处有两个:替代C3P0、DBCP数据库连接池(因为它的性能更好)自带监控页面,实时监控应用的连接池情况所以本文主要是使用Druid作为数据库连接池并且使用一下实时监控应用,做个入门学习~二、搭建Druid环境由于简化配置,我就直接实用SpringBoot和SpringData JPA的方式来搭建一个Druid的Demo了~~~2.1引入pom<dependencies> <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> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> <scope>test</scope> </dependency> </dependencies>2.2Druid默认的配置配置数据源的信息(Druid),和JPA相关配置~# 数据库访问配置 # 主数据源,默认的 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/druid spring.datasource.username=root spring.datasource.password=root # 下面为连接池的补充设置,应用到上面所有数据源中 # 初始化大小,最小,最大 spring.datasource.initialSize=5 spring.datasource.minIdle=5 spring.datasource.maxActive=20 # 配置获取连接等待超时的时间 spring.datasource.maxWait=60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 spring.datasource.timeBetweenEvictionRunsMillis=60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=SELECT 1 FROM DUAL spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false # 打开PSCache,并且指定每个连接上PSCache的大小 spring.datasource.poolPreparedStatements=true spring.datasource.maxPoolPreparedStatementPerConnectionSize=20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 spring.datasource.filters=stat,wall,log4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 合并多个DruidDataSource的监控数据 #spring.datasource.useGlobalDataSourceStat=true #JPA配置 spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jackson.serialization.indent_output=true更多的配置要去看官方文档了~不过这里一般就够用了。2.3配置监控页面Druid的监控统计功能是通过filter-chain扩展实现,如果你要打开监控统计功能,配置StatFilter配置druid数据源状态监控,配置一个拦截器和一个Servlet即可~package com.example.demo; /** * Created by ozc on 2018/3/26. * * @author ozc * @version 1.0 */ import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * druid 配置. * <p> * 这样的方式不需要添加注解:@ServletComponentScan * * @author Administrator */ @Configuration public class DruidConfiguration { /** * 注册一个StatViewServlet * * @return */ @Bean public ServletRegistrationBean DruidStatViewServle2() { //org.springframework.boot.context.embedded.ServletRegistrationBean提供类的进行注册. ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); //添加初始化参数:initParams //白名单: servletRegistrationBean.addInitParameter("allow", "127.0.0.1"); //IP黑名单 (存在共同时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page. servletRegistrationBean.addInitParameter("deny", "192.168.1.73"); //登录查看信息的账号密码. servletRegistrationBean.addInitParameter("loginUsername", "admin2"); servletRegistrationBean.addInitParameter("loginPassword", "123456"); //是否能够重置数据. servletRegistrationBean.addInitParameter("resetEnable", "false"); return servletRegistrationBean; } /** * 注册一个:filterRegistrationBean * * @return */ @Bean public FilterRegistrationBean druidStatFilter2() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter()); //添加过滤规则. filterRegistrationBean.addUrlPatterns("/*"); //添加不需要忽略的格式信息. filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } }2.4JPA测试POJO:@Entity public class User implements Serializable { /** * serialVersionUID. */ private static final long serialVersionUID = 1L; /** * 主键. */ @Id @GeneratedValue private String id; /** * 用户名. */ private String userName = ""; /** * 手机号码. */ private String mobileNo = ""; /** * 邮箱. */ private String email = ""; /** * 密码. */ private String password = ""; /** * 用户类型. */ private Integer userType = 0; /** * 注册时间. */ private Date registerTime = new Date(); /** * 所在区域. */ private String region = ""; /** * 是否有效 0 有效 1 无效. */ private Integer validity = 0; /** * 头像. */ private String headPortrait = ""; }Controller:@RestController public class UserController { @Autowired private UserRepos userRepos; @RequestMapping(value="saveUser") public User saveUser(){ return userRepos.save(new User()); } @RequestMapping(value="/findByUserName") public List<User> findByUserName(String userName){ return userRepos.findByUserName(userName); } @RequestMapping(value="findByUserNameLike") public List<User> findByUserNameLkie(String userName){ return userRepos.findByUserNameLike(userName); } @RequestMapping(value="findByPage") public Page<User> findByPage(Integer userType){ return userRepos.findByUserType(userType, new PageRequest(1, 5)); } }Repository:public interface UserRepos extends JpaRepository<User, String> { /** * 通过用户名相等查询 * * @param userName 用户名 * @return */ List<User> findByUserName(String userName); /** * 通过名字like查询 * * @param userName 用户名 * @return */ List<User> findByUserNameLike(String userName); /** * 通过用户名和手机号码查询 * * @param userName 用户名 * @param mobileNo 手机号码 * @return */ User findByUserNameAndMobileNo(String userName, String mobileNo); /** * 根据用户类型,分页查询 * * @param userType 用户类型 * @param pageable * @return */ Page<User> findByUserType(Integer userType, Pageable pageable); /** * 根据用户名,排序查询 * * @param userName 用户名 * @param sort * @return */ List<User> findByUserName(String userName, Sort sort); }在页面上访问:http://localhost:8080/findByUserName?userName=Java3y结果:三、最后本文只是简单的对Druid进行入门,Druid是一个非常好的开源数据库连接池框架,更多的资料可看GitHub的文档。
三、回顾URL拦截我们在学习的路途上也是使用过几次URL对权限进行拦截的当时我们做了权限的增删该查的管理系统,但是在权限表中是没有把资源添加进去,我们使用的是Map集合来进行替代的。http://blog.csdn.net/hon_3y/article/details/61926175随后,我们学习了动态代理和注解,我们也做了一个基于注解的拦截在Controller得到service对象的时候,service工厂返回的是一个动态代理对象回去Controller拿着代理对象去调用方法,代理对象就会去解析该方法上是否有注解如果有注解,那么就需要我们进行判断该主体是否认证了,如果认证了就判断该主体是否有权限当我们解析出该主体的权限和我们注解的权限是一致的时候,才放行!http://blog.csdn.net/hon_3y/article/details/70767050流程:这里写图片描述3.1认证的JavaBean我们之前认证都是放在默认的Javabean对象上的,现在既然我们准备学Shiro了,我们就得专业一点,弄一个专门存储认证信息的JavaBean/** * 用户身份信息,存入session 由于tomcat将session会序列化在本地硬盘上,所以使用Serializable接口 * * @author Thinkpad * */ public class ActiveUser implements java.io.Serializable { private String userid;//用户id(主键) private String usercode;// 用户账号 private String username;// 用户名称 private List<SysPermission> menus;// 菜单 private List<SysPermission> permissions;// 权限 public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getUsercode() { return usercode; } public void setUsercode(String usercode) { this.usercode = usercode; } public String getUserid() { return userid; } public void setUserid(String userid) { this.userid = userid; } public List<SysPermission> getMenus() { return menus; } public void setMenus(List<SysPermission> menus) { this.menus = menus; } public List<SysPermission> getPermissions() { return permissions; } public void setPermissions(List<SysPermission> permissions) { this.permissions = permissions; } }认证的服务@Override public ActiveUser authenticat(String userCode, String password) throws Exception { /** 认证过程: 根据用户身份(账号)查询数据库,如果查询不到用户不存在 对输入的密码 和数据库密码 进行比对,如果一致,认证通过 */ //根据用户账号查询数据库 SysUser sysUser = this.findSysUserByUserCode(userCode); if(sysUser == null){ //抛出异常 throw new CustomException("用户账号不存在"); } //数据库密码 (md5密码 ) String password_db = sysUser.getPassword(); //对输入的密码 和数据库密码 进行比对,如果一致,认证通过 //对页面输入的密码 进行md5加密 String password_input_md5 = new MD5().getMD5ofStr(password); if(!password_input_md5.equalsIgnoreCase(password_db)){ //抛出异常 throw new CustomException("用户名或密码 错误"); } //得到用户id String userid = sysUser.getId(); //根据用户id查询菜单 List<SysPermission> menus =this.findMenuListByUserId(userid); //根据用户id查询权限url List<SysPermission> permissions = this.findPermissionListByUserId(userid); //认证通过,返回用户身份信息 ActiveUser activeUser = new ActiveUser(); activeUser.setUserid(sysUser.getId()); activeUser.setUsercode(userCode); activeUser.setUsername(sysUser.getUsername());//用户名称 //放入权限范围的菜单和url activeUser.setMenus(menus); activeUser.setPermissions(permissions); return activeUser; }Controller处理认证,如果身份认证成功,那么把认证信息存储在Session中@RequestMapping("/login") public String login(HttpSession session, String randomcode,String usercode,String password)throws Exception{ //校验验证码,防止恶性攻击 //从session获取正确验证码 String validateCode = (String) session.getAttribute("validateCode"); //输入的验证和session中的验证进行对比 if(!randomcode.equals(validateCode)){ //抛出异常 throw new CustomException("验证码输入错误"); } //调用service校验用户账号和密码的正确性 ActiveUser activeUser = sysService.authenticat(usercode, password); //如果service校验通过,将用户身份记录到session session.setAttribute("activeUser", activeUser); //重定向到商品查询页面 return "redirect:/first.action"; }身份认证拦截器//在执行handler之前来执行的 //用于用户认证校验、用户权限校验 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //得到请求的url String url = request.getRequestURI(); //判断是否是公开 地址 //实际开发中需要公开 地址配置在配置文件中 //从配置中取逆名访问url List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL"); //遍历公开 地址,如果是公开 地址则放行 for(String open_url:open_urls){ if(url.indexOf(open_url)>=0){ //如果是公开 地址则放行 return true; } } //判断用户身份在session中是否存在 HttpSession session = request.getSession(); ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser"); //如果用户身份在session中存在放行 if(activeUser!=null){ return true; } //执行到这里拦截,跳转到登陆页面,用户进行身份认证 request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response); //如果返回false表示拦截不继续执行handler,如果返回true表示放行 return false; }授权拦截器//在执行handler之前来执行的 //用于用户认证校验、用户权限校验 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //得到请求的url String url = request.getRequestURI(); //判断是否是公开 地址 //实际开发中需要公开 地址配置在配置文件中 //从配置中取逆名访问url List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL"); //遍历公开 地址,如果是公开 地址则放行 for(String open_url:open_urls){ if(url.indexOf(open_url)>=0){ //如果是公开 地址则放行 return true; } } //从配置文件中获取公共访问地址 List<String> common_urls = ResourcesUtil.gekeyList("commonURL"); //遍历公用 地址,如果是公用 地址则放行 for(String common_url:common_urls){ if(url.indexOf(common_url)>=0){ //如果是公开 地址则放行 return true; } } //获取session HttpSession session = request.getSession(); ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser"); //从session中取权限范围的url List<SysPermission> permissions = activeUser.getPermissions(); for(SysPermission sysPermission:permissions){ //权限的url String permission_url = sysPermission.getUrl(); if(url.indexOf(permission_url)>=0){ //如果是权限的url 地址则放行 return true; } } //执行到这里拦截,跳转到无权访问的提示页面 request.getRequestDispatcher("/WEB-INF/jsp/refuse.jsp").forward(request, response); //如果返回false表示拦截不继续执行handler,如果返回true表示放行 return false; }拦截器配置:<!--拦截器 --> <mvc:interceptors> <mvc:interceptor> <!-- 用户认证拦截 --> <mvc:mapping path="/**" /> <bean class="cn.itcast.ssm.controller.interceptor.LoginInterceptor"></bean> </mvc:interceptor> <mvc:interceptor> <!-- 授权拦截 --> <mvc:mapping path="/**" /> <bean class="cn.itcast.ssm.controller.interceptor.PermissionInterceptor"></bean> </mvc:interceptor> </mvc:interceptors>四、什么是Shiroshiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。Shiro架构:这里写图片描述subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。securityManager:安全管理器,主体进行认证和授权都 是通过securityManager进行。authenticator:认证器,主体进行认证最终通过authenticator进行的。authorizer:授权器,主体进行授权最终通过authorizer进行的。sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。
二、Spring与Shiro整合2.1导入jar包shiro-web的jar、shiro-spring的jarshiro-code的jar这里写图片描述2.2快速入门shiro也通过filter进行拦截。filter拦截后将操作权交给spring中配置的filterChain(过虑链儿)在web.xml中配置filter<!-- shiro的filter --> <!-- shiro过虑器,DelegatingFilterProxy通过代理模式将spring容器中的bean和filter关联起来 --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <!-- 设置true由servlet容器控制filter的生命周期 --> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> <!-- 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean--> <init-param> <param-name>targetBeanName</param-name> <param-value>shiroFilter</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>在applicationContext-shiro.xml 中配置web.xml中fitler对应spring容器中的bean。<!-- web.xml中shiro的filter对应的bean --> <!-- Shiro 的Web过滤器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 --> <property name="loginUrl" value="/login.action" /> <!-- 认证成功统一跳转到first.action,建议不配置,shiro认证成功自动到上一个请求路径 --> <!-- <property name="successUrl" value="/first.action"/> --> <!-- 通过unauthorizedUrl指定没有权限操作时跳转页面--> <property name="unauthorizedUrl" value="/refuse.jsp" /> <!-- 自定义filter配置 --> <property name="filters"> <map> <!-- 将自定义 的FormAuthenticationFilter注入shiroFilter中--> <entry key="authc" value-ref="formAuthenticationFilter" /> </map> </property> <!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 --> <property name="filterChainDefinitions"> <value> <!--所有url都可以匿名访问--> /** = anon </value> </property> </bean>配置安全管理器<!-- securityManager安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="customRealm" /> </bean>配置reaml<!-- realm --> <bean id="customRealm" class="cn.itcast.ssm.shiro.CustomRealm"> </bean>步骤:在web.xml文件中配置shiro的过滤器在对应的Spring配置文件中配置与之对应的filterChain(过虑链儿)配置安全管理器,注入自定义的reaml配置自定义的reaml2.3静态资源不拦截我们在spring配置过滤器链的时候,我们发现这么一行代码:<!--所有url都可以匿名访问 --> /** = anonanon其实就是shiro内置的一个过滤器,上边的代码就代表着所有的匿名用户都可以访问当然了,后边我们还需要配置其他的信息,为了让页面能够正常显示,我们的静态资源一般是不需要被拦截的。于是我们可以这样配置:<!-- 对静态资源设置匿名访问 --> /images/** = anon /js/** = anon /styles/** = anon三、初识shiro过滤器上面我们了解到了anno过滤器的,shiro还有其他的过滤器的..我们来看看这里写图片描述常用的过滤器有下面几种:anon:例子/admins/**=anon 没有参数,表示可以匿名使用。authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,FormAuthenticationFilter是表单认证,没有参数perms:例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。user:例如/admins/user/**=user没有参数,表示必须存在用户, 身份认证通过或通过记住我认证通过的可以访问,当登入操作时不做检查3.1登陆与退出使用FormAuthenticationFilter过虑器实现 ,原理如下:当用户没有认证时,请求loginurl进行认证【上边我们已经配置了】,用户身份和用户密码提交数据到loginurlFormAuthenticationFilter拦截住取出request中的username和password(两个参数名称是可以配置的)FormAuthenticationFilter 调用realm传入一个token(username和password)realm认证时根据username查询用户信息(在Activeuser中存储,包括 userid、usercode、username、menus)。如果查询不到,realm返回null,FormAuthenticationFilter向request域中填充一个参数(记录了异常信息)查询出用户的信息之后,FormAuthenticationFilter会自动将reaml返回的信息和token中的用户名和密码对比。如果不对,那就返回异常。3.1.1登陆页面由于FormAuthenticationFilter的用户身份和密码的input的默认值(username和password),修改页面的账号和密码的input的名称为username和password<TR> <TD>用户名:</TD> <TD colSpan="2"><input type="text" id="usercode" name="username" style="WIDTH: 130px" /></TD> </TR> <TR> <TD>密 码:</TD> <TD><input type="password" id="pwd" name="password" style="WIDTH: 130px" /> </TD> </TR>3.1.2登陆代码实现上面我们已经说了,当用户没有认证的时候,请求的loginurl进行认证,用户身份的用户密码提交数据到loginrul中。当我们提交到loginurl的时候,表单过滤器会自动解析username和password去调用realm来进行认证。最终在request域对象中存储shiroLoginFailure认证信息,如果返回的是异常的信息,那么我们在login中抛出异常即可//登陆提交地址,和applicationContext-shiro.xml中配置的loginurl一致 @RequestMapping("login") public String login(HttpServletRequest request)throws Exception{ //如果登陆失败从request中获取认证异常信息,shiroLoginFailure就是shiro异常类的全限定名 String exceptionClassName = (String) request.getAttribute("shiroLoginFailure"); //根据shiro返回的异常类路径判断,抛出指定异常信息 if(exceptionClassName!=null){ if (UnknownAccountException.class.getName().equals(exceptionClassName)) { //最终会抛给异常处理器 throw new CustomException("账号不存在"); } else if (IncorrectCredentialsException.class.getName().equals( exceptionClassName)) { throw new CustomException("用户名/密码错误"); } else if("randomCodeError".equals(exceptionClassName)){ throw new CustomException("验证码错误 "); }else { throw new Exception();//最终在异常处理器生成未知错误 } } //此方法不处理登陆成功(认证成功),shiro认证成功会自动跳转到上一个请求路径 //登陆失败还到login页面 return "login"; }配置认证过滤器<value> <!-- 对静态资源设置匿名访问 --> /images/** = anon /js/** = anon /styles/** = anon <!-- /** = authc 所有url都必须认证通过才可以访问--> /** = authc </value>3.2退出不用我们去实现退出,只要去访问一个退出的url(该 url是可以不存在),由LogoutFilter拦截住,清除session。在applicationContext-shiro.xml配置LogoutFilter:<!-- 请求 logout.action地址,shiro去清除session--> /logout.action = logout四、认证后信息在页面显示1、认证后用户菜单在首页显示2、认证后用户的信息在页头显示realm从数据库查询用户信息,将用户菜单、usercode、username等设置在SimpleAuthenticationInfo中。//realm的认证方法,从数据库查询用户信息 @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { // token是用户输入的用户名和密码 // 第一步从token中取出用户名 String userCode = (String) token.getPrincipal(); // 第二步:根据用户输入的userCode从数据库查询 SysUser sysUser = null; try { sysUser = sysService.findSysUserByUserCode(userCode); } catch (Exception e1) { // TODO Auto-generated catch block e1.printStackTrace(); } // 如果查询不到返回null if(sysUser==null){// return null; } // 从数据库查询到密码 String password = sysUser.getPassword(); //盐 String salt = sysUser.getSalt(); // 如果查询到返回认证信息AuthenticationInfo //activeUser就是用户身份信息 ActiveUser activeUser = new ActiveUser(); activeUser.setUserid(sysUser.getId()); activeUser.setUsercode(sysUser.getUsercode()); activeUser.setUsername(sysUser.getUsername()); //.. //根据用户id取出菜单 List<SysPermission> menus = null; try { //通过service取出菜单 menus = sysService.findMenuListByUserId(sysUser.getId()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } //将用户菜单 设置到activeUser activeUser.setMenus(menus); //将activeUser设置simpleAuthenticationInfo SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( activeUser, password,ByteSource.Util.bytes(salt), this.getName()); return simpleAuthenticationInfo; }配置凭配器,因为我们用到了md5和散列<!-- 凭证匹配器 --> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5" /> <property name="hashIterations" value="1" /> </bean><!-- realm --> <bean id="customRealm" class="cn.itcast.ssm.shiro.CustomRealm"> <!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 --> <property name="credentialsMatcher" ref="credentialsMatcher"/> </bean>在跳转到首页的时候,取出用户的认证信息,转发到JSP即可//系统首页 @RequestMapping("/first") public String first(Model model)throws Exception{ //从shiro的session中取activeUser Subject subject = SecurityUtils.getSubject(); //取身份信息 ActiveUser activeUser = (ActiveUser) subject.getPrincipal(); //通过model传到页面 model.addAttribute("activeUser", activeUser); return "/first"; }五、总结Shiro用户权限有三种方式编程式注解式标签式Shiro的reaml默认都是去找配置文件的信息来进行授权的,我们一般都是要reaml去数据库来查询对应的信息。因此,又需要自定义reaml总体上,认证和授权的流程差不多。Spring与Shiro整合,Shiro实际上的操作都是通过过滤器来干的。Shiro为我们提供了很多的过滤器。在web.xml中配置Shiro过滤器在Shiro配置文件中使用web.xml配置过的过滤器。配置安全管理器类,配置自定义的reaml,将reaml注入到安全管理器类上。将安全管理器交由Shiro工厂来进行管理。在过滤器链中设置静态资源不拦截。在Shiro使用过滤器来进行用户认证,流程是这样子的:配置用于认证的请求路径当访问程序员该请求路径的时候,Shiro会使用FormAuthenticationFilter会调用reaml获得用户的信息reaml可以拿到token,通过用户名从数据库获取得到用户的信息,如果用户不存在则返回nullFormAuthenticationFilter会将reaml返回的数据进行对比,如果不同则抛出异常我们的请求路径仅仅是用来检测有没有异常抛出,并不用来做校验的。shiro还提供了退出用户的拦截器,我们配置一个url就行了。当需要获取用户的数据用于回显的时候,我们可以在SecurityUtils.getSubject()来得到主体,再通过主体拿到身份信息。
三、Shiro缓存针对上边授权频繁查询数据库,需要使用shiro缓存3.1缓存流程shiro中提供了对认证信息和授权信息的缓存。shiro默认是关闭认证信息缓存的,对于授权信息的缓存shiro默认开启的。主要研究授权信息缓存,因为授权的数据量大。用户认证通过。该用户第一次授权:调用realm查询数据库该用户第二次授权:不调用realm查询数据库,直接从缓存中取出授权信息(权限标识符)。3.2使用ehcache和Shiro整合导入jar包这里写图片描述配置缓存管理器,注入到安全管理器中<!-- 缓存管理器 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/> </bean><!-- securityManager安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="customRealm" /> <!-- 注入缓存管理器 --> <property name="cacheManager" ref="cacheManager"/> </bean>ehcache的配置文件shiro-ehcache.xml:<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"> <!--diskStore:缓存数据持久化的目录 地址 --> <diskStore path="F:\develop\ehcache" /> <defaultCache maxElementsInMemory="1000" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>3.3缓存清空如果用户正常退出,缓存自动清空。如果用户非正常退出,缓存自动清空。还有一种情况:当管理员修改了用户的权限,但是该用户还没有退出,在默认情况下,修改的权限无法立即生效。需要手动进行编程实现:在权限修改后调用realm的clearCache方法清除缓存。清除缓存://清除缓存 public void clearCached() { PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals(); super.clearCache(principals); }3.4sessionManager和shiro整合后,使用shiro的session管理,shiro提供sessionDao操作 会话数据。配置sessionManager<!-- 会话管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- session的失效时长,单位毫秒 --> <property name="globalSessionTimeout" value="600000"/> <!-- 删除失效的session --> <property name="deleteInvalidSessions" value="true"/> </bean>注入到安全管理器中<!-- securityManager安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="customRealm" /> <!-- 注入缓存管理器 --> <property name="cacheManager" ref="cacheManager"/> <!-- 注入session管理器 --> <property name="sessionManager" ref="sessionManager" /> </bean>四、验证码在登陆的时候,我们一般都设置有验证码,但是我们如果使用Shiro的话,那么Shiro默认的是使用FormAuthenticationFilter进行表单认证。而我们的验证校验的功能应该加在FormAuthenticationFilter中,在认证之前进行验证码校验。FormAuthenticationFilter是Shiro默认的功能,我们想要在FormAuthenticationFilter之前进行验证码校验,就需要继承FormAuthenticationFilter类,改写它的认证方法!4.1自定义Form认证类public class CustomFormAuthenticationFilter extends FormAuthenticationFilter { //原FormAuthenticationFilter的认证方法 @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { //在这里进行验证码的校验 //从session获取正确验证码 HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpSession session =httpServletRequest.getSession(); //取出session的验证码(正确的验证码) String validateCode = (String) session.getAttribute("validateCode"); //取出页面的验证码 //输入的验证和session中的验证进行对比 String randomcode = httpServletRequest.getParameter("randomcode"); if(randomcode!=null && validateCode!=null && !randomcode.equals(validateCode)){ //如果校验失败,将验证码错误失败信息,通过shiroLoginFailure设置到request中 httpServletRequest.setAttribute("shiroLoginFailure", "randomCodeError"); //拒绝访问,不再校验账号和密码 return true; } return super.onAccessDenied(request, response); } }4.2配置自定义类我们编写完自定义类以后,是需要在Shiro配置文件中配置我们这个自定义类的。由于这是我们自定义的,因此我们并不需要用户名就使用username,密码就使用password,这个也是我们可以自定义的。<!-- 自定义form认证过虑器 --> <!-- 基于Form表单的身份验证过滤器,不配置将也会注册此过虑器,表单中的用户账号、密码及loginurl将采用默认值,建议配置 --> <bean id="formAuthenticationFilter" class="cn.itcast.ssm.shiro.CustomFormAuthenticationFilter "> <!-- 表单中账号的input名称 --> <property name="usernameParam" value="username" /> <!-- 表单中密码的input名称 --> <property name="passwordParam" value="password" /> </bean>在Shiro的bean中注入自定义的过滤器<!-- 自定义filter配置 --> <property name="filters"> <map> <!-- 将自定义 的FormAuthenticationFilter注入shiroFilter中--> <entry key="authc" value-ref="formAuthenticationFilter" /> </map> </property>在我们的Controller添加验证码错误的异常判断,从我们的Controller就可以发现,为什么我们要把错误信息存放在request域对象shiroLoginFailure,因为我们得在Controller中获取获取信息,从而给用户对应的提示@RequestMapping("login") public String login(HttpServletRequest request)throws Exception{ //如果登陆失败从request中获取认证异常信息,shiroLoginFailure就是shiro异常类的全限定名 String exceptionClassName = (String) request.getAttribute("shiroLoginFailure"); //根据shiro返回的异常类路径判断,抛出指定异常信息 if(exceptionClassName!=null){ if (UnknownAccountException.class.getName().equals(exceptionClassName)) { //最终会抛给异常处理器 throw new CustomException("账号不存在"); } else if (IncorrectCredentialsException.class.getName().equals( exceptionClassName)) { throw new CustomException("用户名/密码错误"); } else if("randomCodeError".equals(exceptionClassName)){ throw new CustomException("验证码错误 "); }else { throw new Exception();//最终在异常处理器生成未知错误 } } //此方法不处理登陆成功(认证成功),shiro认证成功会自动跳转到上一个请求路径 //登陆失败还到login页面 return "login"; }这里写图片描述<TR> <TD>验证码:</TD> <TD><input id="randomcode" name="randomcode" size="8" /> <img id="randomcode_img" src="${baseurl}validatecode.jsp" alt="" width="56" height="20" align='absMiddle' /> <a href=javascript:randomcode_refresh()>刷新</a></TD> </TR>五、记住我Shiro还提供了记住用户名和密码的功能!用户登陆选择“自动登陆”本次登陆成功会向cookie写身份信息,下次登陆从cookie中取出身份信息实现自动登陆。想要实现这个功能,我们的认证信息需要实现Serializable接口。public class ActiveUser implements java.io.Serializable { private String userid;//用户id(主键) private String usercode;// 用户账号 private String username;// 用户名称 private List<SysPermission> menus;// 菜单 private List<SysPermission> permissions;// 权限 }5.1配置rememeber管理器<!-- rememberMeManager管理器,写cookie,取出cookie生成用户信息 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cookie" ref="rememberMeCookie" /> </bean> <!-- 记住我cookie --> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <!-- rememberMe是cookie的名字 --> <constructor-arg value="rememberMe" /> <!-- 记住我cookie生效时间30天 --> <property name="maxAge" value="2592000" /> </bean>注入到安全管理器类上<!-- securityManager安全管理器 --> <bean id="securityManager"~~~···· <property name="cacheManager" ref="cacheManager"/> <!-- 注入session管理器 --> <property name="sessionManager" ref="sessionManager" /> <!-- 记住我 --> <property name="rememberMeManager" ref="rememberMeManager"/> </bean>配置页面的input名称:<tr> <TD></TD> <td><input type="checkbox" name="rememberMe" />自动登陆</td> </tr>如果设置了“记住我”,那么访问某些URL的时候,我们就不需要登陆了。将记住我即可访问的地址配置让UserFilter拦截。<!-- 配置记住我或认证通过可以访问的地址 --> /index.jsp = user /first.action = user /welcome.jsp = user六、总结Shiro的授权过程和认证过程是类似的,在配置文件上配置需要授权的路径,当访问路径的时候,Shiro过滤器去找到reaml,reaml返回数据以后进行比对。Shiro支持注解式授权,直接在Controller方法上使用注解声明访问该方法需要授权Shiro还支持标签授权,但一般很少用由于每次都要对reaml查询数据库,性能会低。Shiro默认是支持授权缓存的。为了达到很好的效果,我们使用Ehcache来对Shiro的缓存进行管理配置会话管理器,对会话时间进行控制手动清空缓存由于验证用户名和密码之前,一般需要验证验证码的。所以,我们要改写表单验证的功能,先让它去看看验证码是否有错,如果验证码有错的话,那么用户名和密码就不用验证了。将自定义的表单验证类配置起来。使用Shiro提供的记住我功能,如果用户已经认证了,那就不用再次登陆了。可以直接访问某些页面。
五、连线上面我们已将学过了流程变量了,可以在【任务服务、运行时服务、流程开始、完成某个任务时设置流程变量】,而我们的连接就是流程变量的实际应用了….5.1定义流程图我们并不是所有的流程都是按一条的路径来走的,我们有的时候会根据条件来走不同的路。当然了,最终该流程是会一步步走完….例子:重要的信息交由老板来处理,不重要的信息交由经理来处理这里写图片描述表达式的结果必须是布尔型#{variable=='value'}${variable==value}5.2测试我在任务完成时设置流程变量为不重要,那么跳到下一个流程时就是经理来进行处理这里写图片描述当我设置为重要的时候,那么就是交由老板来处理这里写图片描述六、排他网关上面我们使用连线的时候用了两个条件 : 要么条件是“重要”,要么条件是“不重要”….如果有另一种情况呢???就是用户把条件输入错了,写成“不知道重不重要”,那么我们的流程怎么走???岂不是奔溃了???因此,我们要有一条默认的路来走,就是当该变量不符合任何的条件时,我们也有一条默认的路这里写图片描述值得注意的是:如果是在Eclipse中使用插件的BPMN流程图,如果使用了排他网关,那么在Idea下是解析不了的…解决:我们只要重新定义BPMN流程图的排他网关就行了,idea中的Activiti插件是不用制定默认流程的,只要我们不设置条件,那就是默认的连接线6.1测试public class ExclusiveGetWay { private ProcessEngine processEngine = ProcessEngines .getDefaultProcessEngine(); // 部署流程定义,资源来在bpmn格式 @Test public void deployProcessDefi() { Deployment deploy = processEngine.getRepositoryService() .createDeployment().name("排他网关流程") .addClasspathResource("ExclusiveGateway.bpmn") .deploy(); System.out.println("部署名称:" + deploy.getName()); System.out.println("部署id:" + deploy.getId()); } // 执行流程,开始跑流程 @Test public void startProcess() { String processDefiKey = "bankBill";// bpmn 的 process id属性 ProcessInstance pi = processEngine.getRuntimeService() .startProcessInstanceByKey(processDefiKey); System.out.println("流程执行对象的id:" + pi.getId());// Execution 对象 System.out.println("流程实例的id:" + pi.getProcessInstanceId());// ProcessInstance // 对象 System.out.println("流程定义的id:" + pi.getProcessDefinitionId());// 默认执行的是最新版本的流程定义 } // 查询正在运行任务 @Test public void queryTask() { // 取得任务服务 TaskService taskService = processEngine.getTaskService(); // 创建一个任务查询对象 TaskQuery taskQuery = taskService.createTaskQuery(); // 办理人的任务列表 List<Task> list = taskQuery.list(); // 遍历任务列表 if (list != null && list.size() > 0) { for (Task task : list) { System.out.println("任务的办理人:" + task.getAssignee()); System.out.println("任务的id:" + task.getId()); System.out.println("任务的名称:" + task.getName()); } } } // 完成任务 @Test public void compileTask() { String taskId = "2404"; Map<String,Object> params=new HashMap<String, Object>(); params.put("visitor", 6); // taskId:任务id processEngine.getTaskService().complete(taskId, params); // processEngine.getTaskService().complete(taskId); System.out.println("当前任务执行完毕"); } }我们指定的值并不是VIP也不是后台,那么就会自动去普通窗口中处理这里写图片描述七、拓展阅读并行网关:这里写图片描述等待活动:这里写图片描述用户任务:使用流程变量指定处理人:我们在快速入门的例子中,是在定义流程图中硬性指定处理人,其实这么干是不够灵活的,我们学了流程变量之后,我们是可以灵活地指定处理人的….这里写图片描述@Test public void deployProcessDefi() { Deployment deploy = processEngine.getRepositoryService() .createDeployment().name("用户任务指定流程") .addClasspathResource("AppayBill.bpmn") .deploy(); System.out.println("部署名称:" + deploy.getName()); System.out.println("部署id:" + deploy.getId()); } // 执行流程,开始跑流程 @Test public void startProcess() { String processDefiKey = "appayBill";// bpmn 的 process id属性 Map<String,Object> params=new HashMap<String, Object>(); params.put("userID", "王某某"); ProcessInstance pi = processEngine.getRuntimeService() .startProcessInstanceByKey(processDefiKey, params); System.out.println("流程执行对象的id:" + pi.getId());// Execution 对象 System.out.println("流程实例的id:" + pi.getProcessInstanceId());// ProcessInstance // 对象 System.out.println("流程定义的id:" + pi.getProcessDefinitionId());// 默认执行的是最新版本的流程定义 } // 查询正在运行任务 @Test public void queryTask() { String assignee="王某某";//指定任务处理人 // 取得任务服务 TaskService taskService = processEngine.getTaskService(); // 创建一个任务查询对象 TaskQuery taskQuery = taskService.createTaskQuery(); // 办理人的任务列表 List<Task> list = taskQuery .taskAssignee(assignee) .list(); // 遍历任务列表 if (list != null && list.size() > 0) { for (Task task : list) { System.out.println("任务的办理人:" + task.getAssignee()); System.out.println("任务的id:" + task.getId()); System.out.println("任务的名称:" + task.getName()); } } }这里写图片描述使用类指定:这里写图片描述组任务:直接指定办理人这里写图片描述使用流程变量这里写图片描述使用类这里写图片描述总结如果一个业务需要多方面角色进行处理的话,那么我们最好就是用工作流框架。因为如果其中一个环节的需求发生了变化的话,我们要是没有用到工作流。那就需要修改很多的代码。十分麻烦。Activiti工作流框架快速入门:定义工作流,使用插件来把我们的流程图画出来。这个流程图就是我们定义的工作流。工作流引擎是工作流的核心,能够让我们定义出来的工作流部署起来。由于我们使用工作流的时候是有很多数据产生的,因此Activiti是将数据保存到数据库表中的。这些数据库表由Actitviti创建,由Activiti维护。部署完的工作流是需要手动去执行该工作流的。根据由谁处理当前任务,我们就可以查询出具体的任务信息。根据任务的id,我们就可以执行任务了。流程定义涉及到了四张数据库表我们可以通过API把我们的流程定义图读取出来可以根据查询最新版本的流程定义删除流程定义部署流程定义的时候也可以是ZIP文件流程在运行中,涉及到两个对象,四张数据库表:获取流程实例和任务的历史信息判断流程实例是否为空来判断流程是否结束了查看正在运行服务的详细信息通过流程实例来开启流程如果流程没有分支的话,那么流程实例就等于流程对象流程实例流程任务流程实例可以有多个,流程对象只能有一个。基于这么两个对象,我们就可以做很多事情了流程变量:它涉及到了两张表。流程变量实际上就是我们的条件。流程变量的作用域只在流程实例中。我们可以在流程开始的时候设置流程变量,在任务完成的时候设置流程变量。运行时服务和流程任务都可以设置流程变量。通过连线我们可以在其中设置条件,根据不同的条件流程走不同的分支如果没有设置默认的条件,当条件不吻合的时候,那么流程就走不下去了,因此需要排他网关来设置一条默认的路径。
3.3获取流程实例的状态有的时候,我们需要判断它是在该流程,还是该流程已经结束了。我们可以根据获取出来的对象是否为空来进行判断//获取流程实例的状态 @Test public void getProcessInstanceState(){ String processInstanceId="605"; ProcessInstance pi = processEngine.getRuntimeService() .createProcessInstanceQuery() .processInstanceId(processInstanceId) .singleResult();//返回的数据要么是单行,要么是空 ,其他情况报错 //判断流程实例的状态 if(pi!=null){ System.out.println("该流程实例"+processInstanceId+"正在运行... "+"当前活动的任务:"+pi.getActivityId()); }else{ System.out.println("当前的流程实例"+processInstanceId+" 已经结束!"); } }这里写图片描述3.4查看历史流程实例的信息//查看历史执行流程实例信息 @Test public void queryHistoryProcInst(){ List<HistoricProcessInstance> list = processEngine.getHistoryService() .createHistoricProcessInstanceQuery() .list(); if(list!=null&&list.size()>0){ for(HistoricProcessInstance temp:list){ System.out.println("历史流程实例id:"+temp.getId()); System.out.println("历史流程定义的id:"+temp.getProcessDefinitionId()); System.out.println("历史流程实例开始时间--结束时间:"+temp.getStartTime()+"-->"+temp.getEndTime()); } } }查询表:这里写图片描述这里写图片描述3.5查看历史实例执行任务信息@Test public void queryHistoryTask(){ String processInstanceId="605"; List<HistoricTaskInstance> list = processEngine.getHistoryService() .createHistoricTaskInstanceQuery() .processInstanceId(processInstanceId) .list(); if(list!=null&&list.size()>0){ for(HistoricTaskInstance temp:list){ System.out.print("历史流程实例任务id:"+temp.getId()); System.out.print("历史流程定义的id:"+temp.getProcessDefinitionId()); System.out.print("历史流程实例任务名称:"+temp.getName()); System.out.println("历史流程实例任务处理人:"+temp.getAssignee()); } } }给予对应的实例id就可以查询出执行到哪个任务了…这里写图片描述这里写图片描述3.6执行任务根据任务的id,就可以把该任务执行了。@Test public void compileTask(){ String taskId="608"; //taskId:任务id processEngine.getTaskService().complete(taskId); System.out.println("当前任务执行完毕"); }四、流程变量细讲流程变量涉及到的数据库表:act_ru_variable:正在执行的流程变量表 act_hi_varinst:流程变量历史表流程变量在工作流中扮演着一个非常重要的角色。例如:请假流程中有请假天数、请假原因等一些参数都为流程变量的范围。流程变量的作用域范围是只对应一个流程实例。也就是说各个流程实例的流程变量是不相互影响的。流程实例结束完成以后流程变量还保存在数据库中(存放到流程变量的历史表中)。这里写图片描述4.1设置流程变量我们有两种服务可以设置流程变量,TaskService【任务服务】和RuntimeService【运行时服务】场景在流程开始的时候设置流程变量在完成某个任务的时候设置流程变量使用TaskService设置服务使用RuntimeService设置服务作用:传递业务参数动态指定代理人【我们快速入门的例子是固定在流程定义图上写上代理人的】指定连接【决定流程往哪边走】4.2流程变量支持类型如果我们使用JavaBean来作为流程的变量,那么我们需要将JavaBean实现Serializable接口。Javabean类型设置获取流程变量,除了需要这个javabean实现了Serializable接口外,还要求流程变量对象的属性不能发生变化,否则抛出异常。解决方案,固定序列化ID这里写图片描述4.3setVariable和setVariableLocal的区别这里写图片描述4.4例子//模拟流程变量设置 @Test public void getAndSetProcessVariable(){ //有两种服务可以设置流程变量 // TaskService taskService = processEngine.getTaskService(); // RuntimeService runtimeService = processEngine.getRuntimeService(); /**1.通过 runtimeService 来设置流程变量 * executionId: 执行对象 * variableName:变量名 * values:变量值 */ // runtimeService.setVariable(executionId, variableName, values); // runtimeService.setVariableLocal(executionId, variableName, values); //设置本执行对象的变量 ,该变量的作用域只在当前的execution对象 // runtimeService.setVariables(executionId, variables); //可以设置多个变量 放在 Map<key,value> Map<String,Object> /**2. 通过TaskService来设置流程变量 * taskId:任务id */ // taskService.setVariable(taskId, variableName, values); // taskService.setVariableLocal(taskId, variableName, values); //// 设置本执行对象的变量 ,该变量的作用域只在当前的execution对象 // taskService.setVariables(taskId, variables); //设置的是Map<key,values> /**3. 当流程开始执行的时候,设置变量参数 * processDefiKey: 流程定义的key * variables: 设置多个变量 Map<key,values> */ // processEngine.getRuntimeService() // .startProcessInstanceByKey(processDefiKey, variables) /**4. 当任务完成时候,可以设置流程变量 * taskId:任务id * variables: 设置多个变量 Map<key,values> */ // processEngine.getTaskService().complete(taskId, variables); /** 5. 通过RuntimeService取变量值 * exxcutionId: 执行对象 * */ // runtimeService.getVariable(executionId, variableName);//取变量 // runtimeService.getVariableLocal(executionId, variableName);//取本执行对象的某个变量 // runtimeService.getVariables(variablesName);//取当前执行对象的所有变量 /** 6. 通过TaskService取变量值 * TaskId: 执行对象 * */ // taskService.getVariable(taskId, variableName);//取变量 // taskService.getVariableLocal(taskId, variableName);//取本执行对象的某个变量 // taskService.getVariables(taskId);//取当前执行对象的所有变量 }//设置流程变量值 @Test public void setVariable(){ String taskId="1804";//任务id //采用TaskService来设置流程变量 //1. 第一次设置流程变量 // TaskService taskService = processEngine.getTaskService(); // taskService.setVariable(taskId, "cost", 1000);//设置单一的变量,作用域在整个流程实例 // taskService.setVariable(taskId, "申请时间", new Date()); // taskService.setVariableLocal(taskId, "申请人", "何某某");//该变量只有在本任务中是有效的 //2. 在不同的任务中设置变量 // TaskService taskService = processEngine.getTaskService(); // taskService.setVariable(taskId, "cost", 5000);//设置单一的变量,作用域在整个流程实例 // taskService.setVariable(taskId, "申请时间", new Date()); // taskService.setVariableLocal(taskId, "申请人", "李某某");//该变量只有在本任务中是有效的 /** * 3. 变量支持的类型 * - 简单的类型 :String 、boolean、Integer、double、date * - 自定义对象bean */ TaskService taskService = processEngine.getTaskService(); //传递的一个自定义bean对象 AppayBillBean appayBillBean=new AppayBillBean(); appayBillBean.setId(1); appayBillBean.setCost(300); appayBillBean.setDate(new Date()); appayBillBean.setAppayPerson("何某某"); taskService.setVariable(taskId, "appayBillBean", appayBillBean); System.out.println("设置成功!"); }//查询流程变量 @Test public void getVariable(){ String taskId="1804";//任务id // TaskService taskService = processEngine.getTaskService(); // Integer cost=(Integer) taskService.getVariable(taskId, "cost");//取变量 // Date date=(Date) taskService.getVariable(taskId, "申请时间");//取本任务中的变量 //// Date date=(Date) taskService.getVariableLocal(taskId, "申请时间");//取本任务中的变量 // String appayPerson=(String) taskService.getVariableLocal(taskId, "申请人");//取本任务中的变量 //// String appayPerson=(String) taskService.getVariable(taskId, "申请人");//取本任务中的变量 // // System.out.println("金额:"+cost); // System.out.println("申请时间:"+date); // System.out.println("申请人:"+appayPerson); //读取实现序列化的对象变量数据 TaskService taskService = processEngine.getTaskService(); AppayBillBean appayBillBean=(AppayBillBean) taskService.getVariable(taskId, "appayBillBean"); System.out.println(appayBillBean.getCost()); System.out.println(appayBillBean.getAppayPerson()); }public class AppayBillBean implements Serializable{ private Integer id; private Integer cost;//金额 private String appayPerson;//申请人 private Date date;//申请日期 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Integer getCost() { return cost; } public void setCost(Integer cost) { this.cost = cost; } public String getAppayPerson() { return appayPerson; } public void setAppayPerson(String appayPerson) { this.appayPerson = appayPerson; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } }
Activiti介绍什么是Activiti?Activiti5是由Alfresco软件在2010年5月17日发布的业务流程管理(BPM)框架,它是覆盖了业务流程管理、工作流、服务协作】等领域的一个开源的、灵活的、易扩展的可执行流程语言框架。Activiti基于Apache许可的开源BPM平台,创始人Tom Baeyens是JBoss jBPM的项目架构师,它特色是提供了eclipse插件,开发人员可以通过插件直接绘画出业务流程图。.我们即将学习的是一个业务流程管理框架, 常见开源工作流引擎框架 : OSWorkFlow、jBPM(jboss business process management),Activiti工作流(是对jBPM升级)。一般我们称作为工作流框架..为什么要学习Activiti那我们为什么要学习业务流程管理框架呢???学习它干嘛???工作流(Workflow),就是“业务过程的部分或整体在计算机应用环境下的自动化”我们来提出一个常见的需求来更好地理解:我们在学生时代,肯定会遇到请假写请假条的情况,如果学校的请假抓得非常严,就需要经过多层的同意才能确定能不能请假..这里写图片描述班主任->任课老师->教学总监->校长这么一个流程,首先我们先明确一点:我们作为一个学生,不可能直接跳过老师,去找校长申请请假的【校长是你随便找的吗??】因此我们请假的流程是一步一步的,并不能做跳跃也就是说,当班主任没有批准请假的时候,即使你去找任课老师了,任课老师会让你回去找班主任的,作为任课老师了,只关注班主任有没有批准你请假,同理,作为校长,只关注教学总监有没有批准你请假!进一步说:当教学总监还没有批准你请假时,你请假的请求是不会出现在校长的范围里的。其实是非常好理解的,就是一步步往下执行,当还没有执行到自己处理的点子上时,是不会有对应的处理请求的。分工有序对上面的请假流程进行分析,如果我们没有使用框架,而把这么一个请假流程写到我们的网站中,我们会怎么做呢??这里写图片描述我们需要维护一个变量,来不断传递过去给下一个处理者…如果一切正常,需求不会变,并没有条件的处理。这是我们非常希望看到的…但是,如果有条件判断【请假三天以下、请假三天以上的处理方式不一样】,需求会变【不需要校长批准了,教学总监批准完,你就能够请假了】,那么我们的代码就会变得乱基于这么一个原因,我们是需要学习一个框架来帮我们完成工作流的…采用工作流管理系统的优点1、提高系统的柔性,适应业务流程的变化2、实现更好的业务过程控制,提高顾客服务质量3、降低系统开发和维护成本一、快速入门Activiti首先我们来梳理一下Activiti的开发步骤:我们要用到一个工作流,首先就要把这个工作流定义出来【也就是工作流的步骤的怎么样的】,Activiti支持以“图”的方式来定义工作流定义完工作流,就要部署到起来【我们可以联想到Tomcat,我们光下载了Tomcat是没有用的,要把它部署起来】随后我们就执行该工作流,该工作流就随着我们定义的步骤来一一执行!1.1BPMN业务流程建模与标注(Business Process Model and Notation,BPMN) ,描述流程的基本符号,包括这些图元如何组合成一个业务流程图(Business Process Diagram)BPMN这个就是我们所谓把工作流定义出来的流程图..1.2数据库相关我们在执行工作流步骤的时候会涉及到很多数据【执行该流程的人是谁、所需要的参数是什么、包括想查看之前流程执行的记录等等】,因此我们会需要用到数据库的表来保存数据…由于我们使用的是Activiti框架,这个框架会自动帮我们把对应的数据库表创建起来,它涉及的表有23个,但是常用的并不是很多,因此也不用很慌…下面就列举一下表的情况Activiti的后台是有数据库的支持,所有的表都以ACT_开头。 第二部分是表示表的用途的两个字母标识。 用途也和服务的API对应。 ACT_RE_*: 'RE'表示repository。 这个前缀的表包含了流程定义和流程静态资源 (图片,规则,等等)。 ACT_RU_*: 'RU'表示runtime。 这些运行时的表,包含流程实例,任务,变量,异步任务,等运行中的数据。 Activiti只在流程实例执行过程中保存这些数据, 在流程结束时就会删除这些记录。 这样运行时表可以一直很小速度很快。 ACT_ID_*: 'ID'表示identity。 这些表包含身份信息,比如用户,组等等。 ACT_HI_*: 'HI'表示history。 这些表包含历史数据,比如历史流程实例, 变量,任务等等。 ACT_GE_*: 通用数据, 用于不同场景下,如存放资源文件。1.3搭建配置环境我这里使用的Intellij idea来使用Activiti,首先,我们得下载插件来使用Activiti【因为定义流程图需要用到插件】详情可以看这篇博文:http://blog.sina.com.cn/s/blog_4b3196670102woix.htmlActiviti插件中文乱码问题:http://www.cnblogs.com/mymelody/p/6049291.html流程之前的连线是通过图中的蓝色小点点拖动来进行连接的…导入对应的jar包activation-1.1.jaractiviti-bpmn-converter-5.13.jaractiviti-bpmn-layout-5.13.jaractiviti-bpmn-model-5.13.jaractiviti-common-rest-5.13.jaractiviti-engine-5.13.jaractiviti-json-converter-5.13.jaractiviti-rest-5.13.jaractiviti-simple-workflow-5.13.jaractiviti-spring-5.13.jaraopalliance-1.0.jarcommons-dbcp-1.4.jarcommons-email-1.2.jarcommons-fileupload-1.2.2.jarcommons-io-2.0.1.jarcommons-lang-2.4.jarcommons-pool-1.5.4.jarh2-1.3.170.jarhamcrest-core-1.3.jarjackson-core-asl-1.9.9.jarjackson-mapper-asl-1.9.9.jarjavaGeom-0.11.0.jarjcl-over-slf4j-1.7.2.jarjgraphx-1.10.4.2.jarjoda-time-2.1.jarjunit-4.11.jarlog4j-1.2.17.jarmail-1.4.1.jarmybatis-3.2.2.jarmysql-connector-java.jarorg.restlet.ext.fileupload-2.0.15.jarorg.restlet.ext.jackson-2.0.15.jarorg.restlet.ext.servlet-2.0.15.jarorg.restlet-2.0.15.jarslf4j-api-1.7.2.jarslf4j-log4j12-1.7.2.jarspring-aop-3.1.2.RELEASE.jarspring-asm-3.1.2.RELEASE.jarspring-beans-3.1.2.RELEASE.jarspring-context-3.1.2.RELEASE.jarspring-core-3.1.2.RELEASE.jarspring-expression-3.1.2.RELEASE.jarspring-jdbc-3.1.2.RELEASE.jarspring-orm-3.1.2.RELEASE.jarspring-tx-3.1.2.RELEASE.jar1.4开发步骤上面已经说过了,我们要想使用Activiti就需要有数据库的支持,虽然Activiti是自动帮我们创建对应的数据库表,但是我们是需要配置数据库的信息的。我们配置数据库的信息,接着拿到Activiti最重要的API------Activiti引擎这里写图片描述1.4.1得到工作流引擎Activiti提供使用代码的方式来配置数据库的信息:@Test public void createActivitiEngine(){ /* *1.通过代码形式创建 * - 取得ProcessEngineConfiguration对象 * - 设置数据库连接属性 * - 设置创建表的策略 (当没有表时,自动创建表) * - 通过ProcessEngineConfiguration对象创建 ProcessEngine 对象*/ //取得ProcessEngineConfiguration对象 ProcessEngineConfiguration engineConfiguration=ProcessEngineConfiguration. createStandaloneProcessEngineConfiguration(); //设置数据库连接属性 engineConfiguration.setJdbcDriver("com.mysql.jdbc.Driver"); engineConfiguration.setJdbcUrl("jdbc:mysql://localhost:3306/activitiDB?createDatabaseIfNotExist=true" + "&useUnicode=true&characterEncoding=utf8"); engineConfiguration.setJdbcUsername("root"); engineConfiguration.setJdbcPassword("root"); // 设置创建表的策略 (当没有表时,自动创建表) // public static final java.lang.String DB_SCHEMA_UPDATE_FALSE = "false";//不会自动创建表,没有表,则抛异常 // public static final java.lang.String DB_SCHEMA_UPDATE_CREATE_DROP = "create-drop";//先删除,再创建表 // public static final java.lang.String DB_SCHEMA_UPDATE_TRUE = "true";//假如没有表,则自动创建 engineConfiguration.setDatabaseSchemaUpdate("true"); //通过ProcessEngineConfiguration对象创建 ProcessEngine 对象 ProcessEngine processEngine = engineConfiguration.buildProcessEngine(); System.out.println("流程引擎创建成功!"); }Activiti也可以通过配置文件来配置数据库的信息,加载配置文件从而得到工作流引擎/**2. 通过加载 activiti.cfg.xml 获取 流程引擎 和自动创建数据库及表 * ProcessEngineConfiguration engineConfiguration= ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml"); //从类加载路径中查找资源 activiti.cfg.xm文件名可以自定义 ProcessEngine processEngine = engineConfiguration.buildProcessEngine(); System.out.println("使用配置文件Activiti.cfg.xml获取流程引擎"); */activiti.cfg.xml<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <!-- 配置 ProcessEngineConfiguration --> <bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration"> <!-- 配置数据库连接 --> <property name="jdbcDriver" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/activitiDB?createDatabaseIfNotExist=true&amp;useUnicode=true&amp;characterEncoding=utf8"></property> <property name="jdbcUsername" value="root"></property> <property name="jdbcPassword" value="root"></property> <!-- 配置创建表策略 :没有表时,自动创建 --> <property name="databaseSchemaUpdate" value="true"></property> </bean> </beans>这里写图片描述上面的那种加载配置文件方式,配置文件的名字是可以自定义的,如果我们配置文件的名字默认就是activiti.cfg.xml的话,也是放在类路径下,我们就可以使用默认的方式来进行加载了!@Test public void createActivitiEngine(){ /** * 3. 通过ProcessEngines 来获取默认的流程引擎 */ // 默认会加载类路径下的 activiti.cfg.xml ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine(); System.out.println("通过ProcessEngines 来获取流程引擎"); }1.4.2定义工作流定义工作流就需要我们刚才下载的插件了,我们是使用图形的方式来定义工作流的….这里写图片描述在每个流程中,我们都可以指定对应的处理人是谁,交由谁处理1.4.3部署工作流我们上面已经定义了工作流了,工作流引擎我们也已经拿到了,接下来就是把工作流部署到工作流引擎中了@Test public void deploy() { //获取仓库服务 :管理流程定义 RepositoryService repositoryService = processEngine.getRepositoryService(); Deployment deploy = repositoryService.createDeployment()//创建一个部署的构建器 .addClasspathResource("LeaveActiviti.bpmn")//从类路径中添加资源,一次只能添加一个资源 .name("请求单流程")//设置部署的名称 .category("办公类别")//设置部署的类别 .deploy(); System.out.println("部署的id"+deploy.getId()); System.out.println("部署的名称"+deploy.getName()); }相对应的数据库表就会插入数据、涉及到的数据库表后面会详细说明。现在我们只要了解到,我们工作流引擎执行操作会有数据库表记录这里写图片描述1.4.4执行工作流指定指定工作流就是我们定义时工作流程表的id这里写图片描述@Test public void startProcess(){ //指定执行我们刚才部署的工作流程 String processDefiKey="leaveBill"; //取运行时服务 RuntimeService runtimeService = processEngine.getRuntimeService(); //取得流程实例 ProcessInstance pi = runtimeService.startProcessInstanceByKey(processDefiKey);//通过流程定义的key 来执行流程 System.out.println("流程实例id:"+pi.getId());//流程实例id System.out.println("流程定义id:"+pi.getProcessDefinitionId());//输出流程定义的id }1.4.5根据代理人查询当前任务的信息刚才我们已经开始了工作流了,随后工作流应该去到了申请请假的流程,申请请假的处理人是钟福成,我们可以查询出对应的信息://查询任务 @Test public void queryTask(){ //任务的办理人 String assignee="钟福成"; //取得任务服务 TaskService taskService = processEngine.getTaskService(); //创建一个任务查询对象 TaskQuery taskQuery = taskService.createTaskQuery(); //办理人的任务列表 List<Task> list = taskQuery.taskAssignee(assignee)//指定办理人 .list(); //遍历任务列表 if(list!=null&&list.size()>0){ for(Task task:list){ System.out.println("任务的办理人:"+task.getAssignee()); System.out.println("任务的id:"+task.getId()); System.out.println("任务的名称:"+task.getName()); } } }这里写图片描述1.4.6处理任务我们现在处理流程去到“申请请假”中,处理人是钟福成…接着就是钟福成去处理任务,根据任务的id使得流程继续往下走任务的id刚才我们已经查询出来了【上面】,我们如果是在web端操作数据的话,那么只要传递过去就行了!//完成任务 @Test public void compileTask(){ String taskId="304"; //taskId:任务id processEngine.getTaskService().complete(taskId); System.out.println("当前任务执行完毕"); }这里写图片描述当我们处理完该任务的时候,就到了批准【班主任】任务了,我们查询一下是不是如我们想象的效果:这里写图片描述我们按照定义的工作流程图一步一步往下走,最终把流程走完这里写图片描述
uddiuddi (Universal Description, Discovery and Integration)统一描述、发现、集成它是目录服务,通过该服务可以注册和发布webservcie,以便第三方的调用者统一调用用得并不太多。实现接口的webservice服务端import javax.jws.WebService; /**面向接口的webservice发布方式 * * */ @WebService public interface JobService { public String getJob(); }import javax.jws.WebService; @WebService(endpointInterface="cn.it.ws.e.JobService")//设置服务端点接口 ,指定对外提供服务的接口 public class JobServiceImpl implements JobService { @Override public String getJob() { return "JEE研发工程师|Android研发工程师|数据库工程师|前端工程师|测试工程师|运维工程师"; } public void say(){ System.out.println("早上好!"); } }客户端import javax.xml.ws.Endpoint; public class Test { public static void main(String[] args) { JobService jobService=new JobServiceImpl(); String address="http://192.168.114.10:9999/ws/jobservice"; Endpoint.publish(address, jobService); System.out.println("wsdl地址:"+address+"?WSDL"); } }CXF框架Apache CXF 是一个开源的 Services 框架,CXF 帮助您来构建和开发 Services 这些 Services 可以支持多种协议,比如:SOAP、POST/HTTP、RESTful HTTP CXF 大大简化了 Service可以天然地和 Spring 进行无缝集成。CXF介绍 :soa的框架* cxf 是 Celtrix (ESB框架)和 XFire(webserivice) 合并而成,并且捐给了apache * CxF的核心是org.apache.cxf.Bus(总线),类似于Spring的 ApplicationContext* CXF默认是依赖于Spring的* Apache CXF 发行包中的jar,如果全部放到lib中,需要 JDK1.6 及以上,否则会报JAX-WS版本不一致的问题* CXF 内置了Jetty服务器 ,它是servlet容器,好比tomcatCXF特点与Spring、Servlet做了无缝对接,cxf框架里面集成了Servlet容器Jetty支持注解的方式来发布webservice能够显示一个webservice的服务列表能够添加拦截器:输入拦截器、输出拦截器 :输入日志信息拦截器、输出日志拦截器、用户权限认证的拦截器CXF开发要想使用CXF框架,那么就先导入jar包asm-3.3.jarcommons-logging-1.1.1.jarcxf-2.4.2.jarjetty-continuation-7.4.5.v20110725.jarjetty-http-7.4.5.v20110725.jarjetty-io-7.4.5.v20110725.jarjetty-security-7.4.5.v20110725.jarjetty-server-7.4.5.v20110725.jarjetty-util-7.4.5.v20110725.jarneethi-3.0.1.jarwsdl4j-1.6.2.jarxmlschema-core-2.0.jar接口import javax.jws.WebParam; import javax.jws.WebResult; import javax.jws.WebService; @WebService(serviceName="languageManager") public interface LanguageService { public @WebResult(name="language")String getLanguage(@WebParam(name="position")int position); }实现:package cn.it.ws.cxf.a; import org.apache.cxf.frontend.ServerFactoryBean; import org.apache.cxf.interceptor.LoggingInInterceptor; import org.apache.cxf.interceptor.LoggingOutInterceptor; import org.apache.cxf.jaxws.JaxWsServerFactoryBean; /**开发语言排行描述服务 * * * @author 李俊 2015年5月17日 */ public class LanguageServiceImpl implements LanguageService { /* (non-Javadoc) * @see cn.it.ws.cxf.a.LanguageService#getLanguage(int) */ @Override public String getLanguage(int position){ String language=null; switch (position) { case 1: language="java"; break; case 2: language="C"; break; case 3: language="Objective-C"; break; case 4: language="C#"; break; default: break; } return language; } /**通过cxf框架发布webservice * 1. ServerFactoryBean * - 不设置注解也可以发布webservice服务, 不支持注解 * - 不支持拦截器的添加 * 2. JaxWsServerFactoryBean * - 支持注解 * - 可以添加拦截器 * 3. webservice 访问流程: * 1. 检测本地代理描述的wsdl是否与服务端的wsdl一致 ,俗称为握手 * 2. 通过soap协议实现通信 ,采用的是post请求 , 数据封装在满足soap规约的xml中 * 3. 返回数据 同样采用的是soap通信, 数据封装在满足soap规约的xml中 * @param args public static void main(String[] args) { LanguageService languageService=new LanguageServiceImpl(); ServerFactoryBean bean=new ServerFactoryBean(); //Endpoint :地址 , 实现对象 bean.setAddress("http://192.168.114.10:9999/ws/cxf/languangeService"); bean.setServiceClass(LanguageService.class);//对外提供webservcie的业务类或者接口 bean.setServiceBean(languageService);//服务的实现bean bean.create();//创建,发布webservice System.out.println("wsdl地址:http://192.168.114.10:9999/ws/cxf/languangeService?WSDL"); } */ public static void main(String[] args) { LanguageService languageService=new LanguageServiceImpl(); JaxWsServerFactoryBean bean=new JaxWsServerFactoryBean(); //Endpoint :地址 , 实现对象 bean.setAddress("http://192.168.114.10:9999/ws/cxf/languangeService"); bean.setServiceClass(LanguageService.class);//对外提供webservcie的业务类或者接口 bean.setServiceBean(languageService);//服务的实现bean //添加输入拦截器 :输入显示日志信息的拦截器 bean.getInInterceptors().add(new LoggingInInterceptor()); //添加输出拦截器 :输出显示日志信息的拦截器 bean.getOutInterceptors().add(new LoggingOutInterceptor()); bean.create();//创建,发布webservice System.out.println("wsdl地址:http://192.168.114.10:9999/ws/cxf/languangeService?WSDL"); } }CXF与Spring集成建立一个web项目准备所有jar包,将CXF_HOME\lib项目下的所有jar包,全部都拷贝新项目的lib目录下.其中里面已经包含了Sring3.0的jar包 其中jetty 服务器的包可以不要.因为我们要部署的tomcat服务器中了在web.xml中配置cxf的核心servlet,CXFServlet此配置文件的作用类 拦截/ws/*的所有请求 类似Struts2的过滤器web.xml配置文件:<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <display-name>CXF_Server</display-name> <!-- 添加 CXF 的Servlet ,处理 webservice的请求 --> <servlet> <servlet-name>cxf</servlet-name> <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>cxf</servlet-name> <url-pattern>/ws/*</url-pattern> </servlet-mapping> <!-- Spring 监听添加 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> </web-app>实体:public class Employee { private Integer id; private String name; private Integer age; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } }接口:package cn.it.ws.cxf.b; import java.util.List; import javax.jws.WebParam; import javax.jws.WebResult; import javax.jws.WebService; import cn.it.ws.cxf.bean.Employee; @WebService(serviceName="EmployeeService") public interface EmployeeManager { void add(@WebParam(name="employee")Employee employee); @WebResult(name="employees")List<Employee> query(); }接口实现:package cn.it.ws.cxf.b; import java.util.ArrayList; import java.util.List; import cn.it.ws.cxf.bean.Employee; /**员工管理的业务实现类 * @author 李俊 2015年5月17日 */ public class EmployeeManagerImpl implements EmployeeManager { private List<Employee> employees=new ArrayList<>(); @Override public void add(Employee employee){ //添加到集合中 employees.add(employee); } @Override public List<Employee> query(){ return employees; } }Spring配置信息:<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:jaxws="http://cxf.apache.org/jaxws" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd"> <bean id="employeeManagerImpl" class="cn.it.ws.cxf.b.EmployeeManagerImpl"></bean> <!-- 配置cxf 地址: http://192.168.114.10:8080/CXF_Server/ws/employeeManager 组成 : http://192.168.114.10:8080 +CXF_Server( 项目名)+ws(过滤的路径)+/employeeManager(自定义部分) 服务类 : 服务的实现类: 拦截器 --> <jaxws:server address="/employeeManager" serviceClass="cn.it.ws.cxf.b.EmployeeManager"> <jaxws:serviceBean> <ref bean="employeeManagerImpl"/> </jaxws:serviceBean> <!-- 配置输入显示日志信息的拦截器 --> <jaxws:inInterceptors> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor"></bean> </jaxws:inInterceptors> <jaxws:outInterceptors> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor"></bean> </jaxws:outInterceptors> </jaxws:server> </beans>IDEA下使用webservice我们的Intellij idea是一个非常好用的java ide,当然了,它也支持webservice开发。非常好用…由于在网上见到的教程非常多,我就贴几个我认为比较好的教程:http://www.biliyu.com/article/986.htmlhttp://blog.csdn.net/u010323023/article/details/52926051http://blog.csdn.net/dreamfly88/article/details/52350370获取天气预报我们现在webservice就基本入门了,现在我想要做的就是自己写的网站能够拿到天气预报的信息,于是我去http://www.webxml.com.cn/zh_cn/index.aspx找到了天气预报的服务这个是天气预报的WSDL地址:http://ws.webxml.com.cn/WebServices/WeatherWS.asmx,那么我们只要解析该WSDL服务即可这里写图片描述如果不想得到所有的信息,那么我们可以在服务上找到我们想要对应的数据,也就是说:这里写图片描述这里写图片描述总结应用webservice的原因就在于我们需要一些服务、这些服务是我们自己不能手动写的。比如天气预报,于是就出现了webService技术。webService能够让我们可以获取网上别人发布出来的服务。我们只要调用它,就可以获取相关的数据了。Socket其实就是对TCP/IP协议的一个封装,而我们在网上使用的是HTTP协议。WebService也是Web应用程序。它也当然支持HTTP协议了。不过WebService需要给不同语言都能够使用,因此它使用XML来进行传输。于是,它就有自己一种协议:SOAP(简单对象访问协议)。其实SOAP就是Http+XML。我们可以使用http-get方式访问webservice,由于它使用的是原生Socket来进行访问。会有点复杂。于是我们可以借助Http-Client 框架来访问WebService。Http-Client 框架比HTTP-GET方式会简单一点。但还是不够简洁。最后,我们可以使用Java自带的WsImport来实现本地代理。这种方法会将WebService翻译成Java类,我们使用类一样去访问WebService就行了。非常好用。我们是可以自己写webService的。对服务类上加上注解。通过EndPoint(端点服务)就能够把我们webService服务类发布出去了。为了让WDSL文件更加读取,可以使用注解的方式来写好对应的参数名称。也可以控制某方法是否被发布出去SOAP其实上就是使用XML进行传输的HTTP协议。SOA:面向服务架构。即插即用。也就是耦合非常低,用的时候加上就行了。UDDI (Universal Description, Discovery and Integration)统一描述、发现、集成,其实就是一个webservice的目录结构,不过我们很少把webservice发布到上面去实现接口的webservice只是在类上对其的一种抽象而已,没什么大不了的。CXF框架可以与spring无缝连接,就不用我们自己Endpoint了。它还能记录日志之类的。我们还可以使用Idea下的webservice,能够使用图形画面的方式获取本地代理和生成WSDL文件。
WebService介绍首先我们来谈一下为什么需要学习webService这样的一个技术吧….问题一如果我们的网站需要提供一个天气预报这样一个需求的话,那我们该怎么做?????天气预报这么一个功能并不是简单的JS组件就能够实现的,它的数据是依赖数据库分析出来的,甚至需要卫星探测..我们个人建站是不可能搞这么一个数据库的吧。那么既然我们自己干不了,我们可以去找别人吗???我们从搜索引擎搜索,可以发现很多提供天气预报的网站,但是它返回的是一个网页,而我们仅仅需要的是对应的数据!我们可能就在想,我们能不能仅仅只要它返回的数据,而并不是经过加工处理后返回的网页呢??于是乎,webService就诞生了,webservice就是一个部署在Web服务器上的,它向外界暴露出一个能够通过Web进行调用的API。也就是说:当我们想要获取天气预报的信息,我们可以调用别人写好的service服务,我们调用就能够得到结果了!问题二可是我们写网站主流的就有好几个平台:Java、.net、PHP等等,那么部署在Web服务器上的服务器也就是webserice怎么能够就让我们不同的平台都能够调用呢??我们知道java、.net这样的平台他们语言的基本数据类型、复杂数据类型就可能不一样,那么怎么能够实现调用的呢???来引用一段话大家在写应用程序查询数据库时,并没有考虑过为什么可以将查询结果返回给上层的应用程序,甚至认为,这就是数据库应该做的,其实不然,这是数据库通过TCP/IP协议与另一个应用程序进行交流的结果,而上层是什么样的应用程序,是用什么语言,数据库本身并不知道,它只知道接收到了一份协议,这就是SQL92查询标准协议。无论是Java、.net、PHP等等的平台,只要是网页开发都是可以通过http协议来进行通信的,并且返回的数据要是通用的话,那么我们早就学过这样的一种技术【XML】所以webservice实际上就是http+XML这里写图片描述对webservice的理解WebService,顾名思义就是基于Web的服务。它使用Web(HTTP)方式,接收和响应外部系统的某种请求。从而实现远程调用.我们可以调用互联网上查询天气信息Web服务,然后将它嵌入到我们的程序(C/S或B/S程序)当中来,当用户从我们的网点看到天气信息时,他会认为我们为他提供了很多的信息服务,但其实我们什么也没有做,只是简单调用了一下服务器上的一段代码而已。学习WebService可以将你的服务(一段代码)发布到互联网上让别人去调用,也可以调用别人机器上发布的WebService,就像使用自己的代码一样.。回顾Socket我们在学习Java基础网络编程章节已经知道了Scoket这么一个连接了。Socket服务端public class SocketSer { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(6666); boolean flag = true; while (flag) { //接收客户端的请求 System.out.println("监听客户端的数据:"); Socket sc = ss.accept(); InputStream is = sc.getInputStream(); byte[] buffer = new byte[1024]; int len = -1; len = is.read(buffer); String getData = new String(buffer, 0, len); System.out.println("从客户端获取的数据:" + getData); //业务处理 大小写转化 String outPutData = getData.toUpperCase(); //向客户端写数据 OutputStream os = sc.getOutputStream(); os.write(outPutData.getBytes("UTF-8")); //释放资源 os.close(); is.close(); sc.close(); } ss.close(); } }Socket客服端public class SocketClient { public static void main(String[] args) throws Exception { //获取用户输入的数据 Scanner input = new Scanner(System.in); System.out.println("请输入数据:"); String inputData = input.nextLine(); //开启一个Socket端口 Socket sc = new Socket("127.0.0.1", 6666); OutputStream os = sc.getOutputStream(); os.write(inputData.getBytes()); //获取服务端回传的数据 InputStream is = sc.getInputStream(); byte[] buffer = new byte[1024]; int len = -1; len = is.read(buffer); String getData = new String(buffer, 0, len); System.out.println("从服务端获取的数据:" + getData); //是否流 is.close(); os.close(); sc.close(); } }当我们从客户端输入数据以后,那么服务端就会把数据转成是大写这里写图片描述这里写图片描述其实HTTP协议就是基于Socket对其进行封装,我们也可以在IE浏览器中对其进行访问.我们一样能够获取得到数据!这里写图片描述这里写图片描述Scoket与HTTP简述这里写图片描述ISO的七层模型 : 物理层、数据链路层、网络层、传输层、表示层、会话层、应用层Socket访问 : Socket属于传输层,它是对Tcp/ip协议的实现,包含TCP/UDP,它是所有通信协议的基础,Http协议需要Socket支持,以Socket作为基础Socket通信特点:开启端口,该通信是 长连接的通信 ,很容易被防火墙拦截,可以通过心跳机制来实现 ,开发难度大传输的数据一般是字符串 ,可读性不强socket端口不便于推广性能相对于其他的通信协议是最优的Http协议访问 :属于应用层的协议,对Socket进行了封装跨平台传数据不够友好对第三方应用提供的服务,希望对外暴露服务接口问题:**数据封装不够友好 :可以用xml封装数据 **希望给第三方应用提供web方式的服务 (http + xml) = web ServicewebService相关术语名词1:XML. Extensible Markup Language -扩展性标记语言XML,用于传输格式化的数据,是Web服务的基础。namespace-命名空间。xmlns=“http://itcast.cn” 使用默认命名空间。xmlns:itcast=“http://itcast.cn”使用指定名称的命名空间。名词2:WSDL – WebService Description Language – Web服务描述语言。通过XML形式说明服务在什么地方-地址。通过XML形式说明服务提供什么样的方法 – 如何调用。名词3:SOAP-Simple Object Access Protocol(简单对象访问协议)Envelope – 必须的部分。以XML的根元素出现。Headers – 可选的。Body – 必须的。在body部分,包含要执行的服务器的方法。和发送到服务器的数据。SOAP作为一个基于XML语言的协议用于有网上传输数据。SOAP = 在HTTP的基础上+XML数据。SOAP是基于HTTP的。SOAP的组成如下:
定义统一异常处理器类public class CustomExceptionResolver implements HandlerExceptionResolver { //前端控制器DispatcherServlet在进行HandlerMapping、调用HandlerAdapter执行Handler过程中,如果遇到异常就会执行此方法 //handler最终要执行的Handler,它的真实身份是HandlerMethod //Exception ex就是接收到异常信息 @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { //输出异常 ex.printStackTrace(); //统一异常处理代码 //针对系统自定义的CustomException异常,就可以直接从异常类中获取异常信息,将异常处理在错误页面展示 //异常信息 String message = null; CustomException customException = null; //如果ex是系统 自定义的异常,直接取出异常信息 if(ex instanceof CustomException){ customException = (CustomException)ex; }else{ //针对非CustomException异常,对这类重新构造成一个CustomException,异常信息为“未知错误” customException = new CustomException("未知错误"); } //错误 信息 message = customException.getMessage(); request.setAttribute("message", message); try { //转向到错误 页面 request.getRequestDispatcher("/WEB-INF/jsp/error.jsp").forward(request, response); } catch (ServletException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return new ModelAndView(); } }配置统一异常处理器<!-- 定义统一异常处理器 --> <bean class="cn.itcast.ssm.exception.CustomExceptionResolver"></bean>这里写图片描述RESTful支持我们在学习webservice的时候可能就听过RESTful这么一个名词,当时候与SOAP进行对比的…那么RESTful究竟是什么东东呢???RESTful(Representational State Transfer)软件开发理念,RESTful对http进行非常好的诠释。如果一个架构支持RESTful,那么就称它为RESTful架构…以下的文章供我们了解:http://www.ruanyifeng.com/blog/2011/09/restful综合上面的解释,我们总结一下什么是RESTful架构: (1)每一个URI代表一种资源; (2)客户端和服务器之间,传递这种资源的某种表现层; (3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。关于RESTful幂等性的理解:http://www.oschina.net/translate/put-or-post简单来说,如果对象在请求的过程中会发生变化(以Java为例子,属性被修改了),那么此是非幂等的。多次重复请求,结果还是不变的话,那么就是幂等的。PUT用于幂等请求,因此在更新的时候把所有的属性都写完整,那么多次请求后,我们其他属性是不会变的在上边的文章中,幂等被翻译成“状态统一性”。这就更好地理解了。其实一般的架构并不能完全支持RESTful的,因此,只要我们的系统支持RESTful的某些功能,我们一般就称作为支持RESTful架构…url的RESTful实现非RESTful的http的url:http://localhost:8080/items/editItems.action?id=1&….RESTful的url是简洁的:http:// localhost:8080/items/editItems/1更改DispatcherServlet的配置从上面我们可以发现,url并没有.action后缀的,因此我们要修改核心分配器的配置<!-- restful的配置 --> <servlet> <servlet-name>springmvc_rest</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 加载springmvc配置 --> <init-param> <param-name>contextConfigLocation</param-name> <!-- 配置文件的地址 如果不配置contextConfigLocation, 默认查找的配置文件名称classpath下的:servlet名称+"-serlvet.xml"即:springmvc-serlvet.xml --> <param-value>classpath:spring/springmvc.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>springmvc_rest</servlet-name> <!-- rest方式配置为/ --> <url-pattern>/</url-pattern> </servlet-mapping>在Controller上使用PathVariable注解来绑定对应的参数//根据商品id查看商品信息rest接口 //@RequestMapping中指定restful方式的url中的参数,参数需要用{}包起来 //@PathVariable将url中的{}包起参数和形参进行绑定 @RequestMapping("/viewItems/{id}") public @ResponseBody ItemsCustom viewItems(@PathVariable("id") Integer id) throws Exception{ //调用 service查询商品信息 ItemsCustom itemsCustom = itemsService.findItemsById(id); return itemsCustom; }当DispatcherServlet拦截/开头的所有请求,对静态资源的访问就报错:我们需要配置对静态资源的解析<!-- 静态资源 解析 --> <mvc:resources location="/js/" mapping="/js/**" /> <mvc:resources location="/img/" mapping="/img/**" />/**就表示不管有多少层,都对其进行解析,/*代表的是当前层的所有资源..SpringMVC拦截器在Struts2中拦截器就是我们当时的核心,原来在SpringMVC中也是有拦截器的用户请求到DispatherServlet中,DispatherServlet调用HandlerMapping查找Handler,HandlerMapping返回一个拦截的链儿(多个拦截),springmvc中的拦截器是通过HandlerMapping发起的。实现拦截器的接口:public class HandlerInterceptor1 implements HandlerInterceptor { //在执行handler之前来执行的 //用于用户认证校验、用户权限校验 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("HandlerInterceptor1...preHandle"); //如果返回false表示拦截不继续执行handler,如果返回true表示放行 return false; } //在执行handler返回modelAndView之前来执行 //如果需要向页面提供一些公用 的数据或配置一些视图信息,使用此方法实现 从modelAndView入手 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("HandlerInterceptor1...postHandle"); } //执行handler之后执行此方法 //作系统 统一异常处理,进行方法执行性能监控,在preHandle中设置一个时间点,在afterCompletion设置一个时间,两个时间点的差就是执行时长 //实现 系统 统一日志记录 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("HandlerInterceptor1...afterCompletion"); } }配置拦截器<!--拦截器 --> <mvc:interceptors> <!--多个拦截器,顺序执行 --> <!-- <mvc:interceptor> <mvc:mapping path="/**" /> <bean class="cn.itcast.ssm.controller.interceptor.HandlerInterceptor1"></bean> </mvc:interceptor> <mvc:interceptor> <mvc:mapping path="/**" /> <bean class="cn.itcast.ssm.controller.interceptor.HandlerInterceptor2"></bean> </mvc:interceptor> --> <mvc:interceptor> <!-- /**可以拦截路径不管多少层 --> <mvc:mapping path="/**" /> <bean class="cn.itcast.ssm.controller.interceptor.LoginInterceptor"></bean> </mvc:interceptor> </mvc:interceptors>测试执行顺序如果两个拦截器都放行测试结果: HandlerInterceptor1...preHandle HandlerInterceptor2...preHandle HandlerInterceptor2...postHandle HandlerInterceptor1...postHandle HandlerInterceptor2...afterCompletion HandlerInterceptor1...afterCompletion 总结: 执行preHandle是顺序执行。 执行postHandle、afterCompletion是倒序执行1 号放行和2号不放行测试结果: HandlerInterceptor1...preHandle HandlerInterceptor2...preHandle HandlerInterceptor1...afterCompletion 总结: 如果preHandle不放行,postHandle、afterCompletion都不执行。 只要有一个拦截器不放行,controller不能执行完成1 号不放行和2号不放行测试结果: HandlerInterceptor1...preHandle 总结: 只有前边的拦截器preHandle方法放行,下边的拦截器的preHandle才执行。日志拦截器或异常拦截器要求将日志拦截器或异常拦截器放在拦截器链儿中第一个位置,且preHandle方法放行拦截器应用-身份认证拦截器拦截public class LoginInterceptor implements HandlerInterceptor { //在执行handler之前来执行的 //用于用户认证校验、用户权限校验 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //得到请求的url String url = request.getRequestURI(); //判断是否是公开 地址 //实际开发中需要公开 地址配置在配置文件中 //... if(url.indexOf("login.action")>=0){ //如果是公开 地址则放行 return true; } //判断用户身份在session中是否存在 HttpSession session = request.getSession(); String usercode = (String) session.getAttribute("usercode"); //如果用户身份在session中存在放行 if(usercode!=null){ return true; } //执行到这里拦截,跳转到登陆页面,用户进行身份认证 request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response); //如果返回false表示拦截不继续执行handler,如果返回true表示放行 return false; } //在执行handler返回modelAndView之前来执行 //如果需要向页面提供一些公用 的数据或配置一些视图信息,使用此方法实现 从modelAndView入手 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("HandlerInterceptor1...postHandle"); } //执行handler之后执行此方法 //作系统 统一异常处理,进行方法执行性能监控,在preHandle中设置一个时间点,在afterCompletion设置一个时间,两个时间点的差就是执行时长 //实现 系统 统一日志记录 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("HandlerInterceptor1...afterCompletion"); } }Controller@Controller public class LoginController { //用户登陆提交方法 @RequestMapping("/login") public String login(HttpSession session, String usercode,String password)throws Exception{ //调用service校验用户账号和密码的正确性 //.. //如果service校验通过,将用户身份记录到session session.setAttribute("usercode", usercode); //重定向到商品查询页面 return "redirect:/items/queryItems.action"; } //用户退出 @RequestMapping("/logout") public String logout(HttpSession session)throws Exception{ //session失效 session.invalidate(); //重定向到商品查询页面 return "redirect:/items/queryItems.action"; } }总结使用Spring的校验方式就是将要校验的属性前边加上注解声明。在Controller中的方法参数上加上@Validation注解。那么SpringMVC内部就会帮我们对其进行处理(创建对应的bean,加载配置文件)BindingResult可以拿到我们校验错误的提示分组校验就是将让我们的校验更加灵活:某方法需要校验这个属性,而某方法不用校验该属性。我们就可以使用分组校验了。对于处理异常,SpringMVC是用一个统一的异常处理器类的。实现了HandlerExceptionResolver接口。对模块细分多个异常类,都交由我们的统一异常处理器类进行处理。对于RESTful规范,我们可以使用SpringMVC简单地支持的。将SpringMVC的拦截.action改成是任意的。同时,如果是静态的资源文件,我们应该设置不拦截。对于url上的参数,我们可以使用@PathVariable将url中的{}包起参数和形参进行绑定SpringMVC的拦截器和Struts2的拦截器差不多。不过SpringMVC的拦截器配置起来比Struts2的要简单。至于他们的拦截器链的调用顺序,和Filter的是没有差别的。
用户与角色之间的关系我们在做用户模块的时候,漏掉了最后一个功能。在新增功能中是可以选择角色的。这里写图片描述用户与角色之间的关系也是多对多一个用户对应多个角色一个角色可以被多个用户使用。这里写图片描述现在呢,我们的用户表已经是写的了。我们最好就不要修改原有的用户表数据。那我们在不修改用户表代码的情况下,又怎么来实现多对多呢??跟角色与权限是一样的。使用中间表来维护它们的关系就行了。用户:user 用户id,名称... 1 用户1 2 用户2 用户角色:user_role 用户id,角色id 1 1 1 2 2 2 角色:role 角色Id,名称 1 管理员 2 一般用户设计中间表public class UserRole implements Serializable { private UserRoleId userRoleId; public UserRoleId getUserRoleId() { return userRoleId; } public void setUserRoleId(UserRoleId userRoleId) { this.userRoleId = userRoleId; } }主键表public class UserRoleId implements Serializable { private String user_id; //在使用的时候,Role相关的数据会用得特别多。为了方便使用了Role对象。而user就不需要使用User对象了。 private Role role; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserRoleId that = (UserRoleId) o; if (user_id != null ? !user_id.equals(that.user_id) : that.user_id != null) return false; return role != null ? role.equals(that.role) : that.role == null; } @Override public int hashCode() { int result = user_id != null ? user_id.hashCode() : 0; result = 31 * result + (role != null ? role.hashCode() : 0); return result; } public String getUser_id() { return user_id; } public void setUser_id(String user_id) { this.user_id = user_id; } public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } }映射文件<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="zhongfucheng.user.entity.UserRole" table="user_role"> <composite-id name="userRoleId" class="zhongfucheng.user.entity.UserRoleId"> <!--manytoone可以生成外键字段。--> <key-many-to-one name="role" class="zhongfucheng.role.entity.Role" column="role_id" lazy="false"/> <key-property name="user_id" column="user_id" type="java.lang.String"/> </composite-id> </class> </hibernate-mapping>增加模块在跳转到JSP页面的前,把所有的角色找出来。放到request域对象中,让JSP页面显示出来。public String addUI() { //把所有的角色查询出来,带过去给JSP页面显示 ActionContext.getContext().getContextMap().put("roleList", roleServiceImpl.findObjects()); return "addUI"; }<%-- list是集合对象 name是要带给服务器端的字符串数组。 listkey 是集合元素对象的id listValue 是集合元素对象的名字 --%> <s:checkboxlist list="#roleList" name="userRoleIds" listKey="roleId" listValue="name"/>这里写图片描述编辑模块编辑回显数据在编辑模块中,需要将该用户所拥有的角色查询出来。然后把查询出来的id值放到数组中。public String editUI() { //把所有的角色查询出来,带过去给JSP页面显示 ActionContext.getContext().getContextMap().put("roleList", roleServiceImpl.findObjects()); //外边已经传了id过来了,我们要找到id对应的User if (user != null &&user.getId() != null ) { //直接获取出来,后面JSP会根据User有getter就能读取对应的信息! user = userServiceImpl.findObjectById(user.getId()); //通过用户的id得到所拥有UserRole List<UserRole> roles = userServiceImpl.findRoleById(user.getId()); //把用户拥有角色的id填充到数组中,数组最后回显到JSP页面 int i=0; userRoleIds = new String[roles.size()]; for (UserRole role : roles) { userRoleIds[i++] = role.getUserRoleId().getRole().getRoleId(); } } return "editUI"; }JSP通过checkboxlist进行回显,指定了name值就能够自动判定我们的用户拥有的角色是什么了。<s:checkboxlist list="#roleList" name="userRoleIds" listKey="roleId" listValue="name"></s:checkboxlist>这里写图片描述处理编辑操作在更新之前,首先删除用户与角色之间的关系【历史遗留问题】,如果不删除,那么用户所拥有的角色就一直保留着。无论你在JSP页面有没有勾选。public String edit() throws IOException { //Struts2会自动把JSP带过来的数据封装到User对象上 if (user.getId() != null && user != null) { if (headImg != null) { //得到要把头像上传到服务器的路径 javax.servlet.ServletContext servletContext = ServletActionContext.getServletContext(); String realPath = servletContext.getRealPath("upload/user"); //由于用户上传的名字可能会相同,如果相同就被覆盖掉,因此我们要修改上传文件的名字【独一无二】 headImgFileName = UUID.randomUUID().toString() + headImgFileName.substring(headImgFileName.lastIndexOf(".")); FileUtils.copyFile(headImg, new File(realPath, headImgFileName)); //设置图片与用户的关系 user.setHeadImg(headImgFileName); } if (userRoleIds != null) { //删除用户与角色之间的关系【历史遗留问题】 userServiceImpl.deleteUserRoleById(userRoleIds); //保存用户与角色。 userServiceImpl.saveUserAndRole(user,userRoleIds); } } return "list"; }调用保存用户与角色的关系。如果id不是为空的,那么就执行更新,如果id为空,就执行保存。@Override public void saveUserAndRole(User user, String... userRoleIds) { //保存或更新用户 if (user.getId() != null) { userDaoImpl.update(user); } else { userDaoImpl.save(user); } //判断有没有把id带过来 if (userRoleIds != null) { for (String userRoleId : userRoleIds) { System.out.println(userRoleId); userDaoImpl.saveUserRole(new UserRole(new UserRoleId(user.getId(), new Role(userRoleId)))); } } }总结我们在本次的用户角色权限关系中,没有使用权限表来保存对应的权限。因为我们的权限都被我们固定了,没必要多使用一张数据库表了。因此,我们使用了一个静态Map集合来保存我们的权限数据。之所以用Map是因为我们在页面上还需要通过名称来获取对应的权限。在角色的集合中,如果我们有权限数据表,那我们的保存的是Privilege类型的数据。但是现在我们没有数据库表,因此保存的是Role_Privilege的关系。这样的话,我们就可以通过角色来获取对应的权限了。而对于Role_privilege而言,仅仅只有两个外键。我们设置成复合主键的话要满足以下条件将两个外键封装成一个JavaBean对象,该JavaBean对象要实现Seriliable接口重写equals()和hashCode()方法在设计Role_privilege的时候,我们考虑到了:当我们查看角色获取所有权限的时候,如果该集合保存的是roleId的话,那么我们仅仅只能获取角色的id。但如果我们存储的是Role对象的话,我们就可以直接获取角色的名称了。在数据回显上也有个技巧。我们是使用静态Map集合来装载所有的权限的。而Web端传递过来的是代表权限的Code值。我们在显示的时候就可以把整个Map集合传过去。然后把代表权限的Code值也传过去。展现出有权限Code的那一部分数据。修改角色权限的时候,Hibernate自动会把我们的本来用户的权限查询数据。然后追加那些我们勾选中的权限。在修改前,我们可以把原有的权限干掉,然后再把我们勾选后的权限修改起来。这样就达到我们的效果了。由于我们的用户和角色也是多对多的关系的。我们不想破坏之前已经写好的JavaBean对象。我们也是可以使用中间表来保存我们关联数据的。checkboxlist是Struts2为我们提供的标签,能够遍历集合生成多选框。它会自动将我们的id值通过字符串数组的方式传入Controlller中我们controller使用字符串数据得到勾选的id值。当我们要编辑页面的时候,通过用户的id得到用户的所有信息(包括用户所对应的角色)。我们将得到的角色集合遍历,把角色的id封装到我们的字符串数组中(主要为了通过checkboxList标签回显数据)。如果我们不使用checkboxList的话,那我们得到用户的所有角色,就可以直接返回给页面来进行显示了。我们在service层还可以通过判断用户的id的值是否为null,来执行保存或更新的操作。
处理首页当用户访问首页的时候,我们重定向到登陆页面:访问Index页面的时候,我们就重定向到登陆页面上。<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; response.sendRedirect(basePath + "sys/login_loginUI.action"); %>过滤器模块进入系统拦截我们讲道理是要用户登陆后,才能访问我们的总系统。但是现在假如用户知道了我们的首页地址,他可以直接访问我们的首页地址而不用登陆。这是不合适的。因此,我们写一个过滤器进行拦截,如果用户不是想要登陆,而访问我们其他的页面。都拦截他,让他登陆后才能访问。这里写图片描述过滤器:package zhongfucheng.core.filter; import zhongfucheng.user.entity.User; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Created by ozc on 2017/6/4. */ public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; //得到用户访问的路径 String uri = request.getRequestURI(); //登陆路径 String loginPath = request.getContextPath() + "/sys/login_login.action"; //判断用户访问的是哪里 if (!uri.contains("login_")) {//如果不是访问我们的登陆模块 //判断该用户是否登陆了。 User user = (User) request.getSession().getAttribute("SYS_USER"); if (user == null) {//如果在session找不到,那么就是没有登陆 //没有登陆,跳转到登陆页面 response.sendRedirect(loginPath); return; } else {//有用户信息,就是登陆了。 //放行 chain.doFilter(request, response); } } else {//如果是访问我们的登陆模块,放行 chain.doFilter(request, response); } } public void init(FilterConfig config) throws ServletException { } public void destroy() { } }配置过滤器,需要在struts过滤器之前配置:<filter> <filter-name>LoginFilter</filter-name> <filter-class>zhongfucheng.core.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>*.action</url-pattern> </filter-mapping>权限过滤我们的纳税服务子系统并不是任何人都可以进去操作的,我们有可以对角色的管理,对用户的管理。。一般的用户是没有权限去操作这些东西的。因此,我们要对其进行权限控制。当该用户有权限才能够访问纳税服务系统的内容,没有权限就不给该用户看。权限过滤的前提条件:用户已经登陆了知道用户访问的是什么子系统由于我们在LoginFilter中已经可以得到这两个条件了,于是我们在LoginFilter中接着写就行了。又因为权限过滤是一个比较单独的模块,我们可以将其抽出。这样一来,LoginFilter又不会显得太大,职责又分工了。这里写图片描述过滤器全部代码:WebApplicationContextUtils得到IOC中的对象package zhongfucheng.core.filter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import zhongfucheng.core.utils.PermissionCheck; import zhongfucheng.user.entity.User; import zhongfucheng.user.service.UserService; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Created by ozc on 2017/6/4. */ public class LoginFilter implements Filter { //注入userService @Autowired private UserService userServiceImpl; public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; //得到用户访问的路径 String uri = request.getRequestURI(); //登陆路径 String loginPath = request.getContextPath() + "/sys/login_login.action"; //提示页面 String warningPath = request.getContextPath() + "/sys/login_noPermissionUI.action"; //定义User变量 User user; //判断用户访问的是哪里 if (!uri.contains("login_")) {//如果不是访问我们的登陆模块 //判断该用户是否登陆了。 user = (User) request.getSession().getAttribute("SYS_USER"); if (user == null) {//如果在session找不到,那么就是没有登陆 //没有登陆,跳转到登陆页面 response.sendRedirect(loginPath); return; } else {//有用户信息,就是登陆了。 if (uri.contains("nsfw")) {//如果访问纳税服务系统,就要有对应的权限 //用户已经登陆了,判断用户有没有权限访问子系统 //得到IOC容器中的对象 WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext()); PermissionCheck permissionCheck = (PermissionCheck)applicationContext.getBean("permissionCheck"); if (permissionCheck.check(user, "nsfw")) {//有权限 //放行 chain.doFilter(request, response); } else {//没有权限 //返回到提示页面 response.sendRedirect(warningPath); } } else {//可以不用权限,直接放行 //放行 chain.doFilter(request, response); } } } else {//如果是访问我们的登陆模块,放行 chain.doFilter(request, response); } } public void init(FilterConfig config) throws ServletException { } public void destroy() { } }在登陆完之后,就查询出用户拥有的所有角色,并设置到该用户中:public String login() { if (user != null) { List<User> list = userServiceImpl.findUserByAccountAndPassword(user.getAccount(), user.getPassword()); //如果查到有值,那么就证明有该用户的,给他登陆 if (list != null && list.size() > 0) { //查出用户所有的权限,设置到User中 User user = list.get(0); List<UserRole> roles = userServiceImpl.findRoleById(user.getId()); user.setUserRoles(roles); //保存到Session域中,为了更方便用,我们使用常量保存。 ActionContext.getContext().getSession().put(Constant.USER, user); //保存到日志文件中Log Log log = LogFactory.getLog(getClass()); log.info("用户名称为" + list.get(0).getName() + "登陆了系统!"); //重定向到首页 return "home"; } else { //登陆失败,记载登陆信息 loginResult = "登陆失败了,用户名或密码错误了"; } } //只要不成功的,都回到登陆页面 return loginUI(); }在User.java中加入一个List集合,存储着用户所拥有的角色//得到用户所有的角色 private List<UserRole> userRoles; public List<UserRole> getUserRoles() { return userRoles; } public void setUserRoles(List<UserRole> userRoles) { this.userRoles = userRoles; }到这里,有同学可能会疑问,为啥现在我要修改User的结构呢??明明在编写User和Role的时候说好不修改User类的。我们在验证的时候需要得到用户所有的角色,从而得到权限。如果在检查的时候做的话,我们用的是过滤器检查,每请求一次都要去访问数据库。这样的话就非常耗费我们的性能,于是我们就修改User类,但这次的修改没有影响到我们其他地方的操作。这样一来,我们在检查的时候就可以通过对象来得到用户对应的权限了,不用查询数据库。检查用户是否有权限:public class PermissionCheck { private User user; private String code; @Autowired private UserService userServiceImpl; public boolean check(User user, String code) { this.user = user; this.code = code; //得到该用户的所有权限 List<UserRole> userRoles = user.getUserRoles(); if (userRoles == null) { userRoles = userServiceImpl.findRoleById(user.getId()); } //遍历初用户拥有的角色,看看有没有对应的权限 for (UserRole userRole : userRoles) { Role role = userRole.getUserRoleId().getRole(); //得到角色所拥有的权限 Set<RolePrivilege> rolePrivilegeSet = role.getRolePrivilegeSet(); //遍历权限,看看有没有nsfw的权限 for (RolePrivilege privilege : rolePrivilegeSet) { String code1 = privilege.getCompositeKey().getCode(); if (code1.equals(code)) {//如果该用户有权限 return true; } } } //遍历完都没有return true,那么就是没有权限了。 return false; } }页面嵌套问题现在我打开了两个首页,是同一个会话的。如果用户太久没有操作我们的页面,那么Session就会被摧毁。这里写图片描述等用户再操作的时候,Session已经被Tomcat摧毁了。讲道理用户操作页面的时候,是会回到登陆页面的。我们看看发生了什么:这里写图片描述登陆页面嵌套在我们右边的显示页面了。为啥出现这种情况??当用户的Session没有了,用户操作时,过滤器就会将页面跳转到登陆页面而我们点击了左边的菜单栏,默认在右边显示。所以,到目前为止,我们的逻辑是没毛病的。但怎么解决上面遇到的情况呢??** 我们不需要使用监听器Session,监听Session被摧毁了,然后刷新页面**。。我们用更好地一种解决办法:判断自身页面是否为顶级窗口,如果不是就自动刷新父窗口的地址,跳转到顶级窗口中。/*如果该页面不是顶级窗口,那么就自动刷新一下到父窗口中*/ if(window!=window.parent) { window.parent.location.reload(true); }总结对于登陆和注销功能就没什么好说的,我们已经写过很多次了。在登陆的时候就是将我们的User对象保存Session域对象中而已。当用户访问index页面的时候,我们就让它重定向到登陆页面上只有登陆了才能访问我们的系统首页,因此我们编写了一个过滤器。判断该用户是否是要访问我们的登陆页面,如果不是,就判断该用户是否登陆了(也就是判断session有没有User值)。如果有就放行,如果没有就跳转到登陆页面上我们还可以对其进行权限认证,权限认证是基于用户已经登陆的前提下的。对于权限我们直接使用权限的Code来进行校验。为了方便我们去验证,我们在登陆的时候就把用户的权限全都加载到用户上(这样的话,在每次验证就不用重复去找数据库要数据了。)session失效的问题导致了页面的嵌套。我们无需监听Session失效的问题,直接在javaScript用代码判断该页面是否是顶级页面,如果不是顶级页面,直接就跳转到顶级页面就行了!
什么是SpringMVC?SpringMVC是Spring家族的一员,Spring是将现在开发中流行的组件进行组合而成的一个框架!它用在基于MVC的表现层开发,类似于struts2框架为什么要使用SpringMVC?我们在之前已经学过了Struts2这么一个基于MVC的框架….那么我们已经学会了Struts2,为啥要要学习SpringMVC呢???下面我们来看一下Struts2不足之处:有漏洞【详细可以去搜索】运行速度较慢【比SpringMVC要慢】配置的内容较多【需要使用Struts.xml文件】比较重量级基于这么一些原因,并且业内现在SpringMVC已经逐渐把Struts2给替代了…因此我们学习SpringMVC一方面能够让我们跟上业界的潮流框架,一方面SpringMVC确实是非常好用!可以这么说,Struts2能做的东西,SpringMVC也能够做….回顾Struts2开发在Struts2中,我们的开发特点是这样的:Action类继承着ActionSupport类【如果要使用Struts2提供的额外功能,就要继承它】Action业务方法总是返回一个字符串,再由Struts2内部通过我们手写的Struts.xml配置文件去跳转到对应的viewAction类是多例的,接收Web传递过来的参数需要使用实例变量来记住,通常我们都会写上set和get方法Struts2的工作流程Struts2接收到request请求将请求转向我们的过滤分批器进行过滤读取Struts2对应的配置文件经过默认的拦截器之后创建对应的Action【多例】执行完业务方法就返回给response对象SpringMVC快速入门导入开发包前6个是Spring的核心功能包【IOC】,第7个是关于web的包,第8个是SpringMVC包org.springframework.context-3.0.5.RELEASE.jarorg.springframework.expression-3.0.5.RELEASE.jarorg.springframework.core-3.0.5.RELEASE.jarorg.springframework.beans-3.0.5.RELEASE.jarorg.springframework.asm-3.0.5.RELEASE.jarcommons-logging.jarorg.springframework.web-3.0.5.RELEASE.jarorg.springframework.web.servlet-3.0.5.RELEASE.jar编写ActionAction实现Controller接口public class HelloAction implements Controller { @Override public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception { return null; } }我们只要实现handleRequest方法即可,该方法已经说了request和response对象给我们用了。这是我们非常熟悉的request和response对象。然而该方法返回的是ModelAndView这么一个对象,这是和Struts2不同的。Struts2返回的是字符串,而SpringMVC返回的是ModelAndViewModelAndView其实他就是将我们的视图路径和数据封装起来而已【我们想要跳转到哪,把什么数据存到request域中,设置这个对象的属性就行了】。public class HelloAction implements Controller { @Override public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception { ModelAndView modelAndView = new ModelAndView(); //跳转到hello.jsp页面。 modelAndView.setViewName("/hello.jsp"); return modelAndView; } }注册核心控制器在Struts2中,我们想要使用Struts2的功能,那么就得在web.xml文件中配置过滤器。而我们使用SpringMVC的话,我们是在web.xml中配置核心控制器<!-- 注册springmvc框架核心控制器 --> <servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--到类目录下寻找我们的配置文件--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:hello.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>DispatcherServlet</servlet-name> <!--映射的路径为.action--> <url-pattern>*.action</url-pattern> </servlet-mapping>
字符串转日期类型我们在Struts2中,如果web端传过来的字符串类型是yyyy-mm-dd hh:MM:ss这种类型的话,那么Struts2默认是可以自动解析成日期的,如果是别的字符串类型的话,Struts2是不能自动解析的。要么使用自定义转换器来解析,要么就自己使用Java程序来解析….而在SpringMVC中,即使是yyyy-mm-dd hh:MM:ss这种类型SpringMVC也是不能自动帮我们解析的。我们看如下的例子:JSP传递关于日期格式的字符串给控制器…<form action="${pageContext.request.contextPath}/hello.action" method="post"> <table align="center"> <tr> <td>用户名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>出生日期</td> <td><input type="text" name="date" value="1996-05-24"></td> </tr> <tr> <td colspan="2"> <input type="submit" value="提交"> </td> </tr> </table> </form>User对象定义Date成员变量接收public Date getDate() { return date; } public void setDate(Date date) { this.date = date; }业务方法获取Date值@RequestMapping(value = "/hello.action") public String hello(Model model, User user) throws Exception { System.out.println(user.getUsername() + "的出生日期是:" + user.getDate()); model.addAttribute("message", "你好"); return "/index.jsp"; }结果出问题了,SpringMVC不支持这种类型的参数:现在问题就抛出来了,那我们要怎么解决呢????SpringMVC给出类似于Struts2类型转换器这么一个方法给我们使用:如果我们使用的是继承AbstractCommandController类来进行开发的话,我们就可以重写initBinder()方法了….具体的实现是这样子的:@Override protected void initBinder(HttpServletRequest request,ServletRequestDataBinder binder) throws Exception { binder.registerCustomEditor(Date.class,new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"),true)); }那我们现在用的是注解的方式来进行开发,是没有重写方法的。因此我们需要用到的是一个注解,表明我要重写该方法!@InitBinder protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { binder.registerCustomEditor( Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true)); }再次访问:值得注意的是:如果我们使用的是Oracle插入时间的话,那么我们在SQL语句就要写TimeStrap时间戳插入进去,否则就行不通!结果重定向和转发我们一般做开发的时候,经常编辑完数据就返回到显示列表中。我们在Struts2是使用配置文件进行重定向或转发的:而我们的SpringMVC就非常简单了,只要在跳转前写上关键字就行了!public String hello(Model model, User user) throws Exception { System.out.println(user.getUsername() + "的出生日期是:" + user.getDate()); model.addAttribute("message", user.getDate()); return "redirect:/index.jsp"; }以此类推,如果是想要再次请求的话,那么我们只要写上对应的请求路径就行了!@RequestMapping(value = "/hello.action") public String hello(Model model, User user) throws Exception { return "redirect:/bye.action"; } @RequestMapping("/bye.action") public String bye() throws Exception { System.out.println("我进来了bye方法"); return "/index.jsp"; }返回JSON文本回顾一下Struts2返回JSON文本是怎么操作的:导入jar包要返回JSON文本的对象给出get方法在配置文件中继承json-default包result标签的返回值类型是json那么我们在SpringMVC又怎么操作呢???导入两个JSON开发包jackson-core-asl-1.9.11.jarjackson-mapper-asl-1.9.11.jar在要返回JSON的业务方法上给上注解:@RequestMapping(value = "/hello.action") public @ResponseBody User hello() throws Exception { User user = new User("1", "zhongfucheng"); return user; }配置JSON适配器<!-- 1)导入jackson-core-asl-1.9.11.jar和jackson-mapper-asl-1.9.11.jar 2)在业务方法的返回值和权限之间使用@ResponseBody注解表示返回值对象需要转成JSON文本 3)在spring.xml配置文件中编写如下代码: --> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/> </list> </property> </bean>测试的JSP<input type="button" value="Emp转JSON"/><p> <input type="button" value="List<Emp>转JSON"/><p> <input type="button" value="Map<String,Object>转JSON"/><p> <!-- Map<String,Object>转JSON --> <script type="text/javascript"> $(":button:first").click(function(){ var url = "${pageContext.request.contextPath}/hello.action"; var sendData = null; $.post(url,sendData,function(backData,textStaut,ajax){ alert(ajax.responseText); }); }); </script>测试:Map测试:@RequestMapping(value = "/hello.action") public @ResponseBody Map hello() throws Exception { Map map = new HashMap(); User user = new User("1", "zhongfucheng"); User user2 = new User("12", "zhongfucheng2"); map.put("total", user); map.put("rows", user2); return map; }更新------------------------------------------------------------------如果传递进来的数据就是JSON格式的话,我们我们需要使用到另外一个注解@RequestBody,将请求的json数据转成java对象总结使用注解的开发避免了继承多余的类,并且非常简洁高效。想要中文不乱码,仅仅设置request的编码格式是不行的。因为SpringMVC是通过无参的构造器将数据进行封装的。我们可以使用SpringMVC提供的过滤器来解决中文乱码问题。RequestMapping可以设置我们具体的访问路径,还可以分模块开发。基于这么两个原因,我们就可以在一个Action中写多个业务方法了。RequestMapping还能够限制该请求方法是GET还是POST。在我们的业务方法中,还可以使用传统的request和response等对象,只不过如果不是非要使用的话,最好就别使用了。对于SpringMVC自己帮我们封装参数,也是需要使用与request带过来的名称是相同的。如果不相同的话,我们需要使用注解来帮我们解决的。如果是需要封装成集合,或者封装多个Bean的话,那么我们后台的JavaBean就需要再向上一层封装,在业务方法上写上Bean进行了。当然了,在web页面上要指定对应Bean属性的属性。字符串转日期对象用到 @InitBinder注解来重写方法。返回JSON对象,我们就需要用到@ResponseBody注解,如果接收JSON数据封装成JavaBean的话,我们就需要用到@RequestBody注解。随后在配置文件上创建对应的bean即可。
前言本文主要是讲解在Controller中的开发,主要的知识点有如下:编码过滤器使用注解开发注解@RequestMapping详解业务方法接收参数字符串转日期重定向和转发返回JSONSpringMVC过滤编码器在SpringMVC的控制器中,如果没有对编码进行任何的操作,那么获取到的中文数据是乱码!即使我们在handle()方法中,使用request对象设置编码也不行!原因也非常简单,我们SpringMVC接收参数是通过控制器中的无参构造方法,再经过handle()方法的object对象来得到具体的参数类型的。Struts2是使用拦截器来自动帮我们完成中文乱码的问题的。那么SpringMVC作为一个更加强大的框架,肯定也有对应的方法来帮我们完成中文乱码问题!值得注意的是:该过滤编码器只能解决POST的乱码问题!我们只需要在web.xml配置文件中设置过滤编码器就行了!<!-- 编码过滤器 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class> org.springframework.web.filter.CharacterEncodingFilter </filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>注解开发SpringMVC我们在快速入门的例子中使用的是XML配置的方式来使用SpringMVC的,SpringMVC也能够支持注解。【个人非常喜欢注解的方式】我们在使用Action的时候,要么继承着AbstractCommandController类,要么显示使用注解Controller接口。当我们使用了注解以后就不用显示地继承或实现任何类了!开发流程使用@Controller这个注解,就表明这是一个SpringMVC的控制器!@Controller public class HelloAction { }当然了,现在Spring是不知道有这么一个注解的,因此我们需要在配置文件中配置扫描注解值得注意的是:在配置扫描路径的时候,后面不要加.*不然扫描不了,我不知道学Struts2还是其他的地方时候,习惯加了.*,于是就搞了很久!<!--扫描注解,后面不要加.*--> <context:component-scan base-package="zhongfucheng"/>在控制器中写业务方法@Controller public class HelloAction { /** * * @RequestMapping 表示只要是/hello.action的请求,就交由该方法处理。当然了.action可以去掉 * @param model 它和ModelAndView类似,它这个Model就是把数据封装到request对象中,我们就可以获取出来 * @return 返回跳转的页面【真实路径,就不用配置视图解析器了】 * @throws Exception */ @RequestMapping(value="/hello.action") public String hello(Model model) throws Exception{ System.out.println("HelloAction::hello()"); model.addAttribute("message","你好"); return "/index.jsp"; } }跳转到index页面,首页得到对应的值。<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>$Title$</title> </head> <body> 这是我的首页 <br> ${message} </body> </html>当然了,基于注解和基于XML来开发SpringMVC,都是通过映射器、适配器和视图解析器的。 只是映射器、适配器略有不同。但是都是可以省略的!<!-- 基于注解的映射器(可选) --> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/> <!-- 基于注解的适配器(可选) --> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/> <!-- 视图解析器(可选) --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"/>更新:上边的适配器和映射器只是Spring3.1版本之前使用的、3.1版本之后现在一般用以下的两个映射器: org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping 适配器: org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter当然了,这上面两个配置也可以使用<mvc:annotation-driven>>替代注解处理器和适配器的配置。
前言上一篇Spring博文主要讲解了如何使用Spring来实现AOP编程,本博文主要讲解Spring的DAO模块对JDBC的支持,以及Spring对事务的控制…对于JDBC而言,我们肯定不会陌生,我们在初学的时候肯定写过非常非常多的JDBC模板代码!回顾对模版代码优化过程我们来回忆一下我们怎么对模板代码进行优化的!首先来看一下我们原生的JDBC:需要手动去数据库的驱动从而拿到对应的连接..try { String sql = "insert into t_dept(deptName) values('test');"; Connection con = null; Statement stmt = null; Class.forName("com.mysql.jdbc.Driver"); // 连接对象 con = DriverManager.getConnection("jdbc:mysql:///hib_demo", "root", "root"); // 执行命令对象 stmt = con.createStatement(); // 执行 stmt.execute(sql); // 关闭 stmt.close(); con.close(); } catch (Exception e) { e.printStackTrace(); }因为JDBC是面向接口编程的,因此数据库的驱动都是由数据库的厂商给做到好了,我们只要加载对应的数据库驱动,便可以获取对应的数据库连接….因此,我们写了一个工具类,专门来获取与数据库的连接(Connection),当然啦,为了更加灵活,我们的工具类是读取配置文件的方式来做的。/* * 连接数据库的driver,url,username,password通过配置文件来配置,可以增加灵活性 * 当我们需要切换数据库的时候,只需要在配置文件中改以上的信息即可 * * */ private static String driver = null; private static String url = null; private static String username = null; private static String password = null; static { try { //获取配置文件的读入流 InputStream inputStream = UtilsDemo.class.getClassLoader().getResourceAsStream("db.properties"); Properties properties = new Properties(); properties.load(inputStream); //获取配置文件的信息 driver = properties.getProperty("driver"); url = properties.getProperty("url"); username = properties.getProperty("username"); password = properties.getProperty("password"); //加载驱动类 Class.forName(driver); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url,username,password); } public static void release(Connection connection, Statement statement, ResultSet resultSet) { if (resultSet != null) { try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } }经过上面一层的封装,我们可以在使用的地方直接使用工具类来得到与数据库的连接…那么比原来就方便很多了!但是呢,每次还是需要使用Connection去创建一个Statement对象。并且无论是什么方法,其实就是SQL语句和传递进来的参数不同!于是,我们就自定义了一个JDBC的工具类,详情可以看http://blog.csdn.net/hon_3y/article/details/53760782#t6我们自定义的工具类其实就是以DbUtils组件为模板来写的,因此我们在开发的时候就一直使用DbUtils组件了。使用Spring的JDBC上面已经回顾了一下以前我们的JDBC开发了,那么看看Spring对JDBC又是怎么优化的首先,想要使用Spring的JDBC模块,就必须引入两个jar文件:引入jar文件spring-jdbc-3.2.5.RELEASE.jarspring-tx-3.2.5.RELEASE.jar首先还是看一下我们原生的JDBC代码:获取Connection是可以抽取出来的,直接使用dataSource来得到Connection就行了。public void save() { try { String sql = "insert into t_dept(deptName) values('test');"; Connection con = null; Statement stmt = null; Class.forName("com.mysql.jdbc.Driver"); // 连接对象 con = DriverManager.getConnection("jdbc:mysql:///hib_demo", "root", "root"); // 执行命令对象 stmt = con.createStatement(); // 执行 stmt.execute(sql); // 关闭 stmt.close(); con.close(); } catch (Exception e) { e.printStackTrace(); } }值得注意的是,JDBC对C3P0数据库连接池是有很好的支持的。因此我们直接可以使用Spring的依赖注入,在配置文件中配置dataSource就行了!<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql:///hib_demo"></property> <property name="user" value="root"></property> <property name="password" value="root"></property> <property name="initialPoolSize" value="3"></property> <property name="maxPoolSize" value="10"></property> <property name="maxStatements" value="100"></property> <property name="acquireIncrement" value="2"></property> </bean>// IOC容器注入 private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public void save() { try { String sql = "insert into t_dept(deptName) values('test');"; Connection con = null; Statement stmt = null; // 连接对象 con = dataSource.getConnection(); // 执行命令对象 stmt = con.createStatement(); // 执行 stmt.execute(sql); // 关闭 stmt.close(); con.close(); } catch (Exception e) { e.printStackTrace(); } }Spring来提供了JdbcTemplate这么一个类给我们使用!它封装了DataSource,也就是说我们可以在Dao中使用JdbcTemplate就行了。创建dataSource,创建jdbcTemplate对象<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql:///zhongfucheng"></property> <property name="user" value="root"></property> <property name="password" value="root"></property> <property name="initialPoolSize" value="3"></property> <property name="maxPoolSize" value="10"></property> <property name="maxStatements" value="100"></property> <property name="acquireIncrement" value="2"></property> </bean> <!--扫描注解--> <context:component-scan base-package="bb"/> <!-- 2. 创建JdbcTemplate对象 --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> </beans>userDaopackage bb; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; /** * Created by ozc on 2017/5/10. */ @Component public class UserDao implements IUser { //使用Spring的自动装配 @Autowired private JdbcTemplate template; @Override public void save() { String sql = "insert into user(name,password) values('zhoggucheng','123')"; template.update(sql); } }测试:@Test public void test33() { ApplicationContext ac = new ClassPathXmlApplicationContext("bb/bean.xml"); UserDao userDao = (UserDao) ac.getBean("userDao"); userDao.save(); }
2022年04月