数据专栏

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

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

「深度学习福利」大神带你进阶工程师,立即查看>>>
常用的有:活跃的兔子卡夫卡火箭。
ActiveMQ,Java架构,所以运行需要JDK。是内嵌jetty服务器的web应用。5w/s,默认负载均衡,减轻服务器压力。
RabbitMQ,
RocketMQ,
Kafka,适用大数据,消息易丢失。

规范是接口。
框架的路径用非中文路径,可Copy到桌面。
面试的流程:有没有笔试,个人信息除了联系的信息,不要写重要的个人信息。
项目管理
2018-08-11 17:51:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
软件架构师
插件的实现原理 插件有几部分组成 如何实现插件安装,卸载,启用,禁用? 安装,卸载,启用,禁用怎样实现不停机,不关闭服务的情况进行?
口述如何设计一个MVC框架 怎样实现 URL 路由 怎样实现控制器 怎样实现视图 怎样实现模型
口述如何设计一个SOA框架 框架分为几个部分? 采用什么协议与框架通信? 如果考虑到性能使用二进制协议你怎样实现? 如何解决并发冲突? 如何支持事务? 你怎样与消息队列集成或者通信?
口述设计一个分布式计划任务系统
背景:计划任务即周期或定时运行的程序,我们要解决单点故障问题与负载均衡的问题,在一个分布式系统中单节点是不允许的。
设计要求:能够实现高可用,负载均衡,横向扩展 怎样处理同时运行产生的冲突问题? 怎样排队运行? 任务如何持久化? 一个节点宕机,另一个节点怎么接管没有完成的任务? 如何横向扩展? 扩展,收缩,维护如果能做到不停机,不影响业务?
项目管理
2018-08-11 14:47:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
阿里P9架构师讲解从单机至亿级流量大型网站系统架构的演进过程
阶段一、单机构建网站
网站的初期,我们经常会在单机上跑我们所有的程序和软件。此时我们使用一个容器,如tomcat、jetty、jboos,然后直接使用JSP/servlet技术,或者使用一些开源的框架如maven+spring+struct+hibernate、maven+spring+springmvc+mybatis;最后再选择一个数据库管理系统来存储数据,如mysql、sqlserver、oracle,然后通过JDBC进行数据库的连接和操作。
把以上的所有软件都装载同一台机器上,应用跑起来了,也算是一个小系统了。此时系统结果如下:
阶段二、应用服务器与数据库分离
随着网站的上线,访问量逐步上升,服务器的负载慢慢提高,在服务器还没有超载的时候,我们应该就要做好准备,提升网站的负载能力。假如我们代码层面已难以优化,在不提高单台机器的性能的情况下,增加机器是一个不错的方式,不仅可以有效地提高系统的负载能力,而且性价比高。
增加的机器用来做什么呢?此时我们可以把数据库,web服务器拆分开来,这样不仅提高了单台机器的负载能力,也提高了容灾能力。
应用服务器与数据库分开后的架构如下图所示:
阶段三、应用服务器集群
随着访问量继续增加,单台应用服务器已经无法满足需求了。在假设数据库服务器没有压力的情况下,我们可以把应用服务器从一台变成了两台甚至多台,把用户的请求分散到不同的服务器中,从而提高负载能力。多台应用服务器之间没有直接的交互,他们都是依赖数据库各自对外提供服务。著名的做故障切换的软件有keepalived,keepalived是一个类似于layer3、4、7交换机制的软件,他不是某个具体软件故障切换的专属品,而是可以适用于各种软件的一款产品。keepalived配合上ipvsadm又可以做负载均衡,可谓是神器。
我们以增加了一台应用服务器为例,增加后的系统结构图如下:
系统演变到这里,将会出现下面四个问题:
用户的请求由谁来转发到到具体的应用服务器
有什么转发的算法
应用服务器如何返回用户的请求
用户如果每次访问到的服务器不一样,那么如何维护session的一致性
我们来看看解决问题的方案:
1、第一个问题即是负载均衡的问题,一般有5种解决方案:
1、http重定向。HTTP重定向就是应用层的请求转发。用户的请求其实已经到了HTTP重定向负载均衡服务器,服务器根据算法要求用户重定向,用户收到重定向请求后,再次请求真正的集群
优点:简单。
缺点:性能较差。
2、DNS域名解析负载均衡。DNS域名解析负载均衡就是在用户请求DNS服务器,获取域名对应的IP地址时,DNS服务器直接给出负载均衡后的服务器IP。
优点:交给DNS,不用我们去维护负载均衡服务器。
缺点:当一个应用服务器挂了,不能及时通知DNS,而且DNS负载均衡的控制权在域名服务商那里,网站无法做更多的改善和更强大的管理。
3、反向代理服务器。在用户的请求到达反向代理服务器时(已经到达网站机房),由反向代理服务器根据算法转发到具体的服务器。常用的apache,nginx都可以充当反向代理服务器。
优点:部署简单。
缺点:代理服务器可能成为性能的瓶颈,特别是一次上传大文件。
4、IP层负载均衡。在请求到达负载均衡器后,负载均衡器通过修改请求的目的IP地址,从而实现请求的转发,做到负载均衡。
优点:性能更好。
缺点:负载均衡器的宽带成为瓶颈。
5、数据链路层负载均衡。在请求到达负载均衡器后,负载均衡器通过修改请求的mac地址,从而做到负载均衡,与IP负载均衡不一样的是,当请求访问完服务器之后,直接返回客户。而无需再经过负载均衡器。
2、第二个问题即是集群调度算法问题,常见的调度算法有10种。
1、rr 轮询调度算法。顾名思义,轮询分发请求。
优点:实现简单
缺点:不考虑每台服务器的处理能力
2、wrr 加权调度算法。我们给每个服务器设置权值weight,负载均衡调度器根据权值调度服务器,服务器被调用的次数跟权值成正比。
优点:考虑了服务器处理能力的不同
3、sh 原地址散列:提取用户IP,根据散列函数得出一个key,再根据静态映射表,查处对应的value,即目标服务器IP。过目标机器超负荷,则返回空。
4、dh 目标地址散列:同上,只是现在提取的是目标地址的IP来做哈希。
优点:以上两种算法的都能实现同一个用户访问同一个服务器。
5、lc 最少连接。优先把请求转发给连接数少的服务器。
优点:使得集群中各个服务器的负载更加均匀。
6、wlc 加权最少连接。在lc的基础上,为每台服务器加上权值。算法为:(活动连接数*256+非活动连接数)÷权重 ,计算出来的值小的服务器优先被选择。
优点:可以根据服务器的能力分配请求。
7、sed 最短期望延迟。其实sed跟wlc类似,区别是不考虑非活动连接数。算法为:(活动连接数+1)*256÷权重,同样计算出来的值小的服务器优先被选择。
8、nq 永不排队。改进的sed算法。我们想一下什么情况下才能“永不排队”,那就是服务器的连接数为0的时候,那么假如有服务器连接数为0,均衡器直接把请求转发给它,无需经过sed的计算。
9、LBLC 基于局部性的最少连接。均衡器根据请求的目的IP地址,找出该IP地址最近被使用的服务器,把请求转发之,若该服务器超载,最采用最少连接数算法。
10、LBLCR 带复制的基于局部性的最少连接。均衡器根据请求的目的IP地址,找出该IP地址最近使用的“服务器组”,注意,并不是具体某个服务器,然后采用最少连接数从该组中挑出具体的某台服务器出来,把请求转发之。若该服务器超载,那么根据最少连接数算法,在集群的非本服务器组的服务器中,找出一台服务器出来,加入本服务器组,然后把请求转发之。
3、第三个问题是集群模式问题,一般3种解决方案:
1、NAT:负载均衡器接收用户的请求,转发给具体服务器,服务器处理完请求返回给均衡器,均衡器再重新返回给用户。
2、DR:负载均衡器接收用户的请求,转发给具体服务器,服务器出来玩请求后直接返回给用户。需要系统支持IP Tunneling协议,难以跨平台。
3、TUN:同上,但无需IP Tunneling协议,跨平台性好,大部分系统都可以支持。
4、第四个问题是session问题,一般有4种解决方案:
1、Session Sticky。session sticky就是把同一个用户在某一个会话中的请求,都分配到固定的某一台服务器中,这样我们就不需要解决跨服务器的session问题了,常见的算法有ip_hash法,即上面提到的两种散列算法。
优点:实现简单。
缺点:应用服务器重启则session消失。
2、Session Replication。session replication就是在集群中复制session,使得每个服务器都保存有全部用户的session数据。
优点:减轻负载均衡服务器的压力,不需要要实现ip_hasp算法来转发请求。
缺点:复制时宽带开销大,访问量大的话session占用内存大且浪费。
3、Session数据集中存储:session数据集中存储就是利用数据库来存储session数据,实现了session和应用服务器的解耦。
优点:相比session replication的方案,集群间对于宽带和内存的压力减少了很多。
缺点:需要维护存储session的数据库。
4、Cookie Base:cookie base就是把session存在cookie中,有浏览器来告诉应用服务器我的session是什么,同样实现了session和应用服务器的解耦。
优点:实现简单,基本免维护。
缺点:cookie长度限制,安全性低,宽带消耗。
值得一提的是:
nginx目前支持的负载均衡算法有wrr、sh(支持一致性哈希)、fair(本人觉得可以归结为lc)。但nginx作为均衡器的话,还可以一同作为静态资源服务器。
keepalived+ipvsadm比较强大,目前支持的算法有:rr、wrr、lc、wlc、lblc、sh、dh
keepalived支持集群模式有:NAT、DR、TUN
nginx本身并没有提供session同步的解决方案,而apache则提供了session共享的支持。
推荐一个Java高级技术进阶群:697579751,文章运用到的架构技术都会在群里分享,都能免费下载。有兴趣学习的猿猿可以加一下。
好了,解决了以上的问题之后,系统的结构如下:
阶段四、数据库读写分离化
上面我们总是假设数据库负载正常,但随着访问量的的提高,数据库的负载也在慢慢增大。那么可能有人马上就想到跟应用服务器一样,把数据库一份为二再负载均衡即可。但对于数据库来说,并没有那么简单。假如我们简单的把数据库一分为二,然后对于数据库的请求,分别负载到A机器和B机器,那么显而易见会造成两台数据库数据不统一的问题。那么对于这种情况,我们可以先考虑使用读写分离的方式。
读写分离后的数据库系统结构如下:
这个结构变化后也会带来两个问题:
主从数据库之间数据同步问题
应用对于数据源的选择问题
解决问题方案:
我们可以使用MYSQL自带的master+slave的方式实现主从复制。
采用第三方数据库中间件,例如mycat。mycat是从cobar发展而来的,而cobar是阿里开源的数据库中间件,后来停止开发。mycat是国内比较好的mysql开源数据库分库分表中间件。
阶段五、用搜索引擎缓解读库的压力
数据库做读库的话,常常对模糊查找力不从心,即使做了读写分离,这个问题还未能解决。以我们所举的交易网站为例,发布的商品存储在数据库中,用户最常使用的功能就是查找商品,尤其是根据商品的标题来查找对应的商品。对于这种需求,一般我们都是通过like功能来实现的,但是这种方式的代价非常大。此时我们可以使用搜索引擎的倒排索引来完成。
搜索引擎具有以下优点:
它能够大大提高查询速度。
引入搜索引擎后也会带来以下的开销:
带来大量的维护工作,我们需要自己实现索引的构建过程,设计全量/增加的构建方式来应对非实时与实时的查询需求。
需要维护搜索引擎集群
搜索引擎并不能替代数据库,他解决了某些场景下的“读”的问题,是否引入搜索引擎,需要综合考虑整个系统的需求。引入搜索引擎后的系统结构如下:
阶段六、用缓存缓解读库的压力
1、后台应用层和数据库层的缓存
随着访问量的增加,逐渐出现了许多用户访问同一部分内容的情况,对于这些比较热门的内容,没必要每次都从数据库读取。我们可以使用缓存技术,例如可以使用google的开源缓存技术guava或者使用memcacahe作为应用层的缓存,也可以使用redis作为数据库层的缓存。
另外,在某些场景下,关系型数据库并不是很适合,例如我想做一个“每日输入密码错误次数限制”的功能,思路大概是在用户登录时,如果登录错误,则记录下该用户的IP和错误次数,那么这个数据要放在哪里呢?假如放在内存中,那么显然会占用太大的内容;假如放在关系型数据库中,那么既要建立数据库表,还要简历对应的java bean,还要写SQL等等。而分析一下我们要存储的数据,无非就是类似{ip:errorNumber}这样的key:value数据。对于这种数据,我们可以用NOSQL数据库来代替传统的关系型数据库。
2、页面缓存
除了数据缓存,还有页面缓存。比如使用HTML5的localstroage或者cookie。
优点:
减轻数据库的压力
大幅度提高访问速度
缺点:
需要维护缓存服务器
提高了编码的复杂性
值得一提的是:
缓存集群的调度算法不同与上面提到的应用服务器和数据库。最好采用“一致性哈希算法”,这样才能提高命中率。这个就不展开讲了,有兴趣的可以查阅相关资料。
加入缓存后的结构:
阶段七、数据库水平拆分与垂直拆分
我们的网站演进到现在,交易、商品、用户的数据都还在同一个数据库中。尽管采取了增加缓存,读写分离的方式,但随着数据库的压力继续增加,数据库的瓶颈越来越突出,此时,我们可以有数据垂直拆分和水平拆分两种选择。
推荐一个Java高级技术进阶群:697579751,文章运用到的架构技术都会在群里分享,都能免费下载。有兴趣学习的猿猿可以加一下。
7.1、数据垂直拆分
垂直拆分的意思是把数据库中不同的业务数据拆分道不同的数据库中,结合现在的例子,就是把交易、商品、用户的数据分开。
优点:
解决了原来把所有业务放在一个数据库中的压力问题。
可以根据业务的特点进行更多的优化
缺点:
需要维护多个数据库
问题:
需要考虑原来跨业务的事务
跨数据库的join
解决问题方案:
我们应该在应用层尽量避免跨数据库的事物,如果非要跨数据库,尽量在代码中控制。
我们可以通过第三方应用来解决,如上面提到的mycat,mycat提供了丰富的跨库join方案,详情可参考mycat官方文档。
垂直拆分后的结构如下:
7.2、数据水平拆分
数据水平拆分就是把同一个表中的数据拆分到两个甚至多个数据库中。产生数据水平拆分的原因是某个业务的数据量或者更新量到达了单个数据库的瓶颈,这时就可以把这个表拆分到两个或更多个数据库中。
优点:
如果我们能客服以上问题,那么我们将能够很好地对数据量及写入量增长的情况。
问题:
访问用户信息的应用系统需要解决SQL路由的问题,因为现在用户信息分在了两个数据库中,需要在进行数据操作时了解需要操作的数据在哪里。
主键的处理也变得不同,例如原来自增字段,现在不能简单地继续使用了。
如果需要分页,就麻烦了。
解决问题方案:
我们还是可以通过可以解决第三方中间件,如mycat。mycat可以通过SQL解析模块对我们的SQL进行解析,再根据我们的配置,把请求转发到具体的某个数据库。
我们可以通过UUID保证唯一或自定义ID方案来解决。
mycat也提供了丰富的分页查询方案,比如先从每个数据库做分页查询,再合并数据做一次分页查询等等。
数据水平拆分后的结构:
阶段八、应用的拆分
8.1、拆分应用
随着业务的发展,业务越来越多,应用越来越大。我们需要考虑如何避免让应用越来越臃肿。这就需要把应用拆开,从一个应用变为俩个甚至更多。还是以我们上面的例子,我们可以把用户、商品、交易拆分开。变成“用户、商品”和“用户,交易”两个子系统。
拆分后的结构:
问题:
这样拆分后,可能会有一些相同的代码,如用户相关的代码,商品和交易都需要用户信息,所以在两个系统中都保留差不多的操作用户信息的代码。如何保证这些代码可以复用是一个需要解决的问题。
解决问题:
通过走服务化的路线来解决
8.2、走服务化的道路
为了解决上面拆分应用后所出现的问题,我们把公共的服务拆分出来,形成一种服务化的模式,简称SOA。
采用服务化之后的系统结构:
优点:
相同的代码不会散落在不同的应用中了,这些实现放在了各个服务中心,使代码得到更好的维护。
我们把对数据库的交互放在了各个服务中心,让”前端“的web应用更注重与浏览器交互的工作。
推荐一个Java高级技术进阶群:697579751,文章运用到的架构技术都会在群里分享,都能免费下载。有兴趣学习的猿猿可以加一下。
问题:
如何进行远程的服务调用
解决方法:
我们可以通过下面的引入消息中间件来解决
阶段九、引入消息中间件
随着网站的继续发展,我们的系统中可能出现不同语言开发的子模块和部署在不同平台的子系统。此时我们需要一个平台来传递可靠的,与平台和语言无关的数据,并且能够把负载均衡透明化,能在调用过程中收集调用数据并分析之,推测出网站的访问增长率等等一系列需求,对于网站应该如何成长做出预测。开源消息中间件有阿里的dubbo,可以搭配Google开源的分布式程序协调服务zookeeper实现服务器的注册与发现。
引入消息中间件后的结构:
十、总结
以上的演变过程只是一个例子,并不适合所有的网站,实际中网站演进过程与自身业务和不同遇到的问题有密切的关系,没有固定的模式。只有认真的分析和不断地探究,才能发现适合自己网站的架构。
项目管理
2018-08-11 13:41:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
1.接口隔离原则(DIP)(这个没有多说的)
使用多个专门的接口比使用单一的总接口要好。
2.合成/聚合复用原则(CARP)
合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。
聚合用来表示“拥有”关系或者整体与部分的关系(汽车与发动机);而合成关系则用来表示一种强的多的“拥有”关系;在一个合成关系里,部分和整体的生命周期是一样的(人和四肢)。
项目管理
2018-08-10 22:07:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
限制特定的 IP 地址可以访问管理员后台
Confluence 的管理员控制台界面对整个应用来说是非常重要的,任何人访问 Confluence 的控制台不仅仅可以访问 Confluence 安装实例,同时还可以访问整个服务器。我们可以限制 Confluence 的管理员控制台的访问给真正需要使用的人和使用强密码的方式。你可以考虑只有网络上的部分机器能够访问 Confluence 的管理员控制台或者只有内部网络的机器才可以访问控制台。如果你使用的是 Apache web server ,这个限制可以在 Apache 端 进行配置,按照下面的方法进行配置:
1. 创建一个定义权限的设置
这个文件可以在 Apache 的配置目录中或者系统全局目录中。例如这个配置文件我们可以命名为 "sysadmin_ips_only.conf"。这个配置文件应该包含有下面的内容:
Order Deny,Allow
Deny from All

