0%

简介

  • 是一个开源的分布式的关系型数据库的中间件
  • 已于2020年4月16日成为 Apache 软件基金会的顶级项目
  • 客户端代理模式
  • 定位为轻量级的Java框架,以 jar 包提供服务
  • 可以理解为增强版的 jdbc 驱动
  • 完全兼容各种 ORM 框架
阅读全文 »

1 Keepalived 简介

Keepalived 软件起初是专为 LVS 负载均衡软件设计的,用来管理并监控 LVS 集群系统中各个服务节点的状态,后来又加入了可以实现高可用的 VRRP 功能。因此,Keepalived除了能够管理 LVS 软件外,还可以作为其他服务(例如:Nginx、Haproxy、MySQL等)的高可用解决方案软件。

Keepalived 软件主要通过 VRRP 协议实现高可用功能的,VRRP 是 Virtual Router Redundancy Protocol (虚拟路由器冗余协议)的缩写,VRRP 出现的目的就是为了解决动态路由单点故障问题的,它能够保证当个别节点宕机时,整个网络可以不间断的运行。所以,Keepalived 一方面具有配置管理 LVS 的功能,同时还具有对LVS 下面节点进行健康检查的功能,另一方面也可以实现系统网络服务的高可用功能。

Keepalived 软件的官方站点: http://www.keepalived.org

2 Keepalived 服务的三个重要功能

2.1 管理 LVS 负载均衡软件

早期的 LVS 软件,需要通过命令行或脚本实现管理,并且没有针对 LVS 节点的健康检查功能。为了解决 LVS 的这些使用不便的问题,Keepalived就诞生了,可以说,Keepalived软件起初是专为了解决 LVS 的问题而诞生的。因此,Keepalived和LVS的感情很深,它们的关系如同夫妻一样,可以紧密的结合,愉快的工作。Keepalived 可以通过读取自身的配置文件实现通过更底层的接口直接管理 LVS 的配置以及控制服务的启动、停止等功能,这使得 LVS 的应用就更加简单方便了。

2.2 实现对 LVS 集群节点健康检查功能(healthcheck)

Keepalived 可以通过在自身的keepalived.conf文件里配置 LVS 的节点 IP 和相关参数实现对 LVS 的直接管理;除此之外,当 LVS 集群中的某一个甚至是几个节点服务器同时发生故障无法提供服务时,Keepalived 服务会自动将失效的节点服务器从 LVS 的正常转发队列中清楚出去,并转换到别的正常节点服务器上,从而保证最终用户的访问不受影响;当故障的节点服务器被修复后,Keepalived 服务又会自动地把它们加入到正常转发队列中,对客户提供服务。

2.3作为系统网络服务的高可用功能(failover)

Keepalived 可以实现任意两台主机之间,例如 Master 和 Backup 主机之间的故障转移和自动切换,这个主机可以是普通的不能停机的业务服务器,也可以是 LVS 负载均衡、Nginx 反向代理这样的服务器。

Keepalived 高可用功能实现的原理为:两台主机同时安装好 keepalived 软件并启动服务,开始正常工作时,由角色为 Master 的主机获得所有资源并对用户提供服务,角色 Backup 的主机作为 Master 主机的热备;当角色为 Master 的主机失效或出现故障时,角色为 Backup 的主机将自动接管 Master 主机的所有工作,包括接管 VIP 资源及相应资源服务;而当角色为 Master 的主机故障修复后,又会自动接管回它原来处理的工作,角色为 Backup 的主机则同时释放 Master 主机失效它接管的工作,此时,两台主机将恢复到最初启动时各自的原始角色及工作状态。

3 运行原理

Keepalived 高可用服务对之间的故障切换转移,是通过 VRRP 协议(虚拟路由冗余协议)来实现的。

在 Keepalived 服务正常工作时,主 Master 节点会不断地向备节点发送(多播的方式)心跳消息,用以告诉备 Backup 节点自己还活着,当主 Master 节点发生故障时,就无法发送心跳消息了,备节点也就因此无法继续检测到来自Master 节点的心跳了,进而调用自身的接管程序,接管主 Master 节点的 IP 资源及服务。而当主 Master 节点恢复时,备 Backup 节点又会释放主节点故障时自身接管的 IP 资源及服务,恢复到原来备用角色。

4 选举策略

选举策略是根据 VRRP 协议,完全按照权重大小,权重最大(0~255)的是 MASTER 机器,下面几种情况会触发选举

  1. keepalived 启动的时候
  2. master 服务器出现故障(断网,重启,或者本机器上的 keepalived crash 等,而本机器上其他应用程序 crash 不算)
  3. 有新的备份服务器加入且权重最大

5 VRRP协议

VRRP 协议,全称 Virtual Router Redundancy Protocol,中文名为虚拟路由冗余协议,VRRP 的出现就是为了解决静态路由的单点故障问题,VRRP 协议是通过一种竞选机制来将路由的任务交给某台 VRRP 路由器的。VRRP 协议早期是用来解决交换机、路由器等设备单点故障的。

5.1 VRRP 原理描述(同样适用于 Keepalived 的工作原理)

在一组 VRRP 路由器集群中,有多台物理 VRRP 路由器,但是这多台物理的机器并不是同时工作的,而是由一台称为 MASTER 的机器负责路由工作,其他的机器都是 BACKUP。MASTER 角色并非一成不变,VRRP 协议会让每个 VRRP 路由参与竞选,最终获胜的就是 MASTER。MASTER 拥有虚拟路由器的 IP 地址,我们把这个 IP 地址称为 VIP,MASTER 负责转发发送给网关地址的数据包和响应 ARP 请求。

5.2 VRRP 是如何工作的?

VRRP 协议通过竞选机制来实现虚拟路由器的功能,所有的协议报文都是通过 IP 多播(默认的多播地址:224.0.0.18)形式进行发送。虚拟路由器由 VRID (范围0-255)和一组 IP 地址组成,对外表现为一个周知的 MAC 地址:00-00-5E-00-01-{VRID}。所以,在一个虚拟路由器中,不管谁是 MASTER,对外都是相同的 MAC 地址和 IP 地址,如果其中一台虚拟路由器宕机,角色发生切换,那么客户端并不需要因为 MASTER 的变化修改自己的路由设置,可以做到透明的切换。这样就实现了如果一台机器宕机,那么备用的机器会拥有 MASTER 上的 IP 地址,实现高可用功能。

5.3 VRRP 是如何通信的?

在一组虚拟路由器中,只有作为 MASTER 的 VRRP 路由器会一直发送 VRRP 广播包,此时 BACKUP 不会抢占 MASTER 。当 MASTER 不可用时,这个时候 BACKUP 就收不到来自 MASTER 的广播包了,此时多台 BACKUP 中优先级最高的路由器会去抢占为 MASTER。这种抢占是非常快速的(可能只有1秒甚至更少),以保证服务的连续性。出于安全性考虑,VRRP 数据包使用了加密协议进行了加密。

6 Keepalived 高可用服务脑裂问题

6.1 什么是脑裂?

由于某些原因,导致两台高可用服务器在指定时间内,无法检测到对方的心跳消息,各自取得资源及服务的所有权,而此时的两台高可用服务器都还活着并在正常运行,这样就会导致同一个 IP 或服务在两端同时存在发生冲突,最严重的是两台主机占用同一个 VIP 地址,当用户写入数据时可能会分别写入到两端,这可能会导致服务器两端的数据不一致或造成数据丢失,这种情况就被称为脑裂。

6.2 导致脑裂发生的原因

一般来说,脑裂的发生,有以下几种原因:

1)高可用服务器之间心跳线链路故障,导致无法正常通信。

心跳线坏了(包括断了,老化)
网卡及相关驱动坏了,IP 配置及冲突问题(网卡直连)
心跳线连接的设备故障(网卡及交换机)
2)高可用服务器上开启了 iptables 防火墙阻挡了心跳消息传输。

3)高可用服务器上心跳网卡地址等信息配置不正确,导致发送心跳失败。

4)其他服务配置不当等原因,如心跳方式不同,心跳广播冲突、软件 BUG等。

注意:Keepalived 配置里同一 VRRP 实例如果 virtual_router_id 参数两端配置不一致,也会导致脑裂问题发生。

6.3 解决脑裂的具体方案

在实际生产环境中,可以从以下几个方面来防止脑裂问题的发生

1)同时使用串行电缆和以太网电缆连接,同时用两条心跳线路,这样一条线路坏了,另一个还是好的,依然能够传送心跳消息

2)当检测到脑裂时强行关闭一个心跳节点(这个功能需要特殊设备支持,如Stonith、fence)。相当于备节点接收不到心跳消息,发送关机命令通过单独的线路关闭主节点的电源。

3)做好对脑裂的监控报警(如邮件及手机短信等或值班),在问题发生时人为第一时间介入仲裁,降低损失。例如,百度的监控报警短信就有上行和下行的区别。报警信息报到管理员手机上,管理员可以通过手机回复对应数字或简单的字符串操作返回给服务器,让服务器根据指令自动处理相应故障,这样解决故障的时间更短。

4)如果开启防火墙,一定要让心跳消息通过,一般通过允许 IP 段的形式。

7 KeepAlived 配置详解

Keepalived的所有配置都在一个配置文件里面,主要分为三类:

  • 全局配置
  • VRRPD配置
  • LVS 配置

7.1 全局配置

全局配置是对整个 Keepalived 生效的配置,一个典型的配置如下:

1
2
3
4
5
6
7
8
9
10
11
global_defs {
notification_email { #设置 keepalived 在发生事件(比如切换)的时候,需要发送到的email地址,可以设置多个,每行一个。
acassen@firewall.loc
failover@firewall.loc
sysadmin@firewall.loc
}
notification_email_from Alexandre.Cassen@firewall.loc #设置通知邮件发送来自于哪里,如果本地开启了sendmail的话,可以使用上面的默认值。
smtp_server 192.168.200.1 #指定发送邮件的smtp服务器。
smtp_connect_timeout 30 #设置smtp连接超时时间,单位为秒。
router_id LVS_DEVEL #是运行keepalived的一个表示,多个集群设置不同。
}

7.2 VRRPD配置

VRRPD 的配置是 Keepalived 比较重要的配置,主要分为两个部分 VRRP 同步组和 VRRP实例,也就是想要使用 VRRP 进行高可用选举,那么就一定需要配置一个VRRP实例,在实例中来定义 VIP、服务器角色等。

7.2.1 VRRP Sync Groups**

不使用Sync Group的话,如果机器(或者说router)有两个网段,一个内网一个外网,每个网段开启一个VRRP实例,假设VRRP配置为检查内网,那么当外网出现问题时,VRRPD认为自己仍然健康,那么不会发生Master和Backup的切换,从而导致了问题。Sync group就是为了解决这个问题,可以把两个实例都放进一个Sync Group,这样的话,group里面任何一个实例出现问题都会发生切换。

1
2
3
4
5
6
7
8
9
10
11
12
vrrp_sync_group VG_1{ #监控多个网段的实例
group {
    VI_1 #实例名
    VI_2
    ......
}
notify_master /path/xx.sh     #指定当切换到master时,执行的脚本
netify_backup /path/xx.sh     #指定当切换到backup时,执行的脚本
notify_fault "path/xx.sh VG_1" #故障时执行的脚本
notify /path/xx.sh
smtp_alert   #使用global_defs中提供的邮件地址和smtp服务器发送邮件通知
}

7.2.2 VRRP实例(instance)配置

VRRP实例就表示在上面开启了VRRP协议,这个实例说明了VRRP的一些特征,比如主从,VRID等,可以在每个interface上开启一个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vrrp_instance VI_1 {
state MASTER #指定实例初始状态,实际的MASTER和BACKUP是选举决定的。
interface eth0 #指定实例绑定的网卡
virtual_router_id 51 #设置VRID标记,多个集群不能重复(0..255)
priority 100 #设置优先级,优先级高的会被竞选为Master,Master要高于BACKUP至少50
advert_int 1 #检查的时间间隔,默认1s
nopreempt #设置为不抢占,说明:这个配置只能在BACKUP主机上面设置
preempt_delay #抢占延迟,默认5分钟
debug #debug级别
authentication { #设置认证
auth_type PASS #认证方式,支持PASS和AH,官方建议使用PASS
auth_pass 1111 #认证的密码
}
virtual_ipaddress { #设置VIP,可以设置多个,用于切换时的地址绑定。格式:#<IPADDR>/<MASK> brd <IPADDR> dev <STRING> scope <SCOPT> label <LABE
192.168.200.16/24 dev eth0 label eth0:1
192.168.200.17/24 dev eth1 label eth1:1
192.168.200.18
}
}