# Mark the Sysadmin's workstation
Allow from 192.168 . 12.42
2. 添加这个文件到你的虚拟主机中
在你的 Apache 虚拟主机(Apache Virtual Host)配置文件中,添加下面的行来限制系统管理员可以进行的管理操作:
这个配置是是基于你已经安装 Confluence 在 '/confluence' 目录下。如果你的 Confluence 是安装在 '/' 下或者其他的路径下,仅添加相关的路径即可。

Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf
https://www.cwiki.us/display/CONF6ZH/Using+Apache+to+limit+access+to+the+Confluence+administration+interface
项目管理
2018-08-10 21:39:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
在启用 HTTPS 之前,你需要一个有效的证书,如果你已经有了一个有效的证书,你可以直接跳过这个步骤,进入 step 2 。
你可以创建一个自签名的证书,或者从信任的 Certificate Authority 中获得一个证书。
如果你的项目小组计划使用 Confluence 服务器移动 app。你需要你的证书是从信任的证书签发机构签发的。你不能使用自签名的证书或者从一个不信任的机构获得的证书,或者自由 CA。
选项 1: 创建一个自签名证书
当你需要进行加密,但是你并不需要对网站的的请求校验的话,自签名证书能够帮助你完成这个。在通常的情况下,你可以在你的测试环境下签发你的自签名证书,你也可以在你公司内部的网络上签发自签名证书。
因为证书不是信任的组织签发的(CA),用户可能会收到站点不被信任,并且提供一个步骤让用户先确定,才能访问网站的信息。这个通常是在第一次访问网站的时候出现的提示。如果你使用的 Confluence 的移动 app 的话,用户不能通过你的自签名证书访问你的 Confluence 站点。
在这个情况下,我们需要使用 Java 的 keytool 工具包。这个工具包是包含在 JDK 中的。如果你对命令行工具并不是十分熟悉的话,你可以考虑使用 KeyStore Explorer 工具。
使用 keytool 来创建一个自签名证书: 从命令行中,为你的操作系统运行正确的命令:
Windows
"%JAVA_HOME%\bin\keytool" -genkeypair -keysize 2048 -alias tomcat -keyalg RSA -sigalg SHA256withRSA
Linux (and MacOS)
$JAVA_HOME/bin/keytool -genkeypair -keysize 2048 -alias tomcat -keyalg RSA -sigalg SHA256withRSA
当出现提示后,为你的证书创建 密码(password ) ,私有 key。 仅使用数字和英文字符。如果你使用了特殊字符,Tomcat 可能会出现错误。 请记录你创建的密码,在下一步中你需要使用到你创建的密码。 默认的密码是 ' changeit '。 根据提示来确定证书的细节。这些信息被用来构造 X.500 实体中的 Distinguished Name (DN) 。
First and last name :这个不是你的名字,这个是 Common Name (CN),例如 ' confluence.example.com '。CN 必须与 Confluence 使用的域名完全对应,否则 Tomcat 将不能使用你签名的证书。 Organizational unit :这个是证书使用的部门或者小组,例如 'marketing'。 Organization :是你公司的名字,例如 'SeeSpaceEZ'。 City, State / province, country code :这个是你公司的地理位置,例如 Sydney, NSW, AU。 输出将会如下所示。输入 ' y ' 来确定你输入的内容。
CN= confluence.example.com, OU=Marketing, O=SeeSpaceEZ, L=Sydney, ST=NSW, C=AU
当被询问为 'tomcat' 准备使用的 密码( password )的时候,输入你第二步中输入的密码(在输入密码后单击回车)。 'tomcat' 是你在 keytool 命令行中输入的别名,在这里用来对你提示。 你 keystore 实例必须和你的私有 key 有相同的密码。这个是 Tomcat 服务器要求的。 你的证书现在已经可以用了,进入 下面 的第二步。
选项 2: 使用 Certificate Authority 签发的证书(推荐)
在生产环境中,你需要使用从 Certificate Authority (CA) 签发的证书。下面的内容是从 Tomcat documentation 中拷贝出来的。
首先你需要创建本地证书,然后基于你创建的本地证书再创建一个 'certificate signing request' (CSR) 。你需要提交 CSR 到你的选择的 CA 提供商上进行收取。CA 将会通过 CSR 将授权后的证书发给你。 使用 Java 的 keytool 工具来创建一个本地证书(请按照上面第一步所描述的内容)。 从命令中,将会返回下面的命令工具来创建所需要前面的证书。
keytool -certreq -keyalg RSA -alias tomcat -file certreq.csr -keystore
替换 为路径和你本地证书创建 .keystore 的文件名。 提交创建的文件为 certreq.csr 到你希望进行授权的 CA。
请参考 CA 的文档来找到如何进行这个操作。 CA 将会发个你已经签名好的证书。 导入新证书到你的本地的 keystore:
keytool -importcert -alias tomcat -keystore -file
一些 CA 可能要求你在安装你的证书之前先安装一个中间人证书。你应该按照 CA 提供的文档来完成你本地证书的成功安装。
如果你使用的是 Verisign 或 GoDaddy,然后你收到了错误的信息,你可能需要将 PKCS12 和你的私钥(private key)同时导出。 首先,删除添加到 keystore 中的所有 key:
keytool -delete -alias tomcat -keystore
然后导出为 PKCS12 格式:
openssl pkcs12 -export -in -inkey -out -name tomcat -CAfile -caname root
然后导入 PKCS12 到 jks 中:
keytool -importkeystore -deststorepass -destkeypass -destkeystore -srckeystore

https://www.cwiki.us/display/CONF6ZH/Running+Confluence+Over+SSL+or+HTTPS
项目管理
2018-08-10 21:39:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
备注: 这个页面的文档是 Apache 的配置,而不是 Confluence 自己的配置。Atlassian 将会对 Confluence 的配置提供支持,但是我们不能保证能够对你所有在配置 Apache 的时候遇到的问题提供支持。请注意,这个页面下面提供的信息仅为你提供参考同时也不能保证所有的的配置能正常工作。如果你按照本页面中的内容进行配置,所有的风险自负。
Confluence 的一些功能需要提供 URL 重写,如果 Confluence 能够从多个不同域名进行访问的话。如果 Confluence 被配置为多个域名 不使用URL 重写 ,你将有可能会碰到一系列的问题。请参考 Various Issues Caused when Server Base URL Does Not Match the URL Used to Access Confluence 页面中的内容。
一个为什么你希望从多个域名访问 Confluence 的示例: 从内部网络: http://wiki 从外部可见的网络: http://wiki.domain.com
使用重写来让 Confluence 支持多域名访问
为了让 Confluence 能够从多域名进行访问: 为 Confluence 服务器,添加 DNS 为整个 http://wiki 域名为外部可见的 IP 地址。 设置 Confluence 的 server base URL 为 http://wiki.domain.com . 添加 Apache 的 HTTP 代理,使用页面 Running Confluence behind Apache 页面中的指南。 使用 mod_rewrite 模块来修改 URL。
更多信息
你可能需要插入 UrlRewriteFilter 到你的 Java web 过滤器中。这个与 Apache 的 mod_rewrite 工作原理类似。

https://www.cwiki.us/display/CONF6ZH/Using+mod_rewrite+to+Modify+Confluence+URLs
项目管理
2018-08-10 21:37:00
「深度学习福利」大神带你进阶工程师,立即查看>>>

在 Confluence 6 及其后续版本中,不能使用 mod_jk 来做代理。这是因为 Synchrony 服务导致的这个限制。 Synchrony 在协同编辑的时候需要启动,同时还不能接受 AJP 连接。请参考 Using Apache with mod_proxy 页面中的文档进行配置。
如果你没有办法切换 mod_proxy,请参考 How to configure Apache mod_jk to proxy Confluence 6.x or later 页面中的内容进行配置。
https://www.cwiki.us/display/CONF6ZH/Using+Apache+with+mod_jk
项目管理
2018-08-10 21:32:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
限制特定的 IP 地址可以访问管理员后台
Confluence 的管理员控制台界面对整个应用来说是非常重要的,任何人访问 Confluence 的控制台不仅仅可以访问 Confluence 安装实例,同时还可以访问整个服务器。我们可以限制 Confluence 的管理员控制台的访问给真正需要使用的人和使用强密码的方式。你可以考虑只有网络上的部分机器能够访问 Confluence 的管理员控制台或者只有内部网络的机器才可以访问控制台。如果你使用的是 Apache web server ,这个限制可以在 Apache 端 进行配置,按照下面的方法进行配置:
1. 创建一个定义权限的设置
这个文件可以在 Apache 的配置目录中或者系统全局目录中。例如这个配置文件我们可以命名为 "sysadmin_ips_only.conf"。这个配置文件应该包含有下面的内容:
Order Deny,Allow
Deny from All

# Mark the Sysadmin's workstation
Allow from 192.168 . 12.42
2. 添加这个文件到你的虚拟主机中
在你的 Apache 虚拟主机(Apache Virtual Host)配置文件中,添加下面的行来限制系统管理员可以进行的管理操作:
这个配置是是基于你已经安装 Confluence 在 '/confluence' 目录下。如果你的 Confluence 是安装在 '/' 下或者其他的路径下,仅添加相关的路径即可。

Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf


Include sysadmin_ips_only.conf
https://www.cwiki.us/display/CONF6ZH/Using+Apache+to+limit+access+to+the+Confluence+administration+interface
项目管理
2018-08-10 21:26:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
package com.util;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import oracle.jdbc.driver.OracleConnection;