7.2.3 VRRP 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# VRRP 脚本 
# 如下所示为相关配置示例
vrrp_script check_running {
script "/usr/local/bin/check_running"
interval 10
weight 10
}

vrrp_instance http {
state BACKUP
smtp_alert
interface eth0
virtual_router_id 101
priority 90
advert_int 3
authentication {
auth_type PASS
auth_pass whatever
}
virtual_ipaddress {
1.1.1.1
}
track_script {
check_running
}
}
# 首先在 vrrp_script 区域定义脚本名字和脚本执行的间隔和脚本执行的优先级变更, 如下所示:
vrrp_script check_running {
script "/usr/local/bin/check_running"
interval 10 # 脚本执行间隔
weight 10 # 脚本结果导致的优先级变更 10 表示优先级 + 10-10 则表示优先级 - 10
}
# 然后在实例(vrrp_instance) 里面引用有点类似脚本里面的函数引用一样先定义后引用函数名
track_script {
check_running
}

注意:
VRRP 脚本 (vrrp_script) 和 VRRP 实例 (vrrp_instance) 属于同一个级别
keepalived 会定时执行脚本并对脚本执行的结果进行分析,动态调整 vrrp_instance 的优先级。一般脚本检测返回的值为 0,说明脚本检测成功,如果为非 0 数值,则说明检测失败
如果脚本执行结果为 0,并且 weight 配置的值大于 0,则优先级相应的增加, 如果 weight 为非 0,则优先级不变
如果脚本执行结果非 0,并且 weight 配置的值小于 0,则优先级相应的减少, 如果 weight 为 0,则优先级不变
其他情况,维持原本配置的优先级,即配置文件中 priority 对应的值。
这里需要注意的是:
1) 优先级不会不断的提高或者降低
2) 可以编写多个检测脚本并为每个检测脚本设置不同的 weight
3) 不管提高优先级还是降低优先级,最终优先级的范围是在[1,254],不会出现优先级小于等于 0 或者优先级大于等于 255 的情况
这样可以做到利用脚本检测业务进程的状态,并动态调整优先级从而实现主备切换。

7.3 LVS 配置

虚拟服务器virtual_server定义块 ,虚拟服务器定义是keepalived框架最重要的项目了,是keepalived.conf必不可少的部分。 该部分是用来管理LVS的,是实现keepalive和LVS相结合的模块。ipvsadm命令可以实现的管理在这里都可以通过参数配置实现,注意:real_server是被包含在viyual_server模块中的,是子模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
virtual_server 192.168.202.200 23 {        //VIP地址,要和vrrp_instance模块中的virtual_ipaddress地址一致
    delay_loop 6 #健康检查时间间隔
    lb_algo rr   #lvs调度算法rr|wrr|lc|wlc|lblc|sh|dh
    lb_kind DR #负载均衡转发规则NAT|DR|RUN
    persistence_timeout 5 #会话保持时间
    protocol TCP #使用的协议
    persistence_granularity <NETMASK> #lvs会话保持粒度
    virtualhost <string> #检查的web服务器的虚拟主机(host:头)
    sorry_server<IPADDR> <port> #备用机,所有realserver失效后启用

real_server 192.168.200.5 23 { //RS的真实IP地址
weight 1 #默认为1,0为失效
inhibit_on_failure #在服务器健康检查失效时,将其设为0,而不是直接从ipvs中删除
notify_up <string> | <quoted-string> #在检测到server up后执行脚本
notify_down <string> | <quoted-string> #在检测到server down后执行脚本

TCP_CHECK { //常用
connect_timeout 3 #连接超时时间
nb_get_retry 3 #重连次数
delay_before_retry 3 #重连间隔时间
connect_port 23 #健康检查的端口的端口
bindto <ip>
}

HTTP_GET | SSL_GET{ //不常用
url{ #检查url,可以指定多个
path /
digest <string> #检查后的摘要信息
status_code 200 #检查的返回状态码
}
connect_port <port>
bindto <IPADD>
connect_timeout 5
nb_get_retry 3
delay_before_retry 2
}

SMTP_CHECK{ //不常用
host{
connect_ip <IP ADDRESS>
connect_port <port> #默认检查25端口
bindto <IP ADDRESS>
}
connect_timeout 5
retry 3
delay_before_retry 2
helo_name <string> | <quoted-string> #smtp helo请求命令参数,可选
}

MISC_CHECK{ //不常用
misc_path <string> | <quoted-string> #外部脚本路径
misc_timeout #脚本执行超时时间
misc_dynamic #如设置该项,则退出状态码会用来动态调整服务器的权重,返回0 正常,不修改;返回1,

  检查失败,权重改为0;返回2-255,正常,权重设置为:返回状态码-2
}
}

1 macOS 编译 OpenJDK

目标:编译 OpenJDK17

1.1 准备编译环境

  1. 首先去应用商店安装 xcode.app
  2. 安装 JDK16(比要编译的JDK低一个版本,如要编译的openjdk17,那就安装jdk16)
  3. brew install freetype ccache
阅读全文 »

关键点

  • 主配置 log-bin,指定文件的名字

  • 主配置 server-id,默认为1

  • 从 server-id 与主不能重复

  • 主数据库创建备份账户并授权 REPLICATION SLAVE

  • 主数据库锁表 FLUSH TABLES WITH READ LOCK

  • 主数据库找到 log-bin 的位置 SHOW MASTER STATUS

  • 备份主数据库数据 mysqldump -all-datables --master-data > dbduump.db

  • 主数据库解锁 unlock tables

  • 从数据库导入 dump的数据

  • 在从数据库上设置主数据库的配置

    1
    2
    3
    4
    5
    6
    7
    mysql> CHANGE MASTER TO
    -> MASTER_HOST='master_host_name',
    -> MASTER_PORT=port_num
    -> MASTER_USER='replication_user_name',
    -> MASTER_PASSWORD='replication_password',
    -> MASTER_LOG_FILE='recorded_log_file_name',
    -> MASTER_LOG_POS=recorded_log_position;
    • master_host_name : MySQL主的地址
    • port_num : MySQL主的端口(数字型)
    • replication_user_name : 备份账户的用户名
    • replication_password : 备份账户的密码
    • recorded_log_file_name :bin-log的文件名
    • recorded_log_position : bin-log的位置(数字型)
    • bin-log的文件名和位置 是 从 show master status 得到的。
阅读全文 »

1 环境搭建

  1. 3台服务器

  2. centos 7

  3. 采用 yum 方式,在其中两台安装 mysql

  4. 检查mysql 安装是否正确

  5. 下载 Mycat 软件包

  6. 在第3台机器上安装mycat,并修改配置文件

    image-20210609210635587

  7. 连接mycat,体验数据的增删改查

2 mysql 安装教程

2.1 查询是否安装了mysql**

1
rpm -qa|grep mysql                                                      

2.2 卸载mysql (下面是卸载mysql的库,防止产生冲突,mysql也是类似卸载方式)**

1
2
3
4
rpm -e --nodeps mysql-libs-5.1.*
卸载之后,记得:
find / -name mysql
删除查询出来的所有东西

2.3 安装mysql

1
yum install mysql-server                                                    

注意: centos 7这样安装不行, 详见文档底部

2.4 启动mysql

1
2
3
4
启动方式1:service mysql start
启动方式2:/etc/init.d/mysql start
启动方式3:service mysqld start
启动方式4:/etc/init.d/mysqld start

2.5 root账户默认是没有密码的,修改root密码:

1
2
3
/usr/bin/mysqladmin -u root password 密码 
例如:
/usr/bin/mysqladmin -u root password pwd 这样就将root密码设置成pwd了

2.6 重置root密码(忘记root密码找回)

2.6.1 停止MySQL服务命令:

1
2
/etc/init.d/mysqld stop 
/etc/init.d/mysql stop

2.6.2 输入绕过密码认证命令:

1
mysqld_safe --user=mysql --skip-grant-tables --skip-networking &

1.6.3 输入登录用户命令:

1
mysql -u root mysql                                                        

2.6.4 输入修改root密码SQL语句:

1
update user set Password=password ('123456') where user='root';          

2.6.5 输入数据刷新命令:

1
FLUSH PRIVILEGES;                                                   

2.6.6 退出MySQL命令:

1
quit;                                                              

2.7 设置允许远程连接

1
grant all privileges on *.* to root@'%' identified by '123456789' with grant option;  

2.8 开放端口3306,否则依然无法过远程

2.8.1 打开防火墙配置文件:

1
vi /etc/sysconfig/iptables                                                   

2.8.2 添加下面一行:

1
-A INPUT -m state --state NEW -m tcp -p tcp --dport 3306 -j ACCEPT

注意:开通3306 端口的行必须在icmp-host-prohibited前,否则无效:以下为配置结果图:

图片描述

2.8.3 重启防火墙,使配置生效:

1
/etc/init.d/iptables restart                                                

2.9 设置开机启动mysql:

2.9.1 查看MySQL服务是否自动开启命令

1
2
chkconfig --list | grep mysqld
chkconfig --list | grep mysql

2.9.2 开启MySQL服务自动开启命令

1
2
chkconfig mysqld on
chkconfig mysql on

2.10 将mysql默认引擎设置为InnoDB

修改MySQL配置文件my.cnf

1
2
cd /etc
vi my.cnf

在[mysqld]一段加入

1
default-storage-engine=InnoDB                                            

删除ib_logfile0、ib_logfile1两个文件

1
2
cd /var/lib/mysql
rm -rf ib_logfile*

重启mysql

2.11 开启mysql的日志(监控执行的sql语句)

命令: show global variables like ‘%general%’; 该语句可以查看是否开启, 以及生成的位置

1
2
set global general_log = on; // 打开  
set global general_log = off; // 关闭

参考文档:

http://blog.csdn.net/fdipzone/article/details/16995303

2.12 centos7安装mysql

1
2
3
4
5
6
7
8
9
10
wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm
#rpm -ivh mysql-community-release-el7-5.noarch.rpm
#yum install mysql-community-server
成功安装之后重启mysql服务
#service mysqld restart
初次安装mysql是root账户是没有密码的
设置密码的方法
#mysql -uroot
mysql> set password for ‘root’@‘localhost’ = password('mypasswd');
mysql> exit

3 Mycat 安装教程

mycat官网

mycat1权威指南

mycat2权威指南

3.1 安装 Mycat

1
tar -zxvf Mycat-server-1.6.7.1-release-20200209222254-mac.tar

3.2 启动和验证

1
bin/mycat start

启动后,验证一些基本操作,如下图所示:

image-20210610162658349

可以看到,我们成功连上了 mycat 服务器,MyCat 服务器默认定义了一个名为 TESTDB 的逻辑数据库,并且也在该逻辑数据库中定义了一些逻辑表。

但当我们尝试做一些 select 操作的时候,控制台会提示报错,这是因为 MyCat 配置错误导致的。

所以我们需要进行配置。

3.3 配置

image-20210610163531656

其中 ,

  • bin 目录是 MyCat 的启动目录,
  • conf 目录是 MyCat 的配置文件目录,
  • lib 目录是 MyCat 自身的 Jar 包以及所依赖 Jar 包的目录,
  • logs 目录是日志目录。

在 conf 目录下有 3 个重要的配置文件:

  • schema.xml
  • Server.xml
  • Rule.xml

下面就来简单说明这 3 个配置文件的关键配置

3.3.1 schema.xml

schema.xml 文件定义了 MyCat 到底连接那个数据库实例,连接这个数据库实例的哪个数据库。MyCat 一共有几个逻辑数据库,MyCat 一共有几个逻辑表。

schema.xml 文件一共有四个配置节点:DataHostDataNodeSchemaTable

  • DataHost:定义数据库实例

    • balance:负载均衡类型

      • balance=”0”, 不开启读写分离机制,所有读操作都发送到当前可用的writeHost上。
      • balance=”1”,全部的readHost与stand by writeHost参与select语句的负载均衡,简单的说,当双主双从模式(M1->S1,M2->S2,并且M1与 M2互为主备),正常情况下,M2,S1,S2都参与select语句的负载均衡。
      • balance=”2”,所有读操作都随机的在writeHost、readhost上分发。
      • balance=”3”,所有读请求随机的分发到wiriterHost对应的readhost执行,writerHost不负担读压力,注意balance=3只在1.4及其以后版本有,1.3没有。
    • writeType:写请求类型,0落在第一个writeHost上;1随机;

  • DataNode:定义数据库名称

  • Schema:定义逻辑库

    • checkSQLschema:是否去掉SQL中的schema

    • sqlMaxLimit:select 默认的limit值,仅对分片表有效

    • rule:定义分片表的分片规则,必须与rule.xml中的tableRule对应

    • ruleRequired:是否绑定分片规则,如果为true,没有绑定分片规则,程序报错

  • Table:定义逻辑表

DataHost 节点定义了 MyCat 要连接哪个 MySQL 实例,连接的账号密码是多少。默认的 MyCat 为我们定义了一个名为 localhost1 的数据服务器(DataHost),它指向了本地(localhost)3306 端口的 MySQL 服务器,对应 MySQL 服务器的账号是 root,密码是 123456。

1
2
3
4
5
6
7
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1"  slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="localhost:3306" user="root" password="123456">
<readHost host="hostS2" url="192.168.1.200:3306" user="root" password="xxx" />
</writeHost>
<writeHost host="hostS1" url="localhost:3316" user="root" password="123456" />
</dataHost>

DataNode 节点指定了需要连接的具体数据库名称,其使用一个 dataHost 属性指定该数据库位于哪个数据库实例上。默认的 MyCat 为我们创建了三个数据节点(DataNode),dn1 数据节点对应 localhost1 数据服务器上的 db1 数据库,dn2 数据节点对应 localhost1 数据服务器上的 db2 数据库,dn1 数据节点对应 localhost1 数据服务器上的 db3 数据库。

1
2
3
<dataNode name="dn1" dataHost="localhost1" database="db1" />
<dataNode name="dn2" dataHost="localhost1" database="db2" />
<dataNode name="dn3" dataHost="localhost1" database="db3" />

Schema 节点定义了 MyCat 的所有逻辑数据库,Table 节点定义了 MyCat 的所有逻辑表。默认的 MyCat 为我们定义了一个名为 TESTDB 的逻辑数据库,在这个逻辑数据库下又定义了名为 travaelrecord、company 等 6 个逻辑表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100">
<!-- auto sharding by id (long) -->
<table name="travelrecord" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />

<!-- global table is auto cloned to all defined data nodes ,so can join
with any table whose sharding node is in the same data node -->
<table name="company" primaryKey="ID" type="global" dataNode="dn1,dn2,dn3" />
<table name="goods" primaryKey="ID" type="global" dataNode="dn1,dn2" />
<!-- random sharding using mod sharind rule -->
<table name="hotnews" primaryKey="ID" autoIncrement="true" dataNode="dn1,dn2,dn3"
rule="mod-long" />
<!-- <table name="dual" primaryKey="ID" dataNode="dnx,dnoracle2" type="global"
needAddLimit="false"/> <table name="worker" primaryKey="ID" dataNode="jdbc_dn1,jdbc_dn2,jdbc_dn3"
rule="mod-long" /> -->
<table name="employee" primaryKey="ID" dataNode="dn1,dn2"
rule="sharding-by-intfile" />
<table name="customer" primaryKey="ID" dataNode="dn1,dn2"
rule="sharding-by-intfile">
<childTable name="orders" primaryKey="ID" joinKey="customer_id"
parentKey="id">
<childTable name="order_items" joinKey="order_id"
parentKey="id" />
</childTable>
<childTable name="customer_addr" primaryKey="ID" joinKey="customer_id"
parentKey="id" />
</table>
<!-- <table name="oc_call" primaryKey="ID" dataNode="dn1$0-743" rule="latest-month-calldate"
/> -->
</schema>

所以上面当我们登陆 MyCat 输入show databases会看到只有一个名为 TESTDB 的数据库,这个就是 MyCat 的逻辑数据库。

3.3.2 server.xml

server.xml 定义了项目中连接 MyCat 服务器所需要的账号密码,以及该账号能访问那些逻辑数据库。 server.xml 配置文件中有 SystemUser 两个配置节点。

System 节点定义了连接 MyCat 服务器的系统配置信息。例如是否开启实时统计功能,是否开启全加班一致性检测等。

1
2
3
4
5
6
7
<system>
<property name="useSqlStat">0</property> <!-- 1为开启实时统计、0为关闭 -->
<property name="useGlobleTableCheck">0</property> <!-- 1为开启全加班一致性检测、0为关闭 -->
<property name="sequnceHandlerType">2</property>
<property name="processorBufferPoolType">0</property>
……
</system>

User 配置节点定义了连接 MyCat 服务器的账号密码,以及该账号密码所能进行的数据库操作。默认的 MyCat 为我们创建了一个账户名为 root,密码为 123456 的账号,只能访问 TESTDB 逻辑数据库,并且定义了对相关表的操作权限。

1
2
3
4
5
6
7
8
9
10
<user name="root">
<property name="password">123456</property>
<property name="schemas">TESTDB</property>
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
</user>

3.3.3 rule.xml

rule.xml 定义了逻辑表使用哪个字段进行拆分,使用什么拆分算法进行拆分。rule.xml 中有两个配置节点,分别是:TableRuleFunction 配置节点。

TableRule 配置节点定义了逻辑表的拆分信息,例如使用哪个字段进行拆分,使用什么拆分算法。默认的 MyCat 为我们配置了一个名为 rule2 的表拆分规则,表示根据 user_id 字段进行拆分,拆分算法是 func1。

1
2
3
4
5
6
<tableRule name="rule2">
<rule>
<columns>user_id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>

Function 配置节点则定义了具体的拆分算法。例如使用对 1000 取余的拆分算法,对 100 取余的拆分算分等等。默认的 MyCat 为我们定义了一个名为 func1拆分算法,这个拆分算法定义在 io.mycat.route.function.PartitionByLong 类中,并且还传入了两个参数值。

1
2
3
4
<function name="func1" class="io.mycat.route.function.PartitionByLong">
<property name="partitionCount">8</property>
<property name="partitionLength">128</property>
</function>

3.4 Mycat FAQ

3.4.1 ERROR 1184 (HY000): Invalid DataSource:1

具体错误如下:

image-20210611093234112

错误原因有两种可能:

  1. 没有为mysql用户配置远程访问的权限

    1
    <writeHost host="db1" url="192.168.0.3:3306" user="root" password="123456" />

    授予mysql用户远程访问的权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
       GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456'

    2. 没有在mysql中创建数据库

    ```sql
    # 此操作在当前机的mysql上操作(不再mycat)
    # mysql -uroot -p
    CREATE DATABASE IF NOT EXISTS mycatdb1 DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
    CREATE DATABASE IF NOT EXISTS mycatdb2 DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
    CREATE DATABASE IF NOT EXISTS mycatdb3 DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;

    # 三个分库各自创建表travelrecord
    CREATE TABLE `travelrecord` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(22) NOT NULL DEFAULT '',
    `time` int(10) unsigned NOT NULL DEFAULT '0',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

    # 模拟数据
    INSERT INTO `mycat-db1`.`travelrecord` (`name`, `time`) VALUES ('qkl', '0');
    INSERT INTO `mycat-db1`.`travelrecord` (`name`, `time`) VALUES ('andy', '0');
    INSERT INTO `mycat-db2`.`travelrecord` (`name`, `time`) VALUES ('zgq', '0');
    INSERT INTO `mycat-db3`.`travelrecord` (`name`, `time`) VALUES ('pcb', '0');

    在 mycat 的配置文件 schema.xml 中修改 dataNode节点:

    1
    2
    3
    <dataNode name="dn1" dataHost="localhost1" database="mycatdb1" />
    <dataNode name="dn2" dataHost="localhost1" database="mycatdb2" />
    <dataNode name="dn3" dataHost="localhost1" database="mycatdb3" />

1 Channel 接口

基本的 I/O 操作(bind()、connect()、read()和 write())依赖于底层网络传输所提 供的原语。在基于 Java 的网络编程中,其基本的构造是 class Socket。Netty 的 Channel 接 口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。此外,Channel 也是拥有许多 预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:

阅读全文 »

转载自 http://www.imooc.com/article/316995

数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。

最普通的做法就像下面这样。我们通过 if/else 语句对请求的每一个参数一一校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/api/person")
public class PersonController {

@PostMapping
public ResponseEntity<PersonRequest> save(@RequestBody PersonRequest personRequest) {
if (personRequest.getClassId() == null
|| personRequest.getName() == null
|| !Pattern.matches("(^Man$|^Woman$|^UGM$)", personRequest.getSex())) {

}
return ResponseEntity.ok().body(personRequest);
}
}

这样的代码,小伙伴们在日常开发中一定不少见,很多开源项目都是这样对请求入参做校验的。

但是,不太建议这样来写,这样的代码明显违背了 单一职责原则。大量的非业务代码混杂在业务代码中,非常难以维护,还会导致业务层代码冗杂!

实际上,我们是可以通过一些简单的手段对上面的代码进行改进的!这也是本文主要要介绍的内容!

废话不多说!下面我会结合自己在项目中的实际使用经验,通过实例程序演示如何在 SpringBoot 程序中优雅地的进行参数验证(普通的 Java 程序同样适用)。

不了解的朋友一定要好好看一下,学完马上就可以实践到项目上去。

并且,本文示例项目使用的是目前最新的 Spring Boot 版本 2.4.5!(截止到 2021-04-21)

示例项目源代码地址:github.com/CodingDocs/springboot-guide/tree/master/source-code/bean-validation-demo

添加相关依赖

如果开发普通 Java 程序的的话,你需要可能需要像下面这样依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.9.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>

不过,相信大家都是使用的 Spring Boot 框架来做开发。

基于 Spring Boot 的话,就比较简单了,只需要给项目添加上 spring-boot-starter-web 依赖就够了,它的子依赖包含了我们所需要的东西。另外,我们的示例项目中还使用到了 Lombok。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

但是!!! Spring Boot 2.3 1 之后,spring-boot-starter-validation 已经不包括在了 spring-boot-starter-web 中,需要我们手动加上!

img

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

验证 Controller 的输入

验证请求体

验证请求体即使验证被 @RequestBody 注解标记的方法参数。

PersonController

我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。

1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/api/person")
@Validated
public class PersonController {

@PostMapping
public ResponseEntity<PersonRequest> save(@RequestBody @Valid PersonRequest personRequest) {
return ResponseEntity.ok().body(personRequest);
}
}

PersonRequest

我们使用校验注解对请求的参数进行校验!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {

@NotNull(message = "classId 不能为空")
private String classId;

@Size(max = 33)
@NotNull(message = "name 不能为空")
private String name;

@Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围")
@NotNull(message = "sex 不能为空")
private String sex;

}

正则表达式说明:

  • ^string : 匹配以 string 开头的字符串
  • string$ :匹配以 string 结尾的字符串
  • ^string$ :精确匹配 string 字符串
  • (^Man$|^Woman$|^UGM$) : 值只能在 Man,Woman,UGM 这三个值中选择

GlobalExceptionHandler

自定义异常处理器可以帮助我们捕获异常,并进行一些简单的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ControllerAdvice(assignableTypes = {PersonController.class})
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}

通过测试验证

下面我通过 MockMvc 模拟请求 Controller 的方式来验证是否生效。当然了,你也可以通过 Postman 这种工具来验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SpringBootTest
@AutoConfigureMockMvc
public class PersonControllerTest {
@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;
/**
* 验证出现参数不合法的情况抛出异常并且可以正确被捕获
*/
@Test
public void should_check_person_value() throws Exception {
PersonRequest personRequest = PersonRequest.builder().sex("Man22")
.classId("82938390").build();
mockMvc.perform(post("/api/personRequest")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest)))
.andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可选范围"))
.andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空"));
}
}

使用 Postman 验证

img

验证请求参数

验证请求参数(Path Variables 和 Request Parameters)即是验证被 @PathVariable 以及 @RequestParam 标记的方法参数。

PersonController

一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/api/persons")
@Validated
public class PersonController {

@GetMapping("/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) {
return ResponseEntity.ok().body(id);
}

@PutMapping
public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name) {
return ResponseEntity.ok().body(name);
}
}

ExceptionHandler

1
2
3
4
@ExceptionHandler(ConstraintViolationException.class)
ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}

通过测试验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void should_check_path_variable() throws Exception {
mockMvc.perform(get("/api/person/6")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(content().string("getPersonByID.id: 超过 id 的范围了"));
}

@Test
public void should_check_request_param_value2() throws Exception {
mockMvc.perform(put("/api/person")
.param("name", "snailclimbsnailclimb")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andExpect(content().string("getPersonByName.name: 超过 name 的范围了"));
}

使用 Postman 验证

img

img

验证 Service 中的方法

我们还可以验证任何 Spring Bean 的输入,而不仅仅是 Controller 级别的输入。通过使用@Validated@Valid注释的组合即可实现这一需求!

一般情况下,我们在项目中也更倾向于使用这种方案。

一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。

1
2
3
4
5
6
7
8
9
@Service
@Validated
public class PersonService {

public void validatePersonRequest(@Valid PersonRequest personRequest) {
// do something
}

}

通过测试验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonServiceTest {
@Autowired
private PersonService service;

@Test
public void should_throw_exception_when_person_request_is_not_valid() {
try {
PersonRequest personRequest = PersonRequest.builder().sex("Man22")
.classId("82938390").build();
service.validatePersonRequest(personRequest);
} catch (ConstraintViolationException e) {
// 输出异常信息
e.getConstraintViolations().forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}
}
}

输出结果如下:

1
2
name 不能为空
sex 值不在可选范围

Validator 编程方式手动进行参数验证

某些场景下可能会需要我们手动校验并获得校验结果。

我们通过 Validator 工厂类获得的 Validator 示例。另外,如果是在 Spring Bean 中的话,还可以通过 @Autowired 直接注入的方式。

1
2
@Autowired
Validator validate

具体使用情况如下:

1
2
3
4
5
6
7
8
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator()
PersonRequest personRequest = PersonRequest.builder().sex("Man22")
.classId("82938390").build();
Set<ConstraintViolation<PersonRequest>> violations = validator.validate(personRequest);
// 输出异常信息
violations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage()));
}

输出结果如下:

1
2
sex 值不在可选范围
name 不能为空

自定以 Validator(实用)

如果自带的校验注解无法满足你的需求的话,你还可以自定义实现注解。

案例一:校验特定字段的值是否在可选范围

比如我们现在多了这样一个需求:PersonRequest 类多了一个 Region 字段,Region 字段只能是ChinaChina-TaiwanChina-HongKong这三个中的一个。

第一步,你需要创建一个注解 Region

1
2
3
4
5
6
7
8
9
10
11
12
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = RegionValidator.class)
@Documented
public @interface Region {

String message() default "Region 值不在可选范围内";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}

第二步,你需要实现 ConstraintValidator接口,并重写isValid 方法。

1
2
3
4
5
6
7
8
9
10
11
public class RegionValidator implements ConstraintValidator<Region, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
HashSet<Object> regions = new HashSet<>();
regions.add("China");
regions.add("China-Taiwan");
regions.add("China-HongKong");
return regions.contains(value);
}
}

现在你就可以使用这个注解:

1
2
@Region
private String region;

通过测试验证

1
2
3
4
5
6
PersonRequest personRequest = PersonRequest.builder()
.region("Shanghai").build();
mockMvc.perform(post("/api/person")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest)))
.andExpect(MockMvcResultMatchers.jsonPath("region").value("Region 值不在可选范围内"));

使用 Postman 验证

img

案例二:校验电话号码

校验我们的电话号码是否合法,这个可以通过正则表达式来做,相关的正则表达式都可以在网上搜到,你甚至可以搜索到针对特定运营商电话号码段的正则表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
PhoneNumber.java
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface PhoneNumber {
String message() default "Invalid phone number";
Class[] groups() default {};
Class[] payload() default {};
}
PhoneNumberValidator.java
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {

@Override
public boolean isValid(String phoneField, ConstraintValidatorContext context) {
if (phoneField == null) {
// can be null
return true;
}
// 大陆手机号码11位数,匹配格式:前三位固定格式+后8位任意数
// ^ 匹配输入字符串开始的位置
// \d 匹配一个或多个数字,其中 \ 要转义,所以是 \\d
// $ 匹配输入字符串结尾的位置
String regExp = "^[1]((3[0-9])|(4[5-9])|(5[0-3,5-9])|([6][5,6])|(7[0-9])|(8[0-9])|(9[1,8,9]))\\d{8}$";
return phoneField.matches(regExp);
}
}

搞定,我们现在就可以使用这个注解了。

1
2
3
@PhoneNumber(message = "phoneNumber 格式不正确")
@NotNull(message = "phoneNumber 不能为空")
private String phoneNumber;

通过测试验证

1
2
3
4
5
6
PersonRequest personRequest = PersonRequest.builder()
.phoneNumber("1816313815").build();
mockMvc.perform(post("/api/person")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(personRequest)))
.andExpect(MockMvcResultMatchers.jsonPath("phoneNumber").value("phoneNumber 格式不正确"));

img

使用验证组

验证组我们基本是不会用到的,也不太建议在项目中使用,理解起来比较麻烦,写起来也比较麻烦。简单了解即可!

当我们对对象操作的不同方法有不同的验证规则的时候才会用到验证组。

我写一个简单的例子,你们就能看明白了!

1.先创建两个接口,代表不同的验证组

1
2
3
4
public interface AddPersonGroup {
}
public interface DeletePersonGroup {
}

2.使用验证组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
public class Person {
// 当验证组为 DeletePersonGroup 的时候 group 字段不能为空
@NotNull(groups = DeletePersonGroup.class)
// 当验证组为 AddPersonGroup 的时候 group 字段需要为空
@Null(groups = AddPersonGroup.class)
private String group;
}

@Service
@Validated
public class PersonService {

@Validated(AddPersonGroup.class)
public void validatePersonGroupForAdd(@Valid Person person) {
// do something
}

@Validated(DeletePersonGroup.class)
public void validatePersonGroupForDelete(@Valid Person person) {
// do something
}

}

通过测试验证:

1
2
3
4
5
6
7
8
9
10
11
12
@Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups() {
Person person = new Person();
person.setGroup("group1");
service.validatePersonGroupForAdd(person);
}

@Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups2() {
Person person = new Person();
service.validatePersonGroupForDelete(person);
}

验证组使用下来的体验就是有点反模式的感觉,让代码的可维护性变差了!尽量不要使用!

常用校验注解总结

JSR303 定义了 Bean Validation(校验)的标准 validation-api,并没有提供实现。Hibernate Validation是对这个规范/规范的实现 hibernate-validator,并且增加了 @Email@Length@Range 等注解。Spring Validation 底层依赖的就是Hibernate Validation

JSR 提供的校验注解:

  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
  • @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past 被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期
  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

Hibernate Validator 提供的校验注解

  • @NotBlank(message =) 验证字符串非 null,且长度必须大于 0
  • @Email 被注释的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
  • @NotEmpty 被注释的字符串的必须非空
  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内

拓展

经常有小伙伴问到:“@NotNull@Column(nullable = false) 两者有什么区别?”

我这里简单回答一下:

  • @NotNull是 JSR 303 Bean 验证批注,它与数据库约束本身无关。
  • @Column(nullable = false) : 是 JPA 声明列为非空的方法。

总结来说就是即前者用于验证,而后者则用于指示数据库创建表的时候对表的约束。

转载自 https://blog.lqdev.cn/2019/05/08/springboot/chapter-thirty-seven/

前言

近期在进行项目安全方面评审时,质量管理部门有提出需要对配置文件中的敏高文件进行加密处理,避免了信息泄露问题。想想前段时间某公司上传github时,把相应的生产数据库明文密码也一并上传了,导致了相应的数据泄露问题。也确实,大部分项目无论开发、测试还是生产环境,相关的敏高信息都是明文存储的,也是一大安全隐患呀。所以今天来说说,如何对配置文件进行加密操作。

一点知识

何为Jasypt

Jasypt是一个Java库,允许开发人员以很简单的方式添加基本加密功能,而无需深入研究加密原理。利用它可以实现高安全性的,基于标准的加密技术,无论是单向和双向加密。加密密码,文本,数字,二进制文件。

  1. 高安全性的,基于标准的加密技术,无论是单向和双向加密。加密密码,文本,数字,二进制文件…
  2. 集成Hibernate的。
  3. 可集成到Spring应用程序中,与Spring Security集成。
  4. 集成的能力,用于加密的应用程序(即数据源)的配置。
  5. 特定功能的高性能加密的multi-processor/multi-core系统。
  6. 与任何JCE提供者使用开放的API

官网:http://www.jasypt.org/

SpringBoot集成Jasypt

SpringBoot中集成Jasypt,可直接使用开源的jasypt-spring-boot直接集成,使用简单方便。

mark

常规集成示例

  1. 引入pom依赖
1
2
3
4
5
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>1.18</version>
</dependency>
  1. 设置盐值和修改相应需要加密的配置参数
1
2
3
4
5
# 需要解密的地方,使用ENC()进行包裹处理
okong.name=ENC(Xj7Ykn2O0Hni/tN4oojPfw==)

# 设置盐值,生产环境中,切记不要直接进行设置,可通过环境变量、命令行等形式进行设置。
jasypt.encryptor.password=lqdev

简单来说,就是在需要加密的值使用ENC()进行包裹,即:ENC(密文)。若想避免参数冲突,可修改前缀和后缀,可以直接使用jasypt.encryptor.property.prefixjasypt.encryptor.property.suffix进行修改即可。

之后想往常一样使用@Value("${}")即可。

包含xml引入时

在一些使用javaBean配置和xml两种混合模式时,使用第一种配置时,xml参数并未替换。此时看了官方文档,可以使用另一方式进行配置即可。

官方说明

  1. 引入pom依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot</artifactId>
    <version>1.18</version>
    </dependency>

    其实就是不进行自动配置而已。

  2. 启动类启动方式修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @SpringBootApplication
    @Slf4j
    public class JasyptApplication {

    public static void main(String[] args) throws Exception {

    // SpringApplication.run(JasyptApplication.class, args);
    // 使用自定义环境变量 实现一些特殊场景下的加密字符解密操作
    // 若无额外的xml引入文件需要解密时,可直接使用SpringApplication.run(JasyptApplication.class, args);即可
    // 若想在引入的xml中使用,需要加入环境变量,如以下模式
    new SpringApplicationBuilder().environment(new StandardEncryptableEnvironment())
    .sources(JasyptApplication.class).run(args);
    log.info("spring-boot-jasypt-chapter37服务启动!");
    }
    }

其他配置项

Key Required Default Value
jasypt.encryptor.password True 盐值,根密码
jasypt.encryptor.algorithm False PBEWithMD5AndDES
jasypt.encryptor.keyObtentionIterations False 1000
jasypt.encryptor.poolSize False 1
jasypt.encryptor.providerName False SunJCE
jasypt.encryptor.providerClassName False null
jasypt.encryptor.saltGeneratorClassname False org.jasypt.salt.RandomSaltGenerator
jasypt.encryptor.ivGeneratorClassname False org.jasypt.salt.NoOpIVGenerator
jasypt.encryptor.stringOutputType False base64
jasypt.encryptor.proxyPropertySources False false

运维说明

为了方便运维人员对各类敏感密钥进行加密操作,提供了自动化脚本,方便生成相应的加密串。

密钥(盐值)存储说明

本身加解密过程都是通过盐值进行处理的,所以正常情况下盐值加密串是分开存储的。**盐值应该放在系统属性命令行或是环境变量来使用,而不是放在配置文件。**

命令行示例

1
java -jar xxx.jar --jasypt.encryptor.password=xxx &

环境变量示例

设置环境变量:

1
vim /etc/profileexport JASYPT_PASSWORD = xxxx

启动命令:

1
java -jar xxx.jar --jasypt.encryptor.password=${JASYPT_PASSWORD} &

bat脚本

为了方便,简单编写了一个bat脚本方便使用。

1
2
3
4
5
6
@echo off
set/p input=待加密的明文字符串:
set/p password=加密密钥(盐值):
echo 加密中......
java -cp jasypt-1.9.2.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input=%input% password=%password% algorithm=PBEWithMD5AndDES
pause

注意:jasypt-1.9.2.jar 文件需要和bat脚本放在相同目录下。此包可直接在示例项目中直接下载。

使用示例:

注意:相应加密串,每次加密的结果是不同的。

使用示例

总结

本章节主要简单介绍了如何使用jasypt对配置文件进行加密操作。一些其他高级应用,可以查看官方文档进行相关集成即可。集成起来相对来说比较简单,注意是要对密码(盐值)的管理,需要进行安全把控下,建议运维人员针对每个项目进行不一样的盐值操作,避免一个项目泄露了,造成其他关联项的信息泄露。安全无大小呀,还是谨慎为妙!

参考资料

  1. https://github.com/ulisesbocchio/jasypt-spring-boot
  2. http://www.jasypt.org/