/**
* @Description: JDBC操作元数据示例-- DatabaseMetaData接口
*/
public class JdbcUtil {
//获得驱动
private static String DRIVER = "oracle.jdbc.driver.OracleDriver";
//获得url
private static String URL = "jdbc:oracle:thin: @localhost :test";
//获得连接数据库的用户名
private static String USER = "root";
//获得连接数据库的密码
private static String PASS = "root";

static {
try {
//初始化JDBC驱动并让驱动加载到jvm中
Class.forName(DRIVER);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

public static Connection getConnection(){
Connection conn = null;
try {
//连接数据库

/*
* 设置可获取REMARK备注信息
Properties props =new Properties();
props.put("remarksReporting","true");
props.put("user", USER);
props.put("password", PASS);
conn =DriverManager.getConnection(URL,props);*/

conn = DriverManager.getConnection(URL,USER,PASS);
conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}

//关闭连接
public static void close(Object o){
if (o == null){
return;
}
if (o instanceof ResultSet){
try {
((ResultSet)o).close();
} catch (SQLException e) {
e.printStackTrace();
}
} else if(o instanceof Statement){
try {
((Statement)o).close();
} catch (SQLException e) {
e.printStackTrace();
}
} else if (o instanceof Connection){
Connection c = (Connection)o;
try {
if (!c.isClosed()){
c.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}


public static void close(ResultSet rs, Statement stmt,
Connection conn){
close(rs);
close(stmt);
close(conn);
}

public static void close(ResultSet rs,
Connection conn){
close(rs);
close(conn);
}



/**
* @Description: 获取数据库相关信息
*/
public static void getDataBaseInfo() {
Connection conn = getConnection();
ResultSet rs = null;
try{
DatabaseMetaData dbmd = conn.getMetaData();
System.out.println("数据库已知的用户: "+ dbmd.getUserName());
System.out.println("数据库的系统函数的逗号分隔列表: "+ dbmd.getSystemFunctions());
System.out.println("数据库的时间和日期函数的逗号分隔列表: "+ dbmd.getTimeDateFunctions());
System.out.println("数据库的字符串函数的逗号分隔列表: "+ dbmd.getStringFunctions());
System.out.println("数据库供应商用于 'schema' 的首选术语: "+ dbmd.getSchemaTerm());
System.out.println("数据库URL: " + dbmd.getURL());
System.out.println("是否允许只读:" + dbmd.isReadOnly());
System.out.println("数据库的产品名称:" + dbmd.getDatabaseProductName());
System.out.println("数据库的版本:" + dbmd.getDatabaseProductVersion());
System.out.println("驱动程序的名称:" + dbmd.getDriverName());
System.out.println("驱动程序的版本:" + dbmd.getDriverVersion());

System.out.println("数据库中使用的表类型");
rs = dbmd.getTableTypes();
while (rs.next()) {
System.out.println(rs.getString("TABLE_TYPE"));
}
}catch (SQLException e){
e.printStackTrace();
} finally{
JdbcUtil.close(rs,conn);
}
}

/**
* @Description:获得数据库中所有Schemas(对应于oracle中的Tablespace)
*/
public static void getSchemasInfo(){
Connection conn = getConnection();
ResultSet rs = null;
try{
DatabaseMetaData dbmd = conn.getMetaData();
rs = dbmd.getSchemas();
while (rs.next()){
String tableSchem = rs.getString("TABLE_SCHEM");
System.out.println(tableSchem);
}
} catch (SQLException e){
e.printStackTrace();
} finally{
JdbcUtil.close(rs,conn);
}
}

/**
* @Description: 获取数据库中所有的表信息
*/
public static void getTablesList() {
Connection conn = getConnection();
ResultSet rs = null;
try {
/**
* 设置连接属性,使得可获取到表的REMARK(备注)
*/
((OracleConnection)conn).setRemarksReporting(true);
DatabaseMetaData dbmd = conn.getMetaData();
String[] types = { "TABLE" };
rs = dbmd.getTables(null, null, "%", types);
while (rs.next()) {
String tableName = rs.getString("TABLE_NAME"); //表名
String tableType = rs.getString("TABLE_TYPE"); //表类型
String remarks = rs.getString("REMARKS"); //表备注
System.out.println(tableName + " - " + tableType + " - " + remarks);
}
} catch (SQLException e) {
e.printStackTrace();
} finally{
JdbcUtil.close(rs,conn);
}
}

/**
* @Description: 获取某表信息
*/
public static void getTablesInfo(){
Connection conn = getConnection();
ResultSet rs = null;
try {
/**
* 设置连接属性,使得可获取到表的REMARK(备注)
*/
((OracleConnection)conn).setRemarksReporting(true);
DatabaseMetaData dbmd = conn.getMetaData();
/**
* 获取给定类别中使用的表的描述。
* 方法原型:ResultSet getTables(String catalog,String schemaPattern,String tableNamePattern,String[] types);
* catalog - 表所在的类别名称;""表示获取没有类别的列,null表示获取所有类别的列。
* schema - 表所在的模式名称(oracle中对应于Tablespace);""表示获取没有模式的列,null标识获取所有模式的列; 可包含单字符通配符("_"),或多字符通配符("%");
* tableNamePattern - 表名称;可包含单字符通配符("_"),或多字符通配符("%");
* types - 表类型数组; "TABLE"、"VIEW"、"SYSTEM TABLE"、"GLOBAL TEMPORARY"、"LOCAL TEMPORARY"、"ALIAS" 和 "SYNONYM";null表示包含所有的表类型;可包含单字符通配符("_"),或多字符通配符("%");
*/
rs = dbmd.getTables(null, null, "CUST_INTER_TF_SERVICE_REQ", new String[]{"TABLE","VIEW"});


while(rs.next()){
String tableCat = rs.getString("TABLE_CAT"); //表类别(可为null)
String tableSchemaName = rs.getString("TABLE_SCHEM");//表模式(可能为空),在oracle中获取的是命名空间,其它数据库未知
String tableName = rs.getString("TABLE_NAME"); //表名
String tableType = rs.getString("TABLE_TYPE"); //表类型,典型的类型是 "TABLE"、"VIEW"、"SYSTEM TABLE"、"GLOBAL TEMPORARY"、"LOCAL TEMPORARY"、"ALIAS" 和 "SYNONYM"。
String remarks = rs.getString("REMARKS"); //表备注

System.out.println(tableCat + " - " + tableSchemaName + " - " +tableName + " - " + tableType + " - "
+ remarks);
}
} catch (Exception ex) {
ex.printStackTrace();
}finally{
JdbcUtil.close(rs,conn);
}
}

/**
* @Description: 获取表主键信息
*/
public static void getPrimaryKeysInfo() {
Connection conn = getConnection();
ResultSet rs = null;
try{
DatabaseMetaData dbmd = conn.getMetaData();
/**
* 获取对给定表的主键列的描述
* 方法原型:ResultSet getPrimaryKeys(String catalog,String schema,String table);
* catalog - 表所在的类别名称;""表示获取没有类别的列,null表示获取所有类别的列。
* schema - 表所在的模式名称(oracle中对应于Tablespace);""表示获取没有模式的列,null标识获取所有模式的列; 可包含单字符通配符("_"),或多字符通配符("%");
* table - 表名称;可包含单字符通配符("_"),或多字符通配符("%");
*/
rs = dbmd.getPrimaryKeys(null, null, "CUST_INTER_TF_SERVICE_REQ");

while (rs.next()){
String tableCat = rs.getString("TABLE_CAT"); //表类别(可为null)
String tableSchemaName = rs.getString("TABLE_SCHEM");//表模式(可能为空),在oracle中获取的是命名空间,其它数据库未知
String tableName = rs.getString("TABLE_NAME"); //表名
String columnName = rs.getString("COLUMN_NAME");//列名
short keySeq = rs.getShort("KEY_SEQ");//序列号(主键内值1表示第一列的主键,值2代表主键内的第二列)
String pkName = rs.getString("PK_NAME"); //主键名称

System.out.println(tableCat + " - " + tableSchemaName + " - " + tableName + " - " + columnName + " - "
+ keySeq + " - " + pkName);
}
}catch (SQLException e){
e.printStackTrace();
}finally{
JdbcUtil.close(rs,conn);
}
}

/**
* @Description: 获取表索引信息
*/
public static void getIndexInfo() {
Connection conn = getConnection();
ResultSet rs = null;
try{
DatabaseMetaData dbmd = conn.getMetaData();
/**
* 获取给定表的索引和统计信息的描述
* 方法原型:ResultSet getIndexInfo(String catalog,String schema,String table,boolean unique,boolean approximate)
* catalog - 表所在的类别名称;""表示获取没有类别的列,null表示获取所有类别的列。
* schema - 表所在的模式名称(oracle中对应于Tablespace);""表示获取没有模式的列,null标识获取所有模式的列; 可包含单字符通配符("_"),或多字符通配符("%");
* table - 表名称;可包含单字符通配符("_"),或多字符通配符("%");
* unique - 该参数为 true时,仅返回唯一值的索引; 该参数为 false时,返回所有索引;
* approximate - 该参数为true时,允许结果是接近的数据值或这些数据值以外的值;该参数为 false时,要求结果是精确结果;
*/
rs = dbmd.getIndexInfo(null, null, "CUST_INTER_TF_SERVICE_REQ", false, true);
while (rs.next()){
String tableCat = rs.getString("TABLE_CAT"); //表类别(可为null)
String tableSchemaName = rs.getString("TABLE_SCHEM");//表模式(可能为空),在oracle中获取的是命名空间,其它数据库未知
String tableName = rs.getString("TABLE_NAME"); //表名
boolean nonUnique = rs.getBoolean("NON_UNIQUE");// 索引值是否可以不唯一,TYPE为 tableIndexStatistic时索引值为 false;
String indexQualifier = rs.getString("INDEX_QUALIFIER");//索引类别(可能为空),TYPE为 tableIndexStatistic 时索引类别为 null;
String indexName = rs.getString("INDEX_NAME");//索引的名称 ;TYPE为 tableIndexStatistic 时索引名称为 null;
/**
* 索引类型:
* tableIndexStatistic - 此标识与表的索引描述一起返回的表统计信息
* tableIndexClustered - 此为集群索引
* tableIndexHashed - 此为散列索引
* tableIndexOther - 此为某种其他样式的索引
*/
short type = rs.getShort("TYPE");//索引类型;
short ordinalPosition = rs.getShort("ORDINAL_POSITION");//在索引列顺序号;TYPE为 tableIndexStatistic 时该序列号为零;
String columnName = rs.getString("COLUMN_NAME");//列名;TYPE为 tableIndexStatistic时列名称为 null;
String ascOrDesc = rs.getString("ASC_OR_DESC");//列排序顺序:升序还是降序[A:升序; B:降序];如果排序序列不受支持,可能为 null;TYPE为 tableIndexStatistic时排序序列为 null;
int cardinality = rs.getInt("CARDINALITY"); //基数;TYPE为 tableIndexStatistic 时,它是表中的行数;否则,它是索引中唯一值的数量。
int pages = rs.getInt("PAGES"); //TYPE为 tableIndexStatisic时,它是用于表的页数,否则它是用于当前索引的页数。
String filterCondition = rs.getString("FILTER_CONDITION"); //过滤器条件,如果有的话(可能为 null)。

System.out.println(tableCat + " - " + tableSchemaName + " - " + tableName + " - " + nonUnique + " - "
+ indexQualifier + " - " + indexName + " - " + type + " - " + ordinalPosition + " - " + columnName
+ " - " + ascOrDesc + " - " + cardinality + " - " + pages + " - " + filterCondition);
}
} catch (SQLException e){
e.printStackTrace();
} finally{
JdbcUtil.close(rs,conn);
}
}


/**
* @Description: 获取表中列值信息
*/
public static void getColumnsInfo(){
Connection conn = getConnection();
ResultSet rs = null;

try{
/**
* 设置连接属性,使得可获取到列的REMARK(备注)
*/
((OracleConnection)conn).setRemarksReporting(true);
DatabaseMetaData dbmd = conn.getMetaData();
/**
* 获取可在指定类别中使用的表列的描述。
* 方法原型:ResultSet getColumns(String catalog,String schemaPattern,String tableNamePattern,String columnNamePattern)
* catalog - 表所在的类别名称;""表示获取没有类别的列,null表示获取所有类别的列。
* schema - 表所在的模式名称(oracle中对应于Tablespace);""表示获取没有模式的列,null标识获取所有模式的列; 可包含单字符通配符("_"),或多字符通配符("%");
* tableNamePattern - 表名称;可包含单字符通配符("_"),或多字符通配符("%");
* columnNamePattern - 列名称; ""表示获取列名为""的列(当然获取不到);null表示获取所有的列;可包含单字符通配符("_"),或多字符通配符("%");
*/
rs =dbmd.getColumns(null, null, "CUST_INTER_TF_SERVICE_REQ", null);

while(rs.next()){
String tableCat = rs.getString("TABLE_CAT"); //表类别(可能为空)
String tableSchemaName = rs.getString("TABLE_SCHEM"); //表模式(可能为空),在oracle中获取的是命名空间,其它数据库未知
String tableName_ = rs.getString("TABLE_NAME"); //表名
String columnName = rs.getString("COLUMN_NAME"); //列名
int dataType = rs.getInt("DATA_TYPE"); //对应的java.sql.Types的SQL类型(列类型ID)
String dataTypeName = rs.getString("TYPE_NAME"); //java.sql.Types类型名称(列类型名称)
int columnSize = rs.getInt("COLUMN_SIZE"); //列大小
int decimalDigits = rs.getInt("DECIMAL_DIGITS"); //小数位数
int numPrecRadix = rs.getInt("NUM_PREC_RADIX"); //基数(通常是10或2) --未知
/**
* 0 (columnNoNulls) - 该列不允许为空
* 1 (columnNullable) - 该列允许为空
* 2 (columnNullableUnknown) - 不确定该列是否为空
*/
int nullAble = rs.getInt("NULLABLE"); //是否允许为null
String remarks = rs.getString("REMARKS"); //列描述
String columnDef = rs.getString("COLUMN_DEF"); //默认值
int charOctetLength = rs.getInt("CHAR_OCTET_LENGTH"); // 对于 char 类型,该长度是列中的最大字节数
int ordinalPosition = rs.getInt("ORDINAL_POSITION"); //表中列的索引(从1开始)
/**
* ISO规则用来确定某一列的是否可为空(等同于NULLABLE的值:[ 0:'YES'; 1:'NO'; 2:''; ])
* YES -- 该列可以有空值;
* NO -- 该列不能为空;
* 空字符串--- 不知道该列是否可为空
*/
String isNullAble = rs.getString("IS_NULLABLE");

/**
* 指示此列是否是自动递增
* YES -- 该列是自动递增的
* NO -- 该列不是自动递增
* 空字串--- 不能确定该列是否自动递增
*/
//String isAutoincrement = rs.getString("IS_AUTOINCREMENT"); //该参数测试报错


System.out.println(tableCat + " - " + tableSchemaName + " - " + tableName_ + " - " + columnName +
" - " + dataType + " - " + dataTypeName + " - " + columnSize + " - " + decimalDigits + " - "
+ numPrecRadix + " - " + nullAble + " - " + remarks + " - " + columnDef + " - " + charOctetLength
+ " - " + ordinalPosition + " - " + isNullAble );

}
}catch(SQLException ex){
ex.printStackTrace();
}finally{
JdbcUtil.close(rs,conn);
}
}

/**
* @Description: TODO
*/
public static void main(String[] args) {
getDataBaseInfo(); //获取数据库信息
getSchemasInfo(); //获取数据库所有Schema
getTablesList(); //获取某用户下所有的表
getTablesInfo(); //获取表信息
getPrimaryKeysInfo(); //获取表主键信息
getIndexInfo(); //获取表索引信息
getColumnsInfo(); //获取表中列值信息
}
}
项目管理
2018-08-10 16:46:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
一、事务的基本要素
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
二、事务在并发时出现问题
1、丢失更新
两个不同事物同时获得相同数据,然后在各自事务中同时修改了该数据,那么先提交的事务更新会被后提交事务的更新给覆盖掉,这种情况事务A的更新就被覆盖掉了、丢失了。
举个栗子: 事务A 事务B 读取X=100 读取X=100 写入x=X+100 写入x=X+200 事务结束x=200 事务结束x=300 最后x=300
2、脏读(未提交读)
事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
3、不可重复读
事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
举个栗子: 事务A 事务B 读取X=100 读取X=100 读取X=100 写入x=X+100 事务结束, x=200 读取X=200 (此时,在同一个事务A中, 读取的X值发生了变化!) 事务结束
4、 幻读
事务A读的时候读出了15条记录,事务B在事务A执行的过程中 增加 了1条,事务A再读的时候就变成了 16 条,这种情况就叫做幻影读。
不可重复读 和 幻读 很容易混淆,不可重复读侧重于 修改 ,幻读侧重于 新增或删除 。解决不可重复读的问题只需 锁住满足条件的行 ,解决幻读需要 锁表
三、mysql隔离级别
是:还存在,不能避免; 否:不存在,能避免
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read) 串行化(serializable)


四、封锁协议
排他锁(X锁) :若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能在对A加任何类型的锁,直到T释放A上的锁为止。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。
共享锁(S锁) :若事务T对数据对象A加上S锁,则只允许T读A但不能修改A,其他事务只能再对A加S锁而不能加X锁,知道T释放A上的S锁为止。
一级封锁协议(对应 read uncommited) :
事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。
二级封锁协议(对应read commited):
在一级封锁协议基础上增加事务T在读数据R之前必须先对其加S锁,读完后即可释放S锁。二级封锁协议出防止了丢失修改,还可以进一步防止读“脏”数据。
三级封锁协议(对应reapetable read ):
在一级封锁协议的基础上增加事务T在读数据R之前必须先对其加S锁,直到事务结束才释放。三级封锁协议出防止了丢失修改和读“脏”数据外,还可以进一步防止了不可重复读。
串行化:
一二三级封锁协议都是在行级加锁,而串行化是对整表加锁。读数据时对整表加共享锁,修改数据对整表加排他锁。
项目管理
2018-08-10 16:13:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
name="language"
data-options="
prompt:'请选择数据类型',
url:'combobox_data1.json',
method:'get',
valueField:'id',
textField:'text',
multiple:true,
panelHeight:'auto'
">
项目管理
2018-08-10 15:06:00
「深度学习福利」大神带你进阶工程师,立即查看>>>


把不需要的删除:
与Eureka Server项目的版本一致,pom.xml:
修改好了Pom.xml记得刷新下,不然可能用的还是之前的版本:
Eureka Server项目要加的注解:@EnableEurekaServer
Eureka Client项目要加的注解:@EnableEurekaClient,不加则注册不了

添加application.yml,并加上以下所有配置:


回到Eureka Server注册中心项目中,在application.yml加上eureka.server.enable-self-preservation:false配置,
注意只能在开发环境把它关闭,生成环境不要关闭了一定要打开,方便做微服务的调用,不会出现错误


如果遇到下面的问题:
SpringCloud Eureka Client启动后服务器就自己关闭了,提示Unregistering application xxx with eureka with status DOWN。
解决办法:在Client的服务中加入这个依赖:然后再mvn clean install 一下就好了
org.springframework.boot
spring-boot-starter-web

项目管理
2018-08-10 10:27:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
分布式
分布式服务:Dubbo+Zookeeper+Proxy+Restful
分布式消息中间件:KafKa+Flume+Zookeeper
分布式缓存:Redis 分布式文件:FastDFS
负载均衡:Keepalived+Nginx+Proxy(三重负载)



JEESZ UI
基于Bootstrap4
简洁、直观、强悍最受欢迎的 HTML、CSS 和 JS 框架,用于开发响应式布局、移动设备优先的 WEB 项目。
为所有开发者、所有应用场景而设计。
让前端开发更快速、简单。所有开发者都能快速上手、所有设备都可以适配、所有项目都适用。



项目管理
2018-08-10 10:27:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
1.页面链接测试
页面的链接是使用户从一个页面浏览到另外一个页面的重要手段,在做页面链接测试的时候,需要验证两个问题:
· 该页面是否存在,如页面不能显示信息,则视为页面链接无效。引起页面无效的因素有很多种,主要有页面文件不存在、链接的地址不正确等;
· 该页面是否跳转到所规定的页面,主要是验证页面正确性,这种测试也应该在Web 功能测试部分被考虑。
2.设计语言测试
这里的设计语言主要指HTML语言和不同的脚本语言,在某些情况下,HTML语言随着客户浏览器的不同可能会产生不同的效果,因此,这也是测试中需要考虑的因素。如在Netscape 4 7里面,不能将表单内容限定成为只读属性,这样当表单的内容需要禁用或者限制使用的时候,程序必须考虑其他的方式来实现,比如利用JavaScript脚本进行处理。
3. Web图形测试
Web图形是一种常见的显示信息的手段,如GIF图片等。很多时候,图形是和文本混合在一起使用的,因此,在Web图形测试的时候,不仅要确认文本是否正确,同时需要确认图片的内容和显示,如文字是否正确地环绕图片,图片的文字提示是否正确,图片所指向的链接是否正确等。当然,页面的负载测试中,图片显示也是一个重要因素,某些时候,在网络状态不好且图片文件比较大的时候,可能会遇到链接超时的错误,这些也需要被考虑在图形测试之内。图形测试还麻当考虑显示问题,例如不同分辨率下的图形显示是否正确,需要浏览器附加程序支持的图形是否能正确加载等。
4.表单测试
从设计的角度来看,表单是在访问者和服务器之间建立了一个对话,允许使用文本框、单选按钮和选择菜单来获取信息,商不是用文本、图片来发送信息。通常情况下.要处理从站点访问者发来的响应(即表单结果),需要使用某种运行在Web服务器端的脚本(如PHP、ISP),同时在提交访问者输入表单的信息之前也可能需要用浏览器运行在客户端的脚本(通常是使用JavaScript)。在进行表单测试的时候,需要保证应用程序能正确处理这些表单信息,并且后台的程序能够正确解释和使用这些信息。举个例子.用户可以通过表单提交来实现联机注册。当注册完毕以后,应该从Web服务器上返回注册成功的消息。
项目管理
2018-08-10 10:18:00
「深度学习福利」大神带你进阶工程师,立即查看>>> 什么是“开-闭”原则?
“开-闭”原则讲的是:一个软件实体应当对扩展开放,对修改关闭。这个原则说的是,在设计一个模块的时候,应当在不必修改源代码的情况下改变这个模块的行为。
满足“开-闭”原则的设计可以给一个软件系统两个无可比拟的优越性: 通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性。 已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性。
2.对可变性的封装原则 一种可变性不应当散落在代码的很多角落中,而应当被封装到一个对象里。 继承应当被看做是封装变化的方法,而不应当被认为是从一般的对象生成特殊的对象的方法。 一种可变性不应当与另一种可变性混合在一起。
3.设计原则 里氏代换原则
任何父类可以出现的地方,子类一定可以出现。里氏代换原则是“开-闭”原则的补充,实现“开-闭”原则的关键步骤就是抽象化。父类与子类的继承关系就是抽象化的具体体现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 依赖倒置原则
要依赖于抽象,不要依赖于实现。 合成/聚合复用原则
要尽量使用合成/聚合,而不是继承关系达到复用的目的。 迪米特法则
一个软件实体应当与尽可能少的其他实体发生相互作用。 接口隔离原则
应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。





项目管理
2018-08-08 21:26:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
单例模式: 确保一个类只有一个实例,并提供一个全局访问点。即多次new对象,只能获取同一个对象。
实现方式一:懒汉模式(线程不安全) public class Singleton { private static Singleton instance; private Singleton() {} public Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
实现方式二:饿汉模式(线程安全) public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public Singleton getInstance() { return instance; } }
构造函数是私有的,外部不能直接创建对象,只能通过静态方法getInstance()方法获取对象。
项目管理
2018-08-08 16:44:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
group_concat([DISTINCT] 要连接的字段 [Order BY ASC/DESC 排序字段] [Separator '分隔符'])

SELECT id,GROUP_CONCAT(score) FROM testgroup GROUP BY id;
SELECT id,GROUP_CONCAT(DISTINCT score) FROM testgroup GROUP BY id;
SELECT id,GROUP_CONCAT(score ORDER BY score DESC) FROM testgroup GROUP BY id;
SELECT id,GROUP_CONCAT(score SEPARATOR ';') FROM testgroup GROUP BY id;
项目管理
2018-08-08 16:27:05
「深度学习福利」大神带你进阶工程师,立即查看>>>
在自己的云平台上部署了IntelliJIDEALicenseServer服务,理论上http://www.jetbrains.com的产品都可以使用,不过目前只测试了PyCharm。
大家也知道,Python好用的IDE不多,PyCharm算是功能比较全的一个啦!
PyCharm LicenseServer:
http://www.yuanzhaoyi.cn:1017
http://www.yuanzhaoyi.cn:1027
Python编程,不再乎功能的话推荐使用wings IDE噢,界面清爽简单。
如果不在乎大小和麻烦的话,那就用eclipse的PyDev吧!
支持 PyCharm WebStorm GoLand
有兴趣部署破解服务器,请参考这里:
IntelliJ IDEA License Server: http://blog.lanyus.com/archives/174.html
项目管理
2018-08-08 14:38:00
「深度学习福利」大神带你进阶工程师,立即查看>>> # 新建文件夹 mkdir -p /usr/java cd /usr/java # 上传 jdk-7u80-linux-x64.tar.gz jdk-8u181-linux-x64.tar.gz # 到这个路径下。解压: tar -xzvf jdk-7u80-linux-x64.tar.gz tar -xzvf jdk-8u181-linux-x64.tar.gz # 创建软链接(ln命令格式:ln -s [源目标(原来的路径)] [目的目标(新的代号)]): ln -s jdk1.8.0_181 java8 ln -s jdk1.7.0_80 java7 ln -s java8 default [root@localhost java]# pwd /usr/java [root@localhost java]# ls -ahl total 324M drwxr-xr-x 4 root root 4.0K Aug 8 11:55 . drwxr-xr-x. 14 root root 4.0K Aug 8 11:00 .. lrwxrwxrwx 1 root root 5 Aug 8 11:55 default -> java8 lrwxrwxrwx 1 root root 11 Aug 8 11:20 java7 -> jdk1.7.0_80 lrwxrwxrwx 1 root root 12 Aug 8 11:20 java8 -> jdk1.8.0_181 drwxr-xr-x 8 10 143 4.0K Apr 11 2015 jdk1.7.0_80 drwxr-xr-x 7 10 143 4.0K Jul 7 16:09 jdk1.8.0_181 -rw-r--r-- 1 root root 147M Aug 8 11:13 jdk-7u80-linux-x64.tar.gz -rw-r--r-- 1 root root 178M Aug 8 11:12 jdk-8u181-linux-x64.tar.gz [root@localhost java]# # 修改 /etc/profile 在其后增加以下几句。 export JAVA_HOME=/usr/java/default export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/jre/lib/rt.jar export PATH=$PATH:$JAVA_HOME/bin # 修改之后 source /etc/profile 使之生效。
另一种方法: Linux配置多个jdk版本
项目管理
2018-08-08 12:00:00
「深度学习福利」大神带你进阶工程师,立即查看>>> 什么是里氏代换原则?
严格的表达是:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。换句话说,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且他根本不能察觉出基类对象和子类对象的区别。
假设有两个类,一个是Base类,另一个是Derived类,并且Derived类是Base类的子类。那么一个方法如果可以接受一个基类对象b的话:
method(Base b)
那么他必然可以接受一个子类对象d,也即可以有method(d)。则反过来不成立。

项目管理
2018-08-09 21:12:00
「深度学习福利」大神带你进阶工程师,立即查看>>> 现在学习请用文中的版本,以后可以用正式版: SpringCloud升级到Finchley.RELEASE(2.0.2.RELEASE):https://www.imooc.com/article/41648
涉及到的插件:
1. E ureka 用来服务注册发现
2. Feign 是用来远程调用服务,使用了Ribbon作负载均衡
3. Zuul 是用来做服务网关(路由、监控、容错、日志、授权、反爬虫)
4. Hystrix 用来服务降级、服务熔断、依赖隔离、监控
5. Spring Cloud Bus 组件用来自动更新配置,这里用到了RabbitMQ消息队列来传递消息,在需要自动更新配置的类加注解@RefreshScope
6. 统一配置中心 就是从git的config项目拉取配置,并在使用到配置的接入配置中心
7. zipkin 分布式链路调用监控系统
8. Rancher 主要负责图形化管理docker


(IntelliJ IDEA ULTIMATE 2017.2.5 30天试用期)
新建一个springCloud项目,如图:


在这里加个注解,启动tomcat后访问才不会404,加入这个注解@EnableEurekaServer,表示着有注册中心的功能。
添加application.yml,如下配置:
访问成功:
需要后台一直运行Eureka Server项目(使用命令行),这样不用每次用idea启动了:
![](https://oscimg.oschina.net/oscnet/ae42b5940af0929b5a17ab0ddee35e1c092.jpg)
一直后台运行的命令(4681是进程的id):
启动成功界面:

不想运行了则kill掉该进程:
项目管理
2018-08-09 14:43:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
1、 用户登录数据采集 用户登录
采集银行或者其他企业数据 首先需要用户登录利用java语言的URL方法获取登录url 或者使用java的开源工具HTTPClient模拟登录,用到的插件有IE的httpwotch工具和FireFox里的Firebug工具和cookie插件得到URL请求的post密码地址或者get密码地址。
2、 采集中对用户密码的登录难度及其验证码和证书解决方案
a) 对于普通的用户登录,只需要查看用户登录信息从中获取POST的值模拟post数据登录,抓取相关数据
b) 对于post提交中发现前台js做过md5加密,或者其他aes、des加密的,查看网站项目js源代码看是什么类型的加密方法,用其解密方法得到即可一般aes加密的key会存储在客户端,所以得花费一定时间找到其加密key即可模拟登录,有些加密是用js做的相关加密,或者用的一些开发加密,而其他语言的加密不相同这时候得需要Rhino JavaScript引擎技术去模拟js获取加密key再去登录。
c) 对于证书加密的解决方案 首先你要去打开一个 https:///www.xxx.com的网站 找到证书选择查看证书然后在详细信息里选择复制到文件选择der编制二进制x.509导出cer证书,将证书转换为c密钥的形式,在windows里有一个keytool工具用此命令导出为加密的keystore文件格式的密钥文件,再用httpclient的keyStore工具获取密钥库登录密钥库密码用sslsocketfactory注册密钥就可以达到密钥库登录的目的。
d) 对于第三方服务器认证服务器的密钥登录方式解决方案。一直登录通过firebug的url检测查看url访问路径和在html代码里对url的变动进行采集分析dom数据查看是否有变动将获取的url得到数据继续模拟登录直到访问数据成功即可,一般这个比较耗费时间。
e) 对于机器IE加密证书和必须安装相关插件的数据抓取方案。有些网站为了限制是一台机器登录这下得按用户登录方式模拟登录、模拟登录时下载证书、模拟访问证书路径、模拟下载证书后访问的页面最后他会有可能生成一个key这个key有可能是按天去生成的有可能每天得去模拟一次然后按照用户post的数据模拟登录提交key即可。
f) 对于有验证码破解的登录方式,用程序将图片去灰度做调整,用Google的开源项目tesseract-ocr去check验证码,有些验证码很难破解的得想办法建立字典学习库。收集一些验证码让程序去学习达到破解的方法(注*得会用c语言写接口和左http服务 Google开源的tesseract-ocr用的c语言),也可以自己写算法实现验证码的破解。
g) 对于U-key登录方式的破解,看此U-KEY存储的是什么类型的key,有些是用证书做的key,有些是不对称加密key对于不同的key有不同的解决方案,可以破解key做一个模拟器,或者是把加密狗破解复制到其他加密狗里再去用。
3、 数据采集页面数据分析之分页和数据日常抓取相关注意事项
a) 对于普通的页面的分页数据分析,一般规律在html分页会存在于url里的参数对于一些带有search的先用sql注入测试一下是否可以直接出现所有数据一次爆出。如果是普通的直接url提交对应的page即可。
b) 对于一些aspx类型网站的数据抓取一般aspx网站很多人都会用一些做好的dll文件很多都会存在会话功能,就是访问第二页数据他会读取相关第一页面数据的input里hidden里的数据,每一页的数据都有所不同只需模拟抓取即可。
c) 对于其他的一些页面数据的抓取有些是ajax返回数据而前台无法解析的数据,对于这样的数据抓取最好的办法是直接用java解析json数据即可。有可能寻找json有些费力。
d) 对于有些横向分页和纵向分页数据的抓取,只能横向切换抓取然后再纵向抓取所有分页数据,有些横向数据的post参数咱们可以自己定义,并抓取得到数据。
4、 数据分析清洗数据
对于一些数据的清洗大致都是用Jsoup、xpath、regex做数据解析分析处理即可。有些数据得需要一些算法把数据取出来,二级数据页面分析解析方式取出数据即可。(对于不同的数据选择不同的解决方案)
5、 对于用户限制和ip限制的解决方案
对于用户限制数据抓取方案只能采取时间间隔掌握网站频率抓取数据和排查用户登录对于多用户登录和ip查看的方案去抓取数据也可以申请多个账号进行抓取,对于ip限定抓取的方式是可以伪装Google IP抓起数据,和用ADSL拨号方式抓取数据。需要消耗的时间比较长。其他方式可以买代理ip和高保真代理数据抓取。
6、 对于下载和编码相关注意事项
对于普通的抓取来说一般用url的数据流下载相关文档即可解析word和excel一般用POI即可达到效果对于pdf来说一般用PDFParser分析即可。其中有一些会造成乱码大多数是用InputStream之类所造成的一般用utf-8\gbk请求时查看请求的头信息看编码做对应的修改即可,有些网站得需要用字节才能才能解决乱码。对于下载有些则需要修改请求头信息才能下载。
对于一些定制化的数据抓取只能写定制化抓取方案解决,统一种子中心分发下载达到分布式数据抓取。(注*有些网站无法用分布式数据采集)。
项目管理
2018-08-09 10:50:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
引言
今天不聊技术,只聊人生。分享一下我在使用Spring Cloud Config过程中遇到的坑,以及是如何解决的,这其中,主要看解决方法和思路,技术只占一小部分。一家之言, 仅供参考.
正文
在某个特别适合敲代码的深夜,我使用 SpringCloud Config (以下简称Config)搭配 SpringCloud Bus (以下简称Bus)实现动态刷新配置. 使用的SpringCloud版本是Finchley.M2,M2就是里程碑版本,非正式版.
第一个坑
要实现配置动态刷新, 修改配置后, Bus会发送消息到MQ, 我发现这一步就走不通. 于是 网上搜索问题(用google), 发现有人遇到 同样的问题 , 可惜的是, 也没有得到解决. 搜索的时候留意SpringCloud的版本. 怀疑是不是我用的姿势不对, 新版本有变化? 查阅官方文档,我的用法没问题. 立马向官方提了 issue
老外的效率很高, 第二天就把这个bug fix了, 如果我要立马用的话, 必须用Finchley.BUILD-SNAPSHOT版本, 这里顺便提一下, SNAPSHOT版本是可覆盖的版本, 今天看到的Finchley.BUILD-SNAPSHOT和明天看到的Finchley.BUILD-SNAPSHOT很可能是不一样的.
第二个坑
Bus的坑算是已经填好了, 但让我意识到, 里程碑版本应该很多坑等着我.....但, 自己选择的路, 跪着也要走完......
在测试git的WebHooks时, 发现 Gitee的推送 无效, Github是没问题的. 于是 不用太费力搜索了, 简单搜一下就行, 因为老外用Gitee肯定少, 把Conifg和Gitee放一起用的人肯定更少. 到目前为止, 还不知道是Gitee的问题还是Config的问题(有人要说了, Config既然支持Github那肯定不是它的问题, 其实未必!)
所以只能自己先去看源码了, 看看到底是什么问题, 一说到看源码, 很多人可能就要头疼了, 因为源码很多, 不知道从哪下手, 如果你能这么想, 说明你还挺聪明的, 至少第一想法不是把源码全部阅读完. 从哪下手呢? 填在WebHooks的地址是 /monitor, Config里有个模块是 spring-cloud-config-monitor , 显然应该从这下手 如果当初没找到这么明显的特征, 那我会在Config搜索“github”关键字
打开源码一看, 原来Config只对以下git服务做了适配
如果git服务不在3者内, Conifg还有个通配的
遗憾的是, Gitee的WebHooks Request里并没有path字段, 所以也没有触发Config. 我还对比了Gitee和Github的WebHooks推送的字段区别, 发现Gitee并没有以下的字段 added, removed等
问题已经找到, 怎么解决? 给Gitee反馈, 希望加上added, removed等字段. 目前已经修改上线. 给Config反馈, 希望支持Gitee, 见 issue , 结果老外让我自己来写, 提pr
好吧, 说干就干, 提了一个 pr , 提个pr也不容易啊
spring团队对代码要求严格
单测什么的就不说了, 这是必须的, 让我以外的是, 连import的顺序也有要求, 比如, 必须 import java.util.Collection; import org.springframework.core.Ordered;
下面这种写法是不允许的 import org.springframework.core.Ordered; import java.util.Collection;
为此他们提供了一份代码格式化的插件.
代码修改的同时, 文档也要相应的修改.
这次pr整整20多天才merged进去, 代码会在Config 2.0.0.M8里面.
后记
有的人或者团队, 遇到此类问题, 可能会fork一份源码, 这样自己改掉立马就可以使用. 这让我想起一段话, 大意如下: 普通人卖自己的时间, 如果你1小时能赚100元, 那你24小时不休息最多能赚2400元. 聪明人复制自己的时间, 比如写一本书花了1年的时间, 10个人来买和10000人来买, 作者都是花了1年的时间, 但收益却天壤之别. 更聪明的人会买别人的时间, 你的老板不就是花钱买你的时间嘛.
作者:廖师兄
链接:https://www.imooc.com/article/23703
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作
项目管理
2018-08-09 10:11:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
备注 在创建证书时候的背景信息: ' keytool -genkeypair ' 命令将会创建秘钥对,包括公钥和关联的私钥,然后存储到 keystore 中。这个命令打包公钥为 X.509 v3 自签名证书,同时存储为证书链中的单一元素。这个证书链和私钥同时存储为一个新的 keystore 实例。是通过你在命令行中指定的别名进行识别的。 Java SE documentation 能帮你更好的使用这个工具。 自定义 SSL 端口 :如果你修改了你的 SSL 连接器运行的默认端口(8443),你必须需要更新标注 HTTPS 连接的 redirectPort 属性来映射到新的 SSL 端口。Tomcat 需要这些信息才能知道针对访问 HTTPS 应该重定向到那个端口。 同一主机的多实例 :当你在同一主机中运行了多个实例,对地址( address )的确定非常重要,这个属性定义在 /conf/server.xml 文件中,默认的连接器 将会列出所有可用的网络接口 ,因此指定地址将会阻止在同一主机中运行相同端口中的连接器的冲突。请参考 Tomcat 连接器的文档来获得有关地址属性的更多信息: https://tomcat.apache.org/tomcat-8.0-doc/config/http.html
maxThreads= "150" minSpareThreads= "25" maxSpareThreads= "75"
enableLookups= "false" disableUploadTimeout= "true"
acceptCount= "100" scheme= "https" secure= "true"
clientAuth= "false" sslProtocol= "TLS" SSLEnabled= "true"
URIEncoding= "UTF-8" keystorePass= ""
keystoreFile= "" />
HTTPS 必须在你全站点配置 :HTTPS 不能仅加密一个独立的页面或者空间。 在你对 Confluence 进行升级之前 :对你修改过的 server.xml 和 web.xml 文件进行记录。每一升级完成后,你都需要对你修改过的配置文件进行重新配置。最好还是手动添加,而不是将整个文件复制粘贴。
问题解决 请参考 Confluence 知识库中有关的文章 troubleshooting SSL 。 有关 Internet Explorer 不能下载附件 的问题,将全站点应用 SSL 将会可能导致 IE 不能正确的下载附件。要修复这个问题,请编辑 /conf/server.xml 然后添加下面的行到 元素:
disableProxyCaching= "true" securePagesWithPragma= "false" />
https://www.cwiki.us/display/CONF6ZH/Running+Confluence+Over+SSL+or+HTTPS
项目管理
2018-08-09 02:15:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
尽管现在 HTTPS 现在已经激活并且可用了。老的 HTTP URLs ( http://localhost:8090 )还是可以访问的。现在你需要重定向所有 URLs 到他们的 HTTPS 链接中。你可以通过在 web.xml 文件中添加加安全常量来达到这个目的。这个能够让 Tomcat 将不是 SSL 端口的访问重定向到 SSL。 检查你的 Confluence 站点的用户是否使用了 RSS macro 。如果你的站点启用了 RSS macro 宏,你可能需要在防火墙规则中配置 Confluence URL 重定向,而不需要通过修改 web.xml 文件达到这个目的。请跳过下面的步骤,然后访问 RSS Feed Macro 页面来获得更多信息。 否则,编辑 /confluence/WEB-INF/web.xml 文件中的内容。 添加下面的定义到文件的页尾,但是需要在 标签的 前面 :


Restricted URLs
/


CONFIDENTIAL


重启 Confluence 然后访问 http://localhost:8090 ,现在你应该是直接重定向到 https://localhost:8443/login.action 中了。
Confluence 有 2 个 web.xml 文件。另一个文件在 /conf/web.xml。 请仅仅按照上面描述的内容在 /confluence/WEB-INF/web.xml 中进行修改。
https://www.cwiki.us/display/CONF6ZH/Running+Confluence+Over+SSL+or+HTTPS
项目管理
2018-08-09 02:15:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
在默认的情况下,Tomcat 希望 keystore 文件被命名为 .keystore 文件,同时这个文件应该放置在 Tomcat 运行的 home 目录中(这个目录可能与你自己的 Home 目录的路径不同)。这个配置的意思是,在默认情况下 Tomcat 将会到下面 2 个位置中去检查你的 SSL 证书: On Windows: C:
项目管理
2018-08-09 01:58:01
「深度学习福利」大神带你进阶工程师,立即查看>>>
Atlassian 应用可以通过 HTTPS 进行访问,但是 Atlassian 并不提供有关访问的支持服务,同时 Atlassian 不能保证能够提供所有的支持 。 如果你的 assistance 在证书转换方面有相关服务的需求,请咨询你证书的提供商来获得他们的帮助。 如果你在 assistance 证书配置上行有相关需求,请到 Atlassian Community 中提问。
这个页面提供了如何在 Confluence 中启用访问 HTTPS(HTTS Secure)的一些基本信息,通过这样的配置你的 Confluence 登录和数据传输将会采用加密的方式进行。这对你的 Confluence 的安全性将会有一些提升,用户登录使用的用户名和密码数据是加密的,能够更好的保障系统的安全性。
在本文章中,我们使用 'SSL' 来定义在加密传输中参考使用的加密协议。在很多时候,这个加密协议是 TLS 。
这个指南包含了在 Tomcat 中使用 SSL 的相关信息,Tomcat 是捆绑在 Confluence 服务器上一起发布的。
如果你决定使用 SSL 并且使用服务器的反向代理,请参考 Apache with mod_proxy 或 Running Confluence behind NGINX with SSL 页面中的内容,来确定如何在你的 Web 服务器上使用 SSL。
你需要使用 JDK 1.8 来完成安装过程中的一些步骤。JER 的版本不足以支持所有的安装。
https://www.cwiki.us/display/CONF6ZH/Running+Confluence+Over+SSL+or+HTTPS
项目管理
2018-08-09 01:53:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
在启用 HTTPS 之前,你需要一个有效的证书,如果你已经有了一个有效的证书,你可以直接跳过这个步骤,进入 step 2 。
你可以创建一个自签名的证书,或者从信任的 Certificate Authority 中获得一个证书。
如果你的项目小组计划使用 Confluence 服务器移动 app。你需要你的证书是从信任的证书签发机构签发的。你不能使用自签名的证书或者从一个不信任的机构获得的证书,或者自由 CA。
选项 1: 创建一个自签名证书
当你需要进行加密,但是你并不需要对网站的的请求校验的话,自签名证书能够帮助你完成这个。在通常的情况下,你可以在你的测试环境下签发你的自签名证书,你也可以在你公司内部的网络上签发自签名证书。
因为证书不是信任的组织签发的(CA),用户可能会收到站点不被信任,并且提供一个步骤让用户先确定,才能访问网站的信息。这个通常是在第一次访问网站的时候出现的提示。如果你使用的 Confluence 的移动 app 的话,用户不能通过你的自签名证书访问你的 Confluence 站点。
在这个情况下,我们需要使用 Java 的 keytool 工具包。这个工具包是包含在 JDK 中的。如果你对命令行工具并不是十分熟悉的话,你可以考虑使用 KeyStore Explorer 工具。
使用 keytool 来创建一个自签名证书: 从命令行中,为你的操作系统运行正确的命令:
Windows
"%JAVA_HOME%\bin\keytool" -genkeypair -keysize 2048 -alias tomcat -keyalg RSA -sigalg SHA256withRSA
Linux (and MacOS)
$JAVA_HOME/bin/keytool -genkeypair -keysize 2048 -alias tomcat -keyalg RSA -sigalg SHA256withRSA
当出现提示后,为你的证书创建 密码(password ) ,私有 key。 仅使用数字和英文字符。如果你使用了特殊字符,Tomcat 可能会出现错误。 请记录你创建的密码,在下一步中你需要使用到你创建的密码。 默认的密码是 ' changeit '。 根据提示来确定证书的细节。这些信息被用来构造 X.500 实体中的 Distinguished Name (DN) 。
First and last name :这个不是你的名字,这个是 Common Name (CN),例如 ' confluence.example.com '。CN 必须与 Confluence 使用的域名完全对应,否则 Tomcat 将不能使用你签名的证书。 Organizational unit :这个是证书使用的部门或者小组,例如 'marketing'。 Organization :是你公司的名字,例如 'SeeSpaceEZ'。 City, State / province, country code :这个是你公司的地理位置,例如 Sydney, NSW, AU。 输出将会如下所示。输入 ' y ' 来确定你输入的内容。
CN= confluence.example.com, OU=Marketing, O=SeeSpaceEZ, L=Sydney, ST=NSW, C=AU
当被询问为 'tomcat' 准备使用的 密码( password )的时候,输入你第二步中输入的密码(在输入密码后单击回车)。 'tomcat' 是你在 keytool 命令行中输入的别名,在这里用来对你提示。 你 keystore 实例必须和你的私有 key 有相同的密码。这个是 Tomcat 服务器要求的。 你的证书现在已经可以用了,进入 下面 的第二步。
选项 2: 使用 Certificate Authority 签发的证书(推荐)
在生产环境中,你需要使用从 Certificate Authority (CA) 签发的证书。下面的内容是从 Tomcat documentation 中拷贝出来的。
首先你需要创建本地证书,然后基于你创建的本地证书再创建一个 'certificate signing request' (CSR) 。你需要提交 CSR 到你的选择的 CA 提供商上进行收取。CA 将会通过 CSR 将授权后的证书发给你。 使用 Java 的 keytool 工具来创建一个本地证书(请按照上面第一步所描述的内容)。 从命令中,将会返回下面的命令工具来创建所需要前面的证书。
keytool -certreq -keyalg RSA -alias tomcat -file certreq.csr -keystore
替换 为路径和你本地证书创建 .keystore 的文件名。 提交创建的文件为 certreq.csr 到你希望进行授权的 CA。
请参考 CA 的文档来找到如何进行这个操作。 CA 将会发个你已经签名好的证书。 导入新证书到你的本地的 keystore:
keytool -importcert -alias tomcat -keystore -file
一些 CA 可能要求你在安装你的证书之前先安装一个中间人证书。你应该按照 CA 提供的文档来完成你本地证书的成功安装。
如果你使用的是 Verisign 或 GoDaddy,然后你收到了错误的信息,你可能需要将 PKCS12 和你的私钥(private key)同时导出。 首先,删除添加到 keystore 中的所有 key:
keytool -delete -alias tomcat -keystore
然后导出为 PKCS12 格式:
openssl pkcs12 -export -in -inkey -out -name tomcat -CAfile -caname root
然后导入 PKCS12 到 jks 中:
keytool -importkeystore -deststorepass -destkeypass -destkeystore -srckeystore

https://www.cwiki.us/display/CONF6ZH/Running+Confluence+Over+SSL+or+HTTPS
项目管理
2018-08-09 01:52:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
下一步你需要配置 Confluence 来使用 HTTPS: 编辑 < install-directory>/conf/server.xml 文件。 取消注释下面的行:
< Connector port = "8443" maxHttpHeaderSize = "8192"
maxThreads = "150" minSpareThreads = "25"
protocol = " org.apache.coyote.http11.Http11NioProtocol "
enableLookups = "false" disableUploadTimeout = "true"
acceptCount = "100" scheme = "https" secure = "true"
clientAuth = "false" sslProtocols = "TLSv1, TLSv1.1,TLSv1.2 "
sslEnabledProtocols = "TLSv1, TLSv1.1,TLSv1.2 " SSLEnabled = "true"
URIEncoding = "UTF-8" keystorePass=""/>
替换 < MY_CERTIFICATE_PASSWORD> 为你在证书中使用的密码。 确定 attribute-value 匹配 SSLEnabled="true" 是连接元素的一部分,如上面所示。如果这个特性没有被显示出来,尝试访问 Confluence 可能会遇到超时错误。 保存服务器配置文件。
不要删除 http 连接器中的备注,Synchrony 代理被从来进行健康检查,这个检查还是通过 HTTP 访问的。如果你不希望包括 http 连接器。你可以使用 synchrony.proxy.healthcheck.disabled 系统属性来禁用健康检查。
你 不应该 禁用内部的 Synchrony proxy(通过设置 synchrony.proxy.enabled 系统属性为 false ),这个我们已知了在 Tomcat 确定 SSL 的时候将会导致问题。
默认针对 Confluence 的连接器端口为 8090.

https://www.cwiki.us/display/CONF6ZH/Running+Confluence+Over+SSL+or+HTTPS
项目管理
2018-08-09 01:52:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
参考 《设计模式:可复用面向对象软件的基础 》3.2 Builder 生成器--对象创建型模式 《Android源码设计模式解析与实战》第3章 Builder模式
意图
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
适用性 相同的方法,不同的执行顺序,产生不同的事件结果时。 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时。 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的作用,这个时候使用建造者模式非常合适。 当初始化一个对象特别复杂,如参数多,且很多参数都具有默认值时。
结构
Product 表示被构造的复杂对象 包含定义组成部件的类,包括将这些部件装配成最终产品的接口 Builder 抽象Builder类,为创建一个Product对象的各个部件指定抽象接口,规范产品的组建,一般是由子类实现具体的组建过程。 ConcreteBuilder 具体的Builder实现类 Director 使用Builder接口进行构造
协作 客户创建Director对象,并用它所想要的Builder对象进行配置。 一旦产品部件被生成,Builder就会通知生成器 生成器处理Builder的请求,并将部件添加到该产品中。 客户从生成器中检索产品
例子1:RTF阅读器
一个RTF(Rich Text Format)阅读器应能将RTF转换为多种正文格式。该阅读器可以将RTF文档转换为普通ASCII文本或者一个能以交互方式编辑的正文窗口组件。但问题是在于可能转换的数目是无限的。因此要能够很容易地实现新的转换的增加,同时却不改变RTF阅读器。
一种解决办法就是运用Builder模式,用一个可以将RTF转换成另一种正文表示的TextConverter对象配置这个RTFReader类。当RTFReader对RTF文档进行语法分析时,它使用TextConverter去做转换。
每种转换器类将创建和装配一个复杂对象的机制隐含在抽象接口的后面。转换器独立于阅读器,阅读器负责对一个RTF文档进行语法分析。
每一个转换器类可以作为生成器builder,而阅读器作为director。Builder模式将分析文本格式的算法与描述怎样创建和表示一个转换后格式的算法分离开来。这使得我们可以重用RTFReader的语法分析算法,分局RTF文档创建不同的正文表示--仅需使用不同的TextConverter的子类配置该RTFReader即可。
例子2:组装电脑
我想随意组装一台电脑,比如一台Inter主板,Retina显示屏,MacOs的电脑
计算机的组装过程较为负复杂,并且组装顺序是不固定的,所以我们可以用Buider模式。
示例代码: // 电脑基础抽象类 public abstract class Computer { protected String mBoard; protected String mDisplay; protected String mOS; protected Computer(){} //设置主板 public void setBoard(String board){ this.mBoard=board; } //设置显示器 public void setDisplay(String display) { mDisplay=display; } //设置操作系统 public abstract void setOS(); @Override public String toString() { StringBuilder stringBuilder=new StringBuilder(); if (mBoard!=null) { stringBuilder.append("myBoard is:"+mBoard); } if (mDisplay!=null) { stringBuilder.append("\nmDisplay is:"+mDisplay); } if (mOS!=null) { stringBuilder.append("\nmyOS is:"+mOS); } return stringBuilder.toString(); } } //Macbook电脑,实现了setOs public class MacBook extends Computer{ @Override public void setOS() { mOS="Mac _S X 10.10"; } } // 抽象Builder public abstract class Builder{ abstract Buider buildBoard(String board); abstract Buider buildDisplay(String display); abstract Buider buildOS(); abstract Computer create(); } //具体的一个Builder类 public class MacBookBuilder extends Buider { private Computer mComputer=new MacBook(); @Override public Buider buildBoard(String board) { mComputer.setBoard(board); return this; } @Override public Buider buildDisplay(String display) { mComputer.setDisplay(display); return this; } @Override public Buider buildOS() { mComputer.setOS(); return this; } @Override public Computer create() { return mComputer; } } // Director类,负责构造Computer public class Director{ Builder mBuilder = null; public Director(Builder builder){ mBuilder = builder; } public void construct(String board,String display){ mBuilder.buildBoard(board); mBuilder.buildDisplay(display); mBuilder.buildOs(); } } //使用 public static void main(String[] args) { Builder macBookBuilder=new MacBookBuilder(); Director director = new Director(macBookBuilder); // 使用Director构造 director.construct(英特尔主板,retina); System.out.println(macBookBuilder.create().toString()) // 通常我们都会通过Builder的链式调用省略Director类,比如如下所示 System.out.println(macBookBuilder.buildBoard("英特尔主板") .buildDisplay("retina") .buildOS() .create().toString()); } 结果: myBoard is:英特尔主板 mDisplay is:retina myOS is:Mac _S X 10.10
效果 它使你可以改变一个产品的内部表示 它将构造代码和表示代码分开 它使你可对构造过程进行更精细的控制 《Effective java》书中建议如果构造一个对象复杂,需要传入很多参数,不妨采用Builder模式,有效减少了传参失误。 当然缺点是增加了代码量。
例子3 Android中的AlertDialog.Builder
// todo 后更
项目管理
2018-08-09 00:04:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
一.接口 什么是接口?
接口是一个类所具有的方法的特征集合,是一种逻辑上的抽象。接口和类最重要的区别是,接口仅仅描述方法的特征,而不给出方法的实现;而类不仅给出方法的特征,而且给出方法的实现。具体说来接口代表一个角色,它包装与该角色相关的操作和属性,而实现这个接口的类便是扮演这个角色的演员。一个角色可以由不同的演员来演,而不同的演员之间除了扮演一个共同的角色之外,并不要求有其他任何的共同之处
2.接口是对可插入性的保证 关联的可插入性
一个对象需要完成一项任务,所以需要知道其他对象,并且调用其他对象的方法。这个对象对其他的对象的知识叫做关联(Association),一般使用成员变量来实现。 调用的可插入性
一个对象不可避免的需要调用其他对象的方法。这种调用不一定非得是某一个具体类,而可以是一个接口。
应当使用Java接口和抽象Java类而不是具体类进行变量的类型声明,参数的类型声明,方法的返还类型声明,以及类型数据的转换等。
3.Java接口的常见用法 单方法接口 标识接口
标识接口是没有任何方法和属性的接口,他仅仅表明实现它的类属于一个特定的类型。 常量接口
凡是实现常量接口的Java类都会自动继承这些常量,并且都可以像使用自己的常量一样使用这些常量。
二.抽象类
1.什么是抽象类?
抽象类仅提供一个类型的部分实现。抽象类可以有实例变量,以及一个或多个构造器。抽象类可以同时有抽象方法和具体方法。
2.抽象类的用途 具体类不是用来继承的
代码重构的建议
抽象类应当拥有尽可能多的共同代码
抽象类应当拥有尽可能少的数据



项目管理
2018-08-08 22:36:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
《代码大全》读书笔记
Chapter2 Mataphors for a Richer Understanding of Software Development
第 2 章 用隐喻来更充分地理解软件开发
2.1 The importance of Metaphors
2.1 隐喻的重要性
通过把你不太理解的东西和一些你较为理解、且十分类似的东西做比较,你可以对这些不太理解的东西产生更深刻的理解。这种使用隐喻的方法叫做“建模( modeling )”.
2.2 How to Use Software Metaphors
2.2 如何使用软件隐喻*
算法和隐喻的区别 算法是一套定义明确的指令,使你能完成某个特定的任务。 算法是可预测的(predictable)、确定性的(deterministic)、不易变化的(not subject to chance)。 启发式方法(试探法)是一种帮助你寻求答案的技术,但它给出的答案是具有偶然性的(subject to chance),因为启发式方法仅仅告诉你该如何去找,而没有告诉你要找什么。 一个告诉你如何从A点到B点的算法,不会让你绕路,不会让你额外地经过D、E、F等地方,更不会让你停下来闻闻玫瑰花或喝咖啡。 启发式方法并不会告诉你该如何直接从A点到达B点,它甚至可能连A点和B点在哪里都不知道。 举个例子来说,驾驶汽车到达某人的家,写成算法是这样的: 沿167号告诉公路往南行至Puyllap;从South Hill Mall出口出来后往山上开4.5英里;在一个杂物店旁边的红绿灯路口右转,接着在第一个路口左转;从左边褐色大房子的车道进去,就是North Cedar路714号。 用启发式方法是这样的: 找出上一次我们寄给你的信,照着信上的寄出地址开车到这个镇;到了之后你问一下我们的房子在哪里。这里每个人都认识我们——肯定会有人愿意帮你的:如果你找不到人,那就找个公共电话亭给我们打电话,我们会出来接你。
那么该如何使用软件中的隐喻呢?应该用它来提高你对编程问题和编程过程的洞察力;用它来帮助你思考编程过程中的活动,想象出更好的做事情的方法。你不可能看到一行代码并说它违反了本章所描述的某个隐喻。但随着时间的流逝,人们会发现,相对于不善于运用隐喻的人来说,那些使用隐喻来照亮自己的软件开发过程的人,他对于编程的理解会更好,并且能够更快的写出更好的代码。
2.3 Common Software Metaphors
2.3 常见的软件隐喻
Software Penmanship:Writing Code
软件中的书法:写作代码
对于个人规模的工作乃至小型的项目来说,这种写信的隐喻已经足够了,然而对于其他场合而言,这个隐喻还远远不够——它没有完整的、充分刻地刻画软件开发工作。
Software Farming:Growing System
软件的耕作法:培植系统
Software Oyster Farming:System Accertion
软件的牡蛎养殖观点:系统生长
你需要学会如何一次为软件系统增加一个小部分。跟“生长”密切相关的另一些词语有:“增量的(incremental)”、“迭代的(iterative)”、“自适应的(adaptive)”、以及“严谨的(evolutionary)”。以增量的方式进行设计、编译和测试,都是目前已知的最强有力的软件开发概念。 在进行增量式开发时,我们先做出软件系统的一个尽可能简单、但能运行的版本。它不必接受真实的输入,也无须对数据进行真正的处理,更不用产生真实的输出——它仅仅需要构成一个足够强壮的骨架,支撑起未来将要开发的真实系统。对于你标志出的每一项基本功能,可能仅需要调用虚假的类(dummy class)。这个最基本的起点,就像牡蛎开始孕育珍珠的那颗细小沙粒。 在骨架形成之后,你要一点点地在其上附着肌肉和皮肤:把每个虚假的类替换为真正的类;不再假装接受输入,而是把接收真实输入的代码替换进去;不再假装产生输出,而是把产生真实输出的代码替换进去。你一次增加一小部分代码,直到得到一个完全可以工作的系统。
Software Construction:Building Software
软件构建:建造软件
建造软件的这一说法暗示了软件开发过程中存在着诸多阶段,如计划、准备及执行等,根据所建造软件的不同,这些阶段的种类和程度可能会发生变化。进一步研究这一隐喻时,你还会发现许多其他方面的相似之处。 当开发软件时,你会大量使用高级语言所提供的功能,而不会自己去编写操作系统层面的代码。你可能还要用些现成的程序库,比如说一些容器类(constainer class),科学计算函数、用户洁面组件、数据库访问组件,等到。总之,自己编写那些能买得到的现成的代码通常是没有意义的。 精心计划,并非意味着事无巨细的计划或过度的计划。你可以把房屋结构性的支撑(structural support)规划清楚,而在日后再决定是用木地板还是地毯,墙面漆成什么颜色,屋顶使用什么材料,等等。一项规划得当的项目能够提升你“在后期改变细节(设计)的能力”。你对同类软件的开发经验越丰富,(在开发新软件时)就能认准更多的细节。你只需要保证已经做了足够的计划,不会到后来因为计划上不足而引发重大问题。
Applying Software Techniques:The Intellectual Toolbox
应用软件技术:智慧工具箱
技术并不是规矩(rule),它只是分析工具(analytical tools).好的工匠知道完成某项工作要用哪样工具,也知道该怎样正确地使用。程序员也该这样。编程方面的知识学的越多,你脑中的工具箱中就会有更多的分析工具,也会知道该在何时用这些工具,以及怎样正确地使用它们。 在软件领域里,专业的咨询人员有时会让你用某种软件开发方法而远离其他方法。这样并不妥当,因为当你百分之百地依赖于某一方法论时,你就只会用一种方法去看世界了。某些情况下,对于你所面临的问题还有其他更好的方法,你可能错失良机。这种“工具箱隐喻”能够帮助你把所有的方法、技术以及技巧留在脑海中——合适的时候即可拿来就用。
Key Points
要点 隐喻是启示而不是算法。因此它们往往有一点随意(sloppy). 隐喻把软件开发过程中其他你熟悉的活动联系在一起,帮助你更好地理解。 有些隐喻比其他一些隐喻更贴切。 通过把软件的构建过程比做是房屋的建设过程,我们可以发现,仔细地准备是必要的,而大型项目和小型项目之间也是有差异的。 通过把软件开发过程中的实践比做是智慧工具箱中的工具,我们又发现,每位程序员都有许多工具,但并不存在任何一个能适用于所有工作的工具,因地制宜地选择正确工具是成为能有效编程的程序员的关键。 不同的隐喻彼此并不排斥,应当使用对你最有益处的某种隐喻组合。
项目管理
2018-08-03 16:22:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
openSUSE imagewriter is a GPL licensed project which contains an example of how to do this. (Relevant files: udisks2_interface.*)
a{sv} is a dict of string:variant pairs. QVariantMap would fit this signature.
a{sa{sv}} is a dict of string:a{sv} pairs. QMap would fit this signature.
a{oa{sa{sv}}} is a dict of objectpath:a{sa{sv}} pairs. QMap> would fit this signature.
We should hide those angle-brackets behind some typedefs in a header file:
typedef QMap InterfaceList; typedef QMap ManagedObjectList; Then declare their QMetaTypes in the same header file:
Q_DECLARE_METATYPE(InterfaceList) Q_DECLARE_METATYPE(ManagedObjectList) Then register them with the Qt metatype system at runtime:
qDBusRegisterMetaType(); qDBusRegisterMetaType(); Then we can annotate the XML:
项目管理
2018-08-03 16:15:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
使用 mybatis generator 自动生成代码,生成项目所需要的Model层 、 Mapper层、mapping映射文件(本文件设置属性将映射文件XML转换成Java类)。下图是:代码生成器

如何操作自动生成器,步骤如下: 首先修改并选择自己本地的数据库驱动包,然后修改数据库的URL,用户名,密码。 然后修改要生成Model层、Mapper层的包路径,和生成文件的存放的路径(targetPackage:生成的类要放的包,真实的包受enableSubPackages属性控制;targetProject:目标项目,指定一个存在的目录下,生成的内容会放到指定目录中,如果目录不存在,MBG不会自动建目录) 在table中写自己需要生成的数据库表名和实体类型
4. 双击mybatis-generator便能执行此文件。
项目管理
2018-08-03 16:11:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
通常一个团队中可能有人用eclipse,有人用intelliJ,那么经常会出现需要导入别人用eclipse建好的web项目。而IntelliJ提供了多种项目类型的导入方式,其中就有eclipse。
在初始窗口中点击Import Project,在文件树中选择你的项目,打开导入窗口,选择Eclipse选项,点击下一步

选择你的项目名称,可以选择下方的Open Project Structure after import,会在导入完成后,直接打开项目设置窗口;若不选,可以后面手动打开,区别不大。下一步。

选择jdk,我这里已经配置好。若没有,点击左侧+号,配置jdk信息。然后选择一项你需要的jdk版本,点击完成。

导入就完成了,这时候主窗口就打开了,左侧是项目目录,右侧是编辑区域。下图是目录树结构,发现已经生成了.idea目录和web_test.iml文件,这些是idea的IDE依赖文件。

若是前面选择了Open Project Structure after import,此时会自动打开下图窗口,若是没有选择,那么鼠标点击项目名称后,按F4打开。此时我们看到highlight部分是选择的jdk版本,下面是默认的编译输出路径,但是这个暂时是没用的,后面会被覆盖掉,所以不用管它。

左侧选择下一个菜单,Modules,这是idea的概念,project下面包含了Modules。这里插一句,有很多人会对这个概念很迷糊甚至不齿,但这个概念的确是正确的,它是来自于maven的思想,如果你看到过一些大型开源软件,开源nosql的源码就知道,一个project下面会分很多模块开发。但是对于常规的java web来说,不用管那么多,就理解为一个project就是一个module,两者合二为一就行。
这个菜单下,首先点击右侧三个横向标签页Dependencies,看下图,底下显示了3 errors found,另外我手贱去画了几条高亮,其实这些高亮处都是红字,说明在idea里是不存在的。包括SDK一栏,和下面两个jar包,都是从eclipse依赖文件中读出的,因此全部删掉。


然后选择三个横向标签页的Paths,会看到highlight处,这里就是我说的覆盖掉前面的编译输出路径,当然,使用默认就可以,不需要改动。会将class文件放到src同级的build目录下。

三个标签页的第一个Sources如下图,没有需要修改的,这里主要定义了几个目录的角色,通过颜色标识,比如src默认定义为Sources,其他暂时都没有角色。而如果项目是用maven构建的,那么Sources,Tests,Resources和Test Resources就都有对应的目录了。可以看出,idea很大程度上都是依靠于maven的思想,或者说它底层很多地方就是靠maven来运行的。而这几个不同的目录角色,各有一些独有的特性,如果不去搞清楚也不要紧。因为这些目录,对于eclipse来讲,都是classpath一个角色。

接着,选择左侧Facets菜单,这个概念在eclipse里也是有的,就是标明项目是由哪些组件构成的,如web,spring,strtus,hibernate等。点击+号,进行选择,这里选择web。

点击后,会发生有趣的事情,左侧菜单会跳转到Modules,而右侧,展现出web配置相关的表单。关于左侧的跳转,你在配置万Facets后会发现,这两个菜单项中都会存在这些配置,web,spring等等。我并不是很理解为什么,猜测和多模块的思想有关。
右侧配置项中,两处高亮,上面是web.xml路径,下面是web根目录。这里是默认路径,与我们不符,需要手动修改。因为idea默认web根目录名字为web,而不是eclipse的WebContent,像MyEclipse是WebRoot。


修改好后,看到右下角,有一个按钮,Create Artifact,这又是maven地概念了,这个Artifact指的就是将要投到tomcat里去跑的那个目录。只不过它不会真的拷贝到tomcat安装目录下。只是会让tomcat来寻找这个路径,运行目录下面的程序。

点击后,左侧跳到Artifact菜单,右侧展现配置表单。这个过程与你手动切换到Artifact菜单,然后点击+号选择打包类型的效果是一样的。刚才那个按钮是idea的智能化行为。
表单中,name,随便取,可以不用修改。Type,Web Application: Exploded是web开发的常用类型,无需修改。Output directory,输出路径就是这个Artifact的目录所在,idea会将所有需要的资源,包括class,lib,web目录等等以tomcat能够识别的目录结构拷贝到这里。而tomcat会直接使用这个目录运行系统。默认是与src平级的out目录下。一般也不用修改。
右侧的区域Available Elements中,最有可能出现的就是lib目录下的jar包,这里测试环境我忘了准备。但是一般情况下会列出一个jar包列表。这时候会有红线提示,要你把jar包加入左侧的输入目录结构中。那么右键菜单中选择Put into Output Root。这样错误提示会消失。但是!!我这里要提一下,这个步骤其实现在的场景下是无效的,或者说这个错误提示完全不用管它。因为你会发现你即使不把jar包导入左边,运行时,lib下的所有jar包依然会拷贝到artifact目录下。但这不是idea的bug,是有原因的,这里就不说了。

点击OK就可以了,这样项目配置就算完成了。然后是配置tomcat。
上部菜单栏会有一个小三角,下拉后选择Edit Configuration

弹出窗口后,按图示选择。

选择后,打开tomcat配置表单。
name,随便取,我一般取tomcat5/6/7。Application Server,选择一个以关联的tomcat版本,若没有关联过,点击右侧Configure做关联。After launch可以勾选,在start/restart tomcat后会自动打开浏览器并访问指定url。右下角有一个和session相关的勾选框,可以勾选,作用是restart以后,前面session中的值可以自动设置,就不用再次登陆啦~~

切换到第二个标签页deployment,点击右侧+号,选择第一项artifact,选择刚才创建的那个artifact,

右侧的Application context作用是你的url访问根路径,默认是/,就是说你的url中不包含我们以往所习惯的项目名,比如若是在web根下有index.jsp,那么路径就是http://127.0.0.1:8080/index.jps,而不是http://127.0.0.1:8080/web_test/index.jsp,当然,出于对传统的习惯,这一项你可以改成/web_test

点击OK,tomcat配置就结束了,然后就可以开始运行tomcat了。界面上部会有tomcat图标,右方有run, debug图标,点击即开始运行。浮出run/debug Panel。

如果log一切正常,就说明运行起来了。在前面的过程中,存在一些配置,对开发过程还是有帮助的,但是与运行起来不影响,就不一一举例说明了。
项目管理
2018-08-03 12:22:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
1.SQL 是一种声明式语言
2.SQL语句并不按照语法执行顺序执行
SQL语句的语法顺序是:
SELECT[DISTINCT]
FROM
WHERE
GROUP BY
HAVING
UNION
ORDER BY
但是其执行顺序是:
FROM
WHERE
GROUP BY
SELECT
DISTINCT
UNION
ORDER BY
(1)FROM 才是 SQL 语句执行的第一步,并非 SELECT 。数据库在执行 SQL 语句的第一步是将数据从硬盘加载到数据缓冲区中,以便对这些数据进行操作。(译者注:原文为“The first thing that happens is loading data from the disk into memory, in order to operate on such data.”,但是并非如此,以 Oracle 等常用数据库为例,数据是从硬盘中抽取到数据缓冲区中进行操作。)
(2) SELECT 是在大部分语句执行了之后才执行的,严格的说是在 FROM 和 GROUP BY 之后执行的。理解这一点是非常重要的,这就是你不能在 WHERE 中使用在 SELECT 中设定别名的字段作为判断条件的原因。
SELECT A.x + A.y AS z
FROM A
WHERE z = 10 -- z 在此处不可用,因为SELECT是最后执行的语句!
如果你想重用别名z,你有两个选择。要么就重新写一遍 z 所代表的表达式:
SELECT A.x + A.y AS z
FROM A
WHERE (A.x + A.y) = 10
…或者求助于衍生表、通用数据表达式或者视图,以避免别名重用。请看下文中的例子。
(3)无论在语法上还是在执行顺序上, UNION 总是排在在 ORDER BY 之前。很多人认为每个 UNION 段都能使用 ORDER BY 排序,但是根据 SQL 语言标准和各个数据库 SQL 的执行差异来看,这并不是真的。尽管某些数据库允许 SQL 语句对子查询(subqueries)或者派生表(derived tables)进行排序,但是这并不说明这个排序在 UNION 操作过后仍保持排序后的顺序。
注意:并非所有的数据库对 SQL 语句使用相同的解析方式。如 MySQL、PostgreSQL和 SQLite 中就不会按照上面第二点中所说的方式执行。
永远要记得: SQL 语句的语法顺序和其执行顺序并不一致
3.SQL语言的核心是对表的引用(table references)
由于 SQL 语句语法顺序和执行顺序的不同,我们会认为SELECT 中的字段信息是 SQL 语句的核心。其实真正的核心在于对表的引用。
根据 SQL 标准,FROM 语句被定义为:
::= FROM
[ {
}... ]
FROM 语句的“输出”是一张联合表,来自于所有引用的表在某一维度上的联合。我们慢慢来分析:
FROM a, b
上面这句 FROM 语句的输出是一张联合表,联合了表 a 和表 b 。如果 a 表有三个字段, b 表有 5 个字段,那么这个“输出表”就有 8 ( =5+3)个字段。
这个联合表里的数据是 a*b,即 a 和 b 的笛卡尔积。换句话说,也就是 a 表中的每一条数据都要跟 b 表中的每一条数据配对。如果 a 表有3 条数据, b 表有 5 条数据,那么联合表就会有 15 ( =5*3)条数据。
FROM 输出的结果被 WHERE 语句筛选后要经过 GROUP BY 语句处理,从而形成新的输出结果。我们后面还会再讨论这方面问题。
如果我们从集合论(关系代数)的角度来看,一张数据库的表就是一组数据元的关系,而每个 SQL 语句会改变一种或数种关系,从而产生出新的数据元的关系(即产生新的表)。
4.灵活引用表
灵活引用表能使 SQL 语句变得更强大。一个简单的例子就是 JOIN 的使用。严格的说 JOIN 语句并非是 SELECT 中的一部分,而是一种特殊的表引用语句。 SQL 语言标准中表的连接定义如下:
::=

|
|
就拿之前的例子来说:
FROM a, b
a 可能是如下表的连接:
a1 JOIN a2 ON a1.id = a2.id
将它放到之前的例子中就变成了:
FROM a1 JOIN a2 ON a1.id = a2.id, b
尽管将一个连接表用逗号跟另一张表联合在一起并不是常用作法,但是你的确可以这么做。结果就是,最终输出的表就有了 a1+a2+b 个字段了。
在 SQL 语句中派生表的应用甚至比表连接更加强大,下面我们就要讲到表连接。
思考问题时,要从表引用的角度出发,这样就很容易理解数据是怎样被 SQL 语句处理的,并且能够帮助你理解那些复杂的表引用是做什么的。
更重要的是,要理解 JOIN 是构建连接表的关键词,并不是 SELECT 语句的一部分。有一些数据库允许在 INSERT 、 UPDATE 、 DELETE 中使用 JOIN 。
5.SQL 语句中推荐使用表连接
FROM a, b
高级 SQL 程序员也许学会给你忠告:尽量不要使用逗号来代替 JOIN 进行表的连接,这样会提高你的 SQL 语句的可读性,并且可以避免一些错误。
利用逗号来简化 SQL 语句有时候会造成思维上的混乱,想一下下面的语句:
FROM a, b, c, d, e, f, g, h
WHERE a.a1 = b.bx
AND a.a2 = c.c1
AND d.d1 = b.bc
-- etc...
不难看出使用 JOIN 语句的好处在于: 安全。 JOIN 和要连接的表离得非常近,这样就能避免错误。 更多连接的方式,JOIN 语句能去区分出来外连接和内连接等。
我们学到了什么?
记着要尽量使用 JOIN 进行表的连接,永远不要在 FROM 后面使用逗号连接表。
6、 SQL 语句中不同的连接操作
SQL 语句中,表连接的方式从根本上分为五种: EQUI JOIN SEMI JOIN ANTI JOIN CROSS JOIN DIVISION
EQUI JOIN
这是一种最普通的 JOIN 操作,它包含两种连接方式: INNER JOIN(或者是 JOIN ) OUTER JOIN(包括: LEFT 、 RIGHT、 FULL OUTER JOIN)
-- This table reference contains authors and their books.
-- There is one record for each book and its author.
-- authors without books are NOT included
author JOIN book ON author.id = book.author_id
-- This table reference contains authors and their books
-- There is one record for each book and its author.
-- ... OR there is an "empty" record for authors without books
-- ("empty" meaning that all book columns are NULL)
author LEFT OUTER JOIN book ON author.id = book.author_id
SEMI JOIN
这种连接关系在 SQL 中有两种表现方式:使用 IN,或者使用 EXISTS。“ SEMI ”在拉丁文中是“半”的意思。这种连接方式是只连接目标表的一部分。这是什么意思呢?再想一下上面关于作者和书名的连接。我们想象一下这样的情况:我们不需要作者 / 书名这样的组合,只是需要那些在书名表中的书的作者信息。那我们就能这么写:
-- Using IN
FROM author
WHERE author.id IN (SELECT book.author_id FROM book)
-- Using EXISTS
FROM author
WHERE EXISTS (SELECT 1 FROM book WHERE book.author_id = author.id)
IN比 EXISTS 的可读性更好
EXISTS 比IN 的表达性更好(更适合复杂的语句)
二者之间性能没有差异(但对于某些数据库来说性能差异会非常大)
因为使用 INNER JOIN 也能得到书名表中书所对应的作者信息,所以很多初学者机会认为可以通过 DISTINCT 进行去重,然后将 SEMI JOIN 语句写成这样:
-- Find only those authors who also have books
SELECT DISTINCT first_name, last_name
FROM author
JOIN book ON author.id = book.author_id
这是一种很糟糕的写法,原因如下: SQL 语句性能低下:因为去重操作( DISTINCT )需要数据库重复从硬盘中读取数据到内存中。(译者注: DISTINCT 的确是一种很耗费资源的操作,但是每种数据库对于 DISTINCT 的操作方式可能不同)。 这么写并非完全正确:尽管也许现在这么写不会出现问题,但是随着 SQL 语句变得越来越复杂,你想要去重得到正确的结果就变得十分困难。
更多的关于滥用 DISTINCT 的危害可以参考这篇博文
更多的关于滥用 DISTINCT 的危害可以参考这篇博文
( http://blog.jooq.org/2013/07/30/10-common-mistakes-java-developers-make-when-writing-sql/ )。
ANTI JOIN
这种连接的关系跟 SEMI JOIN 刚好相反。在 IN 或者 EXISTS 前加一个 NOT 关键字就能使用这种连接。举个例子来说,我们列出书名表里没有书的作者:
-- Using IN
FROM author
WHERE author.id NOT IN (SELECT book.author_id FROM book)
-- Using EXISTS
FROM author
WHERE NOT EXISTS (SELECT 1 FROM book WHERE book.author_id = author.id)
关于性能、可读性、表达性等特性也完全可以参考 SEMI JOIN。
这篇博文介绍了在使用 NOT IN 时遇到 NULL 应该怎么办,因为有一点背离本篇主题,就不详细介绍,有兴趣的同学可以读一下
http://blog.jooq.org/2012/01/27/sql-incompatibilities-not-in-and-null-values/
CROSS JOIN
这个连接过程就是两个连接的表的乘积:即将第一张表的每一条数据分别对应第二张表的每条数据。我们之前见过,这就是逗号在 FROM 语句中的用法。在实际的应用中,很少有地方能用到 CROSS JOIN,但是一旦用上了,你就可以用这样的 SQL语句表达:
-- Combine every author with every book
author CROSS JOIN book

DIVISION
DIVISION 的确是一个怪胎。简而言之,如果 JOIN 是一个乘法运算,那么 DIVISION 就是 JOIN 的逆过程。DIVISION 的关系很难用 SQL 表达出来,介于这是一个新手指南,解释 DIVISION 已经超出了我们的目的。但是有兴趣的同学还是可以来看看这三篇文章
( http://blog.jooq.org/2012/03/30/advanced-sql-relational-division-in-jooq/ )
( http://en.wikipedia.org/wiki/Relational_algebra#Division )
( https://www.simple-talk.com/sql/t-sql-programming/divided-we-stand-the-sql-of-relational-division/ )。
7、 SQL 中如同变量的派生表
在这之前,我们学习到过 SQL 是一种声明性的语言,并且 SQL 语句中不能包含变量。但是你能写出类似于变量的语句,这些就叫做派生表:
说白了,所谓的派生表就是在括号之中的子查询:
FROM (SELECT * FROM author)
需要注意的是有些时候我们可以给派生表定义一个相关名(即我们所说的别名)。
FROM (SELECT * FROM author) a
派生表可以有效的避免由于 SQL 逻辑而产生的问题。举例来说:如果你想重用一个用 SELECT 和 WHERE 语句查询出的结果,这样写就可以(以 Oracle 为例)
SELECT first_name, last_name, age
FROM (
SELECT first_name, last_name, current_date - date_of_birth age
FROM author
)
-- If the age is greater than 10000 days
WHERE age > 10000
8、 SQL 语句中 GROUP BY 是对表的引用进行的操作
让我们再回想一下之前的 FROM 语句:
FROM a, b
现在,我们将 GROUP BY 应用到上面的语句中:
GROUP BY A.x, A.y, B.z
上面语句的结果就是产生出了一个包含三个字段的新的表的引用。我们来仔细理解一下这句话:当你应用 GROUP BY 的时候, SELECT 后没有使用聚合函数的列,都要出现在 GROUP BY 后面。(译者注:原文大意为“当你是用 GROUP BY 的时候,你能够对其进行下一级逻辑操作的列会减少,包括在 SELECT 中的列”)。 需要注意的是:其他字段能够使用聚合函数:
2
3
SELECT A.x, A.y, SUM(A.z)
FROM A
GROUP BY A.x, A.y
还有一点值得留意的是: MySQL 并不坚持这个标准,这的确是令人很困惑的地方。(译者注:这并不是说 MySQL 没有 GROUP BY 的功能)但是不要被 MySQL 所迷惑。 GROUP BY 改变了对表引用的方式。你可以像这样既在 SELECT 中引用某一字段,也在 GROUP BY 中对其进行分组。
GROUP BY,再次强调一次,是在表的引用上进行了操作,将其转换为一种新的引用方式。
9、 SQL 语句中的 SELECT 实质上是对关系的映射
我个人比较喜欢“映射”这个词,尤其是把它用在关系代数上。(译者注:原文用词为 projection ,该词有两层含义,第一种含义是预测、规划、设计,第二种意思是投射、映射,经过反复推敲,我觉得这里用映射能够更直观的表达出 SELECT 的作用)。一旦你建立起来了表的引用,经过修改、变形,你能够一步一步的将其映射到另一个模型中。 SELECT 语句就像一个“投影仪”,我们可以将其理解成一个将源表中的数据按照一定的逻辑转换成目标表数据的函数。
通过 SELECT语句,你能对每一个字段进行操作,通过复杂的表达式生成所需要的数据。
SELECT 语句有很多特殊的规则,至少你应该熟悉以下几条: 你仅能够使用那些能通过表引用而得来的字段; 如果你有 GROUP BY 语句,你只能够使用 GROUP BY 语句后面的字段或者聚合函数; 当你的语句中没有 GROUP BY 的时候,可以使用开窗函数代替聚合函数; 当你的语句中没有 GROUP BY 的时候,你不能同时使用聚合函数和其它函数; 有一些方法可以将普通函数封装在聚合函数中; ……
一些更复杂的规则多到足够写出另一篇文章了。比如:为何你不能在一个没有 GROUP BY 的 SELECT 语句中同时使用普通函数和聚合函数?(上面的第 4 条)
原因如下: 凭直觉,这种做法从逻辑上就讲不通。 如果直觉不能够说服你,那么语法肯定能。 SQL : 1999 标准引入了 GROUPING SETS,SQL: 2003 标准引入了 group sets : GROUP BY() 。无论什么时候,只要你的语句中出现了聚合函数,而且并没有明确的 GROUP BY 语句,这时一个不明确的、空的 GROUPING SET 就会被应用到这段 SQL 中。因此,原始的逻辑顺序的规则就被打破了,映射(即 SELECT )关系首先会影响到逻辑关系,其次就是语法关系。(译者注:这段话原文就比较艰涩,可以简单理解如下:在既有聚合函数又有普通函数的 SQL 语句中,如果没有 GROUP BY 进行分组,SQL 语句默认视整张表为一个分组,当聚合函数对某一字段进行聚合统计的时候,引用的表中的每一条 record 就失去了意义,全部的数据都聚合为一个统计值,你此时对每一条 record 使用其它函数是没有意义的)。
糊涂了?是的,我也是。我们再回过头来看点浅显的东西吧。
SELECT 语句可能是 SQL 语句中最难的部分了,尽管他看上去很简单。其他语句的作用其实就是对表的不同形式的引用。而 SELECT 语句则把这些引用整合在了一起,通过逻辑规则将源表映射到目标表,而且这个过程是可逆的,我们可以清楚的知道目标表的数据是怎么来的。
想要学习好 SQL 语言,就要在使用 SELECT 语句之前弄懂其他的语句,虽然 SELECT 是语法结构中的第一个关键词,但它应该是我们最后一个掌握的。
10、 SQL 语句中的几个简单的关键词: DISTINCT , UNION , ORDER BY 和 OFFSET
在学习完复杂的 SELECT 之后,我们再来看点简单的东西: 集合运算( DISTINCT 和 UNION ) 排序运算( ORDER BY,OFFSET…FETCH)
集合运算( set operation):
集合运算主要操作在于集合上,事实上指的就是对表的一种操作。从概念上来说,他们很好理解: DISTINCT 在映射之后对数据进行去重 UNION 将两个子查询拼接起来并去重 UNION ALL 将两个子查询拼接起来但不去重 EXCEPT 将第二个子查询中的结果从第一个子查询中去掉 INTERSECT 保留两个子查询中都有的结果并去重
排序运算( ordering operation):
排序运算跟逻辑关系无关。这是一个 SQL 特有的功能。排序运算不仅在 SQL 语句的最后,而且在 SQL 语句运行的过程中也是最后执行的。使用 ORDER BY 和 OFFSET…FETCH 是保证数据能够按照顺序排列的最有效的方式。其他所有的排序方式都有一定随机性,尽管它们得到的排序结果是可重现的。
OFFSET…SET是一个没有统一确定语法的语句,不同的数据库有不同的表达方式,如 MySQL 和 PostgreSQL 的 LIMIT…OFFSET、SQL Server 和 Sybase 的 TOP…START AT 等。具体关于 OFFSET..FETCH 的不同语法可以参考这篇文章
( http://www.jooq.org/doc/3.1/manual/sql-building/sql-statements/select-statement/limit-clause/ )。
让我们在工作中尽情的使用 SQL!
正如其他语言一样,想要学好 SQL 语言就要大量的练习。上面的 10 个简单的步骤能够帮助你对你每天所写的 SQL 语句有更好的理解。另一方面来讲,从平时常见的错误中也能积累到很多经验。下面的两篇文章就是介绍一些 JAVA 和其他开发者所犯的一些常见的 SQL 错误: 10 Common Mistakes Java Developers Make when Writing SQL 10 More Common Mistakes Java Developers Make when Writing SQL





项目管理
2018-08-03 11:07:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
Spring Cloud集成项目有很多,下面我们列举一下和Spring Cloud相关的优秀项目,我们的企业架构中用到了很多的优秀项目,说白了,也是站在巨人的肩膀上去整合的。在学习Spring Cloud之前大家必须了解一下相关项目,希望可以帮助到大家。
Spring Cloud Config
配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git以及Subversion。
Spring Cloud Bus
​事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与Spring Cloud Config联合实现热部署。
Eureka
云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。
Hystrix
熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。
Zuul
Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。
Archaius
配置管理API,包含一系列配置管理API,提供动态类型化属性、线程安全配置操作、轮询框架、回调机制等功能。
Consul
封装了Consul操作,consul是一个服务发现与配置工具,与Docker容器可以无缝集成。
Spring Cloud for Cloud Foundry
通过Oauth2协议绑定服务到CloudFoundry,CloudFoundry是VMware推出的开源PaaS云平台。
Spring Cloud Sleuth
日志收集工具包,封装了Dapper和log-based追踪以及Zipkin和HTrace操作,为SpringCloud应用实现了一种分布式追踪解决方案。
Spring Cloud Data Flow
大数据操作工具,作为Spring XD的替代产品,它是一个混合计算模型,结合了流数据与批量数据的处理方式。
Spring Cloud Security
基于spring security的安全工具包,为你的应用程序添加安全控制。
Spring Cloud Zookeeper
操作Zookeeper的工具包,用于使用zookeeper方式的服务发现和配置管理。
Spring Cloud Stream
数据流操作开发包,封装了与Redis,Rabbit、Kafka等发送接收消息。
Spring Cloud CLI
基于 Spring Boot CLI,可以让你以命令行方式快速建立云组件。
Ribbon
提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。
Turbine
Turbine是聚合服务器发送事件流数据的一个工具,用来监控集群下hystrix的metrics情况。
Feign
Feign是一种声明式、模板化的HTTP客户端。
Spring Cloud Task
提供云端计划任务管理、任务调度。
Spring Cloud Connectors
便于云端应用程序在各种PaaS平台连接到后端,如:数据库和消息代理服务。
Spring Cloud Cluster
提供Leadership选举,如:Zookeeper, Redis, Hazelcast, Consul等常见状态模式的抽象和实现。
Spring Cloud Starters
Spring Boot式的启动项目,为Spring Cloud提供开箱即用的依赖管理。
完整项目的源码来源 技术支持1791743380
项目管理
2018-08-03 09:34:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
集群容错
容错主要是指服务出现了非业务异常之后采取的一些弥补措施,注意我这里讲的是非业务异常,因为业务异常出现的绝大多数情况都是代码异常,所以及时采取了重试等逻辑还是会出现一样的业务异常(代码出问题了,锅当然要自己背喽)。
Dubbo中对于容错的处理主要集中在Cluster中,Cluster包装了底层调用的Invoker并且在Cluster自己本层做了一些出现异常之后的处理。
对于Dubbo的容错主要是有两层。第一层是mock,第二层是用户配置的容错策略。对于集群容错的包装逻辑入口就在于RegistryProtocol的doRefer()方法最后的cluster.join(directory),该方法返回了集群包装过后的invoker,这里的cluser其实就是MockClusterWrapper(至于为什么能确定是MockClusterInvoker,就需要大家去理解一下Dubbo的SPI机制了),下面一起来看一下MockClusterInvoke的具体内容: //真正起作用的是MockClusterInvoker public Invoker join(Directory directory) throws RpcException { //因为cluster下面之后一层包装,所以这里的this.cluser就是默认的FialoverCluster return new MockClusterInvoker(directory, this.cluster.join(directory)); } //MockClusterInvoker.invoke() public Result invoke(Invocation invocation) throws RpcException { Result result = null; //获取URL中配置的mock参数 String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim(); if (value.length() == 0 || value.equalsIgnoreCase("false")){ //如果没有配置mock的话就直接进行调用 result = this.invoker.invoke(invocation); //如果配置了强制的mock就直接调用mock,不走正常调用逻辑 } else if (value.startsWith("force")) { if (logger.isWarnEnabled()) { logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl()); } result = doMockInvoke(invocation, null); } else { //调用失败之后再进行mock操作 try { result = this.invoker.invoke(invocation); }catch (RpcException e) { //mock并不会处理义务异常 if (e.isBiz()) { throw e; } else { if (logger.isWarnEnabled()) { logger.info("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e); } result = doMockInvoke(invocation, e); } } } return result; } //主要是挑选出可用的MockInvoker类然后调用其invoke方法返回结果 private Result doMockInvoke(Invocation invocation,RpcException e){ Result result = null; Invoker minvoker ; //选取可用的MockInvoker List> mockInvokers = selectMockInvoker(invocation); if (mockInvokers == null || mockInvokers.size() == 0){ minvoker = (Invoker) new MockInvoker(directory.getUrl()); } else { minvoker = mockInvokers.get(0); } try { result = minvoker.invoke(invocation); } catch (RpcException me) { //如果是业务异常就封装结果(注意这里和上面的区别),因为biz异常是用户自己自己在mock信息中配置的异常,不是预想之外的异常 if (me.isBiz()) { result = new RpcResult(me.getCause()); } else { throw new RpcException(me.getCode(), getMockExceptionMessage(e, me), me.getCause()); } } catch (Throwable me) { throw new RpcException(getMockExceptionMessage(e, me), me.getCause()); } return result; } //核心最后还是调用了MockInvoker的invoker方法 public Result invoke(Invocation invocation) throws RpcException { String mock = getUrl().getParameter(invocation.getMethodName()+"."+Constants.MOCK_KEY); if (invocation instanceof RpcInvocation) { ((RpcInvocation) invocation).setInvoker(this); } if (StringUtils.isBlank(mock)){ mock = getUrl().getParameter(Constants.MOCK_KEY); } if (StringUtils.isBlank(mock)){ //这个错误比较常见,原因就在于客户端调用的时候返回的异常信息是非业务异常,但是客户端又没有配置mock信息,因此就会抛出这个异常 throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url)); } //解析出mock的类型,如果是mock=fail:AA,就返回AA,如果是mock=xx.Service就返回xx.Service,如果是mock=force:XX,就返回XX mock = normallizeMock(URL.decode(mock)); //如果配置的是:mock=fail:return,就直接返回空结果 if (Constants.RETURN_PREFIX.trim().equalsIgnoreCase(mock.trim())){ RpcResult result = new RpcResult(); result.setValue(null); return result; //如果配置的是mock=fail:return **,就解析**为对应的可返回内容然后返回 } else if (mock.startsWith(Constants.RETURN_PREFIX)) { mock = mock.substring(Constants.RETURN_PREFIX.length()).trim(); mock = mock.replace('`', '"'); try { Type[] returnTypes = RpcUtils.getReturnTypes(invocation); Object value = parseMockValue(mock, returnTypes); return new RpcResult(value); } catch (Exception ew) { throw new RpcException("mock return invoke error. method :" + invocation.getMethodName() + ", mock:" + mock + ", url: "+ url , ew); } //如果配置的是mock=fail:throw **(用户自定义的异常信息),就解析**为对应的可返回内容然后返回 } else if (mock.startsWith(Constants.THROW_PREFIX)) { mock = mock.substring(Constants.THROW_PREFIX.length()).trim(); mock = mock.replace('`', '"'); if (StringUtils.isBlank(mock)){ throw new RpcException(" mocked exception for Service degradation. "); } else { //用户自定义类 Throwable t = getThrowable(mock); throw new RpcException(RpcException.BIZ_EXCEPTION, t); } } else { //如果mock信息为ServiceMock的话就直接找到对应的Mock类进行mock调用,然后返回结果 try { Invoker invoker = getInvoker(mock); return invoker.invoke(invocation); } catch (Throwable t) { throw new RpcException("Failed to create mock implemention class " + mock , t); } } }
从上面的逻辑上来看mock主要是根据用户的一些配置,做一些非常具体的容错逻辑,精确到方法界别的,所以算是容错的最小粒度了。我们经常在使用中对一些不可靠服务进行mock处理,防止在其出现异常时候影响我们的核心调用流程。
方法级别的容错总体来说是针对业务异常的一种容错,而针对非业务异常的容错逻辑就是另外一个概念了,比如说由于网络抖动导致某次调用没有成功,针对类似的异常Dubbo也有自己的容错措施,具体如下面几种: Failover Cluster 失败自动切换,当出现失败,重试其它服务器。(缺省) 通常用于读操作,但重试会带来更长延迟。 Failfast Cluster 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 Failsafe Cluster 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 Failback Cluster 失败自动恢复,后台记录失败请求,定时重发。 通常用于消息通知操作。 Forking Cluster 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过forks="2"来设置最大并行数。 Broadcast Cluster 广播调用所有提供者,逐个调用,任意一台报错则报错。(2.1.0开始支持) 通常用于通知所有提供者更新缓存或日志等本地资源信息。
因为种类比较多,全部讲一遍太费时间了,所以选择比较常用的几个进行介绍。
Failover public Result doInvoke(Invocation invocation, final List> invokers, LoadBalance loadbalance) throws RpcException { List> copyinvokers = invokers; //检查copyinvokers是否为null checkInvokers(copyinvokers, invocation); //重试次数,默认为3次,不包含第一次调用 int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1; if (len <= 0) { len = 1; } // last exception. RpcException le = null; //已经调用的Invoker列表 List> invoked = new ArrayList>(copyinvokers.size()); Set providers = new HashSet(len); for (int i = 0; i < len; i++) { //重试时,进行重新选择,避免重试时invoker列表已发生变化. //注意:如果列表发生了变化,那么invoked判断会失效,因为invoker示例已经改变 if (i > 0) { checkWheatherDestoried(); copyinvokers = list(invocation); //重新检查一下有没有对应的提供者 checkInvokers(copyinvokers, invocation); } //通过loadbalance去选出目标Invoker //这里默认的LoadBalance是RandomLoadBalance,选择时候是根据权重来选择目标的Invoker,当然也可以配置其他的LoadBalance Invoker invoker = select(loadbalance, invocation, copyinvokers, invoked); invoked.add(invoker); //添加到上下文环境中去,但是这里为什么会把失败的invoker也加进来,感觉失败的Invoker信息并没有什么意义 RpcContext.getContext().setInvokers((List)invoked); try { //这里才是最后的调用,使用经过loadbalance选出的invoker去调用 Result result = invoker.invoke(invocation); if (le != null && logger.isWarnEnabled()) { logger.warn("Although retry the method " + invocation.getMethodName() + " in the service " + getInterface().getName() + " was successful by the provider " + invoker.getUrl().getAddress() + ", but there have been failed providers " + providers + " (" + providers.size() + "/" + copyinvokers.size() + ") from the registry " + directory.getUrl().getAddress() + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version " + Version.getVersion() + ". Last error is: " + le.getMessage(), le); } return result; } catch (RpcException e) { //业务异常不会重试,直接抛出 if (e.isBiz()) { throw e; } le = e; } catch (Throwable e) { le = new RpcException(e.getMessage(), e); } finally { providers.add(invoker.getUrl().getAddress()); } } throw new RpcException(le != null ? le.getCode() : 0, "Failed to invoke the method " + invocation.getMethodName() + " in the service " + getInterface().getName() + ". Tried " + len + " times of the providers " + providers + " (" + providers.size() + "/" + copyinvokers.size() + ") from the registry " + directory.getUrl().getAddress() + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version " + Version.getVersion() + ". Last error is: " + (le != null ? le.getMessage() : ""), le != null && le.getCause() != null ? le.getCause() : le); }
Failfast //快速失败的逻辑最简单了,就是什么都不做,有调用异常的话就直接往上抛出 public Result doInvoke(Invocation invocation, List> invokers, LoadBalance loadbalance) throws RpcException { checkInvokers(invokers, invocation); Invoker invoker = select(loadbalance, invocation, invokers, null); try { return invoker.invoke(invocation); //简单区分异常类型 } catch (Throwable e) { if (e instanceof RpcException && ((RpcException)e).isBiz()) { // biz exception. throw (RpcException) e; } throw new RpcException(e instanceof RpcException ? ((RpcException)e).getCode() : 0, "Failfast invoke providers " + invoker.getUrl() + " " + loadbalance.getClass().getSimpleName() + " select from all providers " + invokers + " for service " + getInterface().getName() + " method " + invocation.getMethodName() + " on consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", but no luck to perform the invocation. Last error is: " + e.getMessage(), e.getCause() != null ? e.getCause() : e); } }
ailover与failfast的代码基本一样,主要区别就是出现异常之后直接忽略,然后返回空的RpcResult。其余的几种集群策略在平时用的比较少,我就不多介绍了,其实现也都比较简单。
Dubbo的容错不仅体现在provider的cluster,对于注册中心也有提供cluster内容(AvailableCluster),只不过该内容比较简单,只是随机选取了一个可用的注册中心。
负载均衡
负载均衡的概念:从多个目标服务器中选择出其中一个服务器供客户端调用,这个选择的具体过程就叫做负载均衡(纯粹是自己给小白用户的解释)。
一般的服务性框架都会有负载均衡的内容,Dubbo也是基于自己的URL机制做了一层负载均衡,我们看到上面的集群内容时候就看到集群内部其实就依赖了负载均衡策略来从多个Invoker中选取其中的一个,只不过一次负载所选择到的Invoker并不一定能满足条件,比如在Failover策略下,失败之后Loadbalance重新选择的Invoker还是失败过的那个,那就要重新计算了。
Dubbo的负载均衡策略主要有以下几种: Random LoadBalance 随机选取服务提供者 最简单的无状态的负载均衡算法 RoundRobin LoadBalance 以轮训的方式调用服务提供者 缺点是有状态,必须在并发之下记住上一次到谁了 LeastActive LoadBalance 调用最少活跃调调用的服务提供者,这里有一点需要注意,这里的服务调用统计维度是在方法级别的,也就是说是方法级别的LoadBalance。 ConsistentHash LoadBalance 一致性Hash(用的不多,不多讲解)
首先看下LoadBalance的接口就直到它是做什么的: //从invokers列表中根据url和invocation信息选出一个合适的Invoker供consumer端调用 Invoker select(List> invokers, URL url, Invocation invocation) throws RpcException;
在所有的LoadBalance中都提到了一个概念:weight。正常情况下我们无论配置在provider还是service中,对应的所有服务端的providerUrl的weight都是一样的,这种情况其实weight配置不配置意义不大。但是一旦动态针对某个服务调整过weight值,这个影响就出来了。例如:override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&weight=200 配置之后,就将10.20.153.10服务器上的com.foo.BarServic的权值改为了200,那么它在之后的LoadBalance中被选取的可能性就更大了(默认的权值为100)。
Random LoadBalance private final Random random = new Random(); protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int totalWeight = 0; // 总权重 boolean sameWeight = true; // 权重是否都一样 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); totalWeight += weight; // 累计总权重 if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) { sameWeight = false; // 计算所有权重是否一样 } } if (totalWeight > 0 && ! sameWeight) { // 如果权重不相同且权重大于0则按总权重数随机 int offset = random.nextInt(totalWeight); // 随机数落到哪个片段上,就取哪个片段对应的Invoker对象(击鼓传花式往后切割) for (int i = 0; i < length; i++) { offset -= getWeight(invokers.get(i), invocation); if (offset < 0) { return invokers.get(i); } } } // 如果权重相同或权重为0则均等随机 return invokers.get(random.nextInt(length)); }
RoundRobin LoadBalance //轮训是针对方法级别的,并不是所有服务调用 protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName(); int length = invokers.size(); // 总个数 int maxWeight = 0; // 最大权重 int minWeight = Integer.MAX_VALUE; // 最小权重 // invoker->weight,IntegerWrapper就是一个简单的Integer包装类 final LinkedHashMap, IntegerWrapper> invokerToWeightMap = new LinkedHashMap, IntegerWrapper>(); int weightSum = 0; for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); maxWeight = Math.max(maxWeight, weight); // 累计最大权重 minWeight = Math.min(minWeight, weight); // 累计最小权重 if (weight > 0) { invokerToWeightMap.put(invokers.get(i), new IntegerWrapper(weight)); weightSum += weight; } } AtomicPositiveInteger sequence = sequences.get(key); if (sequence == null) { sequences.putIfAbsent(key, new AtomicPositiveInteger()); sequence = sequences.get(key); } //currentSequence代表某个方法是第多少次被调用的,例如第1W次 int currentSequence = sequence.getAndIncrement(); if (maxWeight > 0 && minWeight < maxWeight) { // 权重不一样 // 可以把weightSum理解成一个权重范围内的总集,mod就带表在这个总集中具体执行到的位置 int mod = currentSequence % weightSum; //weightSum < maxWeight*length for (int i = 0; i < maxWeight; i++) { for (Map.Entry, IntegerWrapper> each : invokerToWeightMap.entrySet()) { final Invoker k = each.getKey(); final IntegerWrapper v = each.getValue(); //这里的逻辑比较抽象,本质上就是谁的权重越大,轮询到谁的次数就越多 if (mod == 0 && v.getValue() > 0) { return k; } if (v.getValue() > 0) { v.decrement(); mod--; } } } } // 取模轮循 return invokers.get(currentSequence % length); }
简单题下RoundRobin的弊端:如果某个服务有3台服务器,权重依次是10,1000,100,可能配置的人的本意是在轮训的时候走到三台机器的比例是:1:100:10,但是实际上确实是1000个请求压倒了第二台机器上。。。
LeastActive LoadBalance private final Random random = new Random(); protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int leastActive = -1; // 最小的活跃数 int leastCount = 0; // 相同最小活跃数的个数 int[] leastIndexs = new int[length]; // 相同最小活跃数的下标 int totalWeight = 0; // 总权重 int firstWeight = 0; // 第一个权重,用于于计算是否相同 boolean sameWeight = true; // 是否所有权重相同 for (int i = 0; i < length; i++) { Invoker invoker = invokers.get(i); int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // 活跃数 int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 权重 if (leastActive == -1 || active < leastActive) { // 如果是初始情况下或者某台机器的active数量小于现在保存的leastActive数量,就会重新开始 leastActive = active; // 记录最小活跃数 leastCount = 1; // 重新统计相同最小活跃数的个数 leastIndexs[0] = i; // 重新记录最小活跃数下标 totalWeight = weight; // 重新累计总权重 firstWeight = weight; // 记录第一个权重 sameWeight = true; // 还原权重相同标识 } else if (active == leastActive) { // 累计相同最小的活跃数 leastIndexs[leastCount ++] = i; // 累计相同最小活跃数下标 totalWeight += weight; // 累计总权重 // 判断所有权重是否一样 if (sameWeight && i > 0 && weight != firstWeight) { sameWeight = false; } } } // 如果只有一个最小值的话就直接调用 if (leastCount == 1) { // 如果只有一个最小则直接返回 return invokers.get(leastIndexs[0]); } if (! sameWeight && totalWeight > 0) { // 如果权重不相同且权重大于0则按总权重数随机 int offsetWeight = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < leastCount; i++) { int leastIndex = leastIndexs[i]; offsetWeight -= getWeight(invokers.get(leastIndex), invocation); if (offsetWeight <= 0) return invokers.get(leastIndex); } } // 如果权重相同或权重为0则均等随机 return invokers.get(leastIndexs[random.nextInt(leastCount)]);
一致性哈希对应的LoadBalance本次不讲解。
我个人对于Dubbo 提供的LoadBalance的看法是:基本上满足日常使用,但是应该更加丰富。因为我们日常使用的是leastactive,但是因为该负载均衡策略是基于方法级别的,所以无法控制其他的方法对于应用的影响,这里如果将统计的维度上升到机器纬度,其实可以做到类似于整个集群的leastactive,这样的话就不容易出现部分几台机器负载特别高,而其余的大部分机器都有很多资源结余。当然也可以提供接口纬度的负载均衡,这个完全可以根据具体的业务实现定值,因为SPI机制的缘故,自定义负载均衡策略实现起来还是比较方便的。
路由
说到路由还是贴一张官方的图会比较好理解一些
从上图中能看出来router的作用其实是在LB之前的,也就是说LB的入参其实就是router的结果。
因为router可能理解起来并不直观,因此还是大致介绍一下router的含义。
在日常的服务这里过程中,比如我想给某个接口开个特权,专门设置一些提供者只供其调用;读写分离,读操作配置专门的机器,写操作配置专门的机器;某个消费者有问题,想及时下掉等等。都是可以通过router来实现的,明白了router的具体含义之后我们来一起看一下router的实现:
首先为了明确路由的基本规则,把官方的案例拿过来一起看一下:
服务调用信息,如:method, argument 等 (暂不支持参数路由) URL本身的字段,如:protocol, host, port 等 以及URL上的所有参数,如:application, organization 等
条件支持:
等号"="表示"匹配",如:host = 10.20.153.10 不等号"!="表示"不匹配",如:host != 10.20.153.10
值支持:
以逗号","分隔多个值,如:host != 10.20.153.10,10.20.153.11 以星号" "结尾,表示通配,如:host != 10.20. 以美元符"$"开头,表示引用消费者参数,如:host = $host
下面以ConditionRouter为例来看一下路由规则的具体实现 //构造函数初始化的时候难点在于when和then的初始化: public ConditionRouter(URL url) { this.url = url; //路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为0。 this.priority = url.getParameter(Constants.PRIORITY_KEY, 0); //当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为flase this.force = url.getParameter(Constants.FORCE_KEY, false); try { String rule = url.getParameterAndDecoded(Constants.RULE_KEY); if (rule == null || rule.trim().length() == 0) { throw new IllegalArgumentException("Illegal route rule!"); } rule = rule.replace("consumer.", "").replace("provider.", ""); int i = rule.indexOf("=>"); // =>前的部分 String whenRule = i < 0 ? null : rule.substring(0, i).trim(); // =>后的部分 String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim(); Map when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap() : parseRule(whenRule); Map then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ? null : parseRule(thenRule); // NOTE: When条件是允许为空的,外部业务来保证类似的约束条件 this.whenCondition = when; this.thenCondition = then; } catch (ParseException e) { throw new IllegalStateException(e.getMessage(), e); } } /** * 将对应的rule信息解析为对应的MatchPair * host=10.20.153.10解析出来就是一个host:MatchPair,Matcher的matches内容为10.20.153.10 * host!=10.20.153.10解析出来就是一个host:MatchPair,Matcher的mismatches内容为10.20.153.10 * 可以理解为MatcherPair就是区分matches和mismatches的具体聚合类,拿到这个Matcher就拿到表达式初步解析后的数据 * @param rule * @return * @throws ParseException */ private static Map parseRule(String rule) throws ParseException { Map condition = new HashMap(); if(StringUtils.isBlank(rule)) { return condition; } // 匹配或不匹配Key-Value对 MatchPair pair = null; // 多个Value值 Set values = null; final Matcher matcher = ROUTE_PATTERN.matcher(rule); //例如:host=10.20.153.10 第一次匹配的group1='',group2='host',第二次匹配的group1='=',group2='10.20.153.10' while (matcher.find()) { // 逐个匹配 String separator = matcher.group(1); String content = matcher.group(2); // 表达式开始 if (separator == null || separator.length() == 0) { pair = new MatchPair(); //'host':new MatchPair() condition.put(content, pair); } // &符号还没有遇到过 else if ("&".equals(separator)) { if (condition.get(content) == null) { pair = new MatchPair(); condition.put(content, pair); } else { condition.put(content, pair); } } // 匹配=号部分 else if ("=".equals(separator)) { if (pair == null) throw new RuntimeException(); values = pair.matches; values.add(content); } // 匹配!=号部分 else if ("!=".equals(separator)) { if (pair == null) throw new RuntimeException(); values = pair.mismatches; values.add(content); } // ,号直接跟着前面的=或者!=走 else if (",".equals(separator)) { if (values == null || values.size() == 0) throw new RuntimeException(); values.add(content); } else { throw new RuntimeException(); } } return condition; } public List> route(List> invokers, URL url, Invocation invocation) throws RpcException { if (invokers == null || invokers.size() == 0) { return invokers; } try { //如果没有匹配到的前置条件后直接返回,意思就是当前作用的consumer信息不需要经过路由操作 // 如果路由的配置值有$开头的话就将其替换为URL中对应的key的value if (! matchWhen(url)) { return invokers; } List> result = new ArrayList>(); //黑名单 if (thenCondition == null) { logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey()); return result; } for (Invoker invoker : invokers) { //逐个匹配后置条件 if (matchThen(invoker.getUrl(), url)) { result.add(invoker); } } if (result.size() > 0) { return result; //感觉强制执行的话返回一个空的List并没有卵用呀 } else if (force) { logger.warn("The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(Constants.RULE_KEY)); return result; } } catch (Throwable t) { logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t); } return invokers; }
想了解更多详细资料:1903832579
项目管理
2018-08-03 09:22:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
Dubbo的启动主要是发布服务的过程,起到核心作用的就是ServiceConfig(ServiceConfig就是我们在Dubbo的配置文件中配置的dubbo:service这些配置项对应的实体类)。服务的启动初始位置也基本是在这里,下面我们来看看具体的实现内容。
讲基本内容前首先理清楚几个名词概念:
Invoker:Invoker的概念我们在动态代理的时候就接触过,中文的意思大概是执行者,这里其实可以理解为具体方法的执行者。其核心内容大致如下: Class getInterface(); Result invoke(Invocation invocation) throws RpcException; URL getUrl();

通过以上的三个方法们就可以执行到具体的方法并且获得方法的执行结果。通过getUrl获得需要执行的方法具体实现细节,主要是获得具体的ref;其次就是组装方法的参数信息等等,这些信息在invocation里面都有封装;最后通过执行invoke方法触发具体的方法并返回结果。从这里可以看出Invoker是具体方法执行的最后一个守关者,获得了Invoker,就获得了具体接口的代码,然后执行代理就可以。
Invoker仅仅是作为一个代理的门面,其不仅可以代表本地执行Duubo调用的代理,还可以充当RPC时候的代理,更是可以将自己包装成一个多个Invoker聚合而成的代理(主要是处理集群的一些策略,包括负载均衡和路由等)。
Exporter:服务暴露的过程中会将Invoker转换成Exporter(暴露者),及Exporter其实包含了Invoker,主要是用于不同层面的服务发布。
其实Dubbo 还有一些比较重要的对象,像Protocol,Exchanger等等。我认为在这里直接说明不太合适,所以等到我们用到之后再开始说明。
1. 核心的属性信息
一些基本的属性:group,version,interfaceName,interfaceClass,timeout等等。我们凡是可以在dubbo:service上配置的属性都在ServiceConfig中可以找得到对应的属性;
//dubbo对应的服务发布协议,这里可以清楚地看到Dubbo在这里使用的自己的spi机制,来保证灵活性。(至于SPI机制的具体实现,之后有机会的话会讲到,简单理解就是通过getExtensionLoader获得对应类的扩展类实现类)
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
private final List urls = new ArrayList();
private final List> exporters = new ArrayList>();
2.服务暴露过程
对于服务暴露来说,在ServiceConfig里面的初始方法就是export()方法了,下面我们从export方法开始来看看: public synchronized void export() { if (provider != null) { //默认取provider的配置 if (export == null) { export = provider.getExport(); } if (delay == null) { delay = provider.getDelay(); } } //如果export设置为false的话就直接返回 if (export != null && ! export.booleanValue()) { return; } //如果设置延迟时间的话就延迟指定时间然后进行暴露 if (delay != null && delay > 0) { Thread thread = new Thread(new Runnable() { public void run() { try { Thread.sleep(delay); } catch (Throwable e) { } doExport(); } }); thread.setDaemon(true);//将暴露接口的线程设置为守护线程 thread.setName("DelayExportServiceThread"); thread.start(); } else { doExport(); //一切暴露的核心还都是要看doExport方法。 } } protected synchronized void doExport() { // 防止服务多次暴露 // 设置默认的基本属性 // 针对泛化接口做单独处理 if (ref instanceof GenericService) { interfaceClass = GenericService.class; if (StringUtils.isEmpty(generic)) { generic = Boolean.TRUE.toString(); } } else { try {//通过反射初始化接口(interfaceName是实现类的全称) interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() .getContextClassLoader()); } catch (ClassNotFoundException e) { throw new IllegalStateException(e.getMessage(), e); } //检查定义的方法是否为接口中的方法 checkInterfaceAndMethods(interfaceClass, methods); //检查引用不为空,并且引用必需实现接口 checkRef(); //如果到这一步的话说明类实现是自己定义的,所以设置generic为false generic = Boolean.FALSE.toString(); } // 处理Local和Stub代理处理 // 检查Application,Registry,Protocol的配置情况 //将配置的属性绑定到当前对象 appendProperties(this); //针对Local,Stub和Mock进行校验 //上面的操作主要是做一些检验和初始化的操作,没有涉及到具体的暴露服务逻辑 doExportUrls(); } private void doExportUrls() { //取到注册中心的URL List registryURLs = loadRegistries(true); for (ProtocolConfig protocolConfig : protocols) { //根据配置的通信协议将服务暴露到注册中心 doExportUrlsFor1Protocol(protocolConfig, registryURLs); } } private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs) { // 默认采用Dubbo协议 //之后的部分逻辑就是想尽一切办法取到host //取到端口号(之后的部分关于端口的逻辑就是想尽一切办法取端口号) // 这个map十分重要,它的意义在于所有存储所有最终使用到的属性,我们知道一个属性例如timeout,可能在Application,provider,service中都有配置,具体以哪个为准,都是这个map处理的事情。 Map map = new HashMap(); if (anyhost) { //如果此时anyhost为true的话 map.put(Constants.ANYHOST_KEY, "true"); } // 存储简单的服务信息 //将application,module,provider,protocol和service的信息设置到map里面 //将应用配置的树勇按照层级存入map中。注意这里的层级关系,是一层层覆盖的 即关系为:ServiceConfig->PrtocolConfig->ProviderConfig->ModuleConfig->ApplicaionConfig //单独处理好method层级的参数关系 //判断有没有配置通配协议 if (ProtocolUtils.isGeneric(generic)) { map.put("generic", generic); map.put("methods", Constants.ANY_VALUE); } else { String revision = Version.getVersion(interfaceClass, version); if (revision != null && revision.length() > 0) { map.put("revision", revision); } //通过包装类将interfaceClass进行包装然后取得方法名字,对于wapper包装器就是将不同的类统一化 //参考http://blog.csdn.net/quhongwei_zhanqiu/article/details/41597261理解Wapper String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); if(methods.length == 0) { logger.warn("NO method found in service interface " + interfaceClass.getName()); map.put("methods", Constants.ANY_VALUE); } else { //将所有的方法拼接成以逗号为分隔符的字符串 map.put("methods", StringUtils.join(new HashSet(Arrays.asList(methods)), ",")); } } //处理token属性 //如果配置的injvm的话就代表本地调用(本地调用还用Dubbo的话实在有点蛋疼) //所有的核心属性最后都成了URL的拼接属性,如果我们还记得map里面拼装了多少属性的话就知道这个URL内容有多丰富 URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map); // 下面是核心暴露过程,将不会省略源码 String scope = url.getParameter(Constants.SCOPE_KEY); //配置为none不暴露 if (! Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) { //配置不是remote的情况下做本地暴露 (配置为remote,则表示只暴露远程服务) if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) { exportLocal(url); } //如果配置不是local则暴露为远程服务.(配置为local,则表示只暴露远程服务) if (! Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope) ){ if (logger.isInfoEnabled()) { logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url); } if (registryURLs != null && registryURLs.size() > 0 && url.getParameter("register", true)) { for (URL registryURL : registryURLs) { //dynamic表示是否需要人工管理服务的上线下线(动态管理模式) url = url.addParameterIfAbsent("dynamic", registryURL.getParameter("dynamic")); URL monitorUrl = loadMonitor(registryURL); if (monitorUrl != null) { url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString()); } if (logger.isInfoEnabled()) { logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL); } ==================================================== //取到invoker对象(ref为接口实现类的引用) Invoker invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())); //将invoker转化为exporter对象 Exporter exporter = protocol.export(invoker); exporters.add(exporter); //将exporter添加到需要暴露的列表中取 } ================================================================ } else { //如果找不到注册中心的话就自己充当自己的注册中心吧 Invoker invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url); Exporter exporter = protocol.export(invoker); exporters.add(exporter); } } } //多个协议就有多个url与其对应,所以要一一存储。 this.urls.add(url); } 通过ServicConfig中的内容分解,我们看出来里面主要做的内容如下: 检验所需参数的合法性 将多层的参数(可能重复配置)最终整理出最终的结果(map),然后根据参数拼接成暴露服务需用到的url。 处理generic,Stub,injvm等其他需要支持的内容,补充dubbo的功能多样性,但是都不涉及核心流程。 根据对应的协议将服务进行暴露(将提供的服务推送到注册中心供服务调用者发现),默认使用Dubbo协议。
项目管理
2018-08-03 09:18:00