Compare commits
25 Commits
ab6d79c94c
...
main
Author | SHA1 | Date | |
---|---|---|---|
a8f567b17a
|
|||
917f3bd894
|
|||
53d2f55c21
|
|||
f01421d2f6
|
|||
8e8aba464b
|
|||
84648f971e
|
|||
f5c97428a3
|
|||
633e013f91
|
|||
ead3d16257
|
|||
bc5d4921ef
|
|||
5d70598113
|
|||
aa9b672d4f
|
|||
86542c0689
|
|||
997d8a0ac9
|
|||
133b32989c
|
|||
4cab80f831
|
|||
3c2d75d367
|
|||
39fea53675
|
|||
8bfc885370
|
|||
cb6c797863
|
|||
47227f523a
|
|||
939c510f21
|
|||
8dd4593f58
|
|||
c2545377f1 | |||
b5eb1661dc |
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpge filter=lfs diff=lfs merge=lfs -text
|
||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
300
cdh/CDH部署Kerberos.md
Normal file
300
cdh/CDH部署Kerberos.md
Normal file
@ -0,0 +1,300 @@
|
||||
# CDH部署
|
||||
|
||||
## Kerberos部署
|
||||
|
||||
### 系统环境
|
||||
|
||||
- Centos7.7
|
||||
|
||||
### CDH版本
|
||||
|
||||
- 6.3.2
|
||||
|
||||
### 部署用户权限
|
||||
|
||||
- root权限
|
||||
|
||||
### KDC服务安装及配置
|
||||
|
||||
#### 在Cloudera Manager服务器上安装KDC服务
|
||||
|
||||
```bash
|
||||
yum install krb5-server krb5-libs krb5-auth-dialog krb5-workstation openldap-clients -y
|
||||
```
|
||||
|
||||
- 会生成/etc/krb5.conf、/var/kerberos/krb5kdc/kadm5.acl、/var/kerberos/krb5kdc/kdc.conf三个文件。
|
||||
|
||||
#### 修改/etc/krb5.conf配置
|
||||
|
||||
```bash\
|
||||
vi /etc/krb5.conf
|
||||
```
|
||||
|
||||
- 配置文件内容
|
||||
|
||||
```bash
|
||||
# Configuration snippets may be placed in this directory as well
|
||||
includedir /etc/krb5.conf.d/
|
||||
|
||||
[logging]
|
||||
default = FILE:/var/log/krb5libs.log
|
||||
kdc = FILE:/var/log/krb5kdc.log
|
||||
admin_server = FILE:/var/log/kadmind.log
|
||||
|
||||
[libdefaults]
|
||||
dns_lookup_realm = false
|
||||
ticket_lifetime = 24h
|
||||
renew_lifetime = 7d
|
||||
forwardable = true
|
||||
rdns = false
|
||||
pkinit_anchors = FILE:/etc/pki/tls/certs/ca-bundle.crt
|
||||
default_realm = A.COM
|
||||
#default_ccache_name = KEYRING:persistent:%{uid}
|
||||
|
||||
[realms]
|
||||
A.COM = {
|
||||
kdc = node-1
|
||||
admin_server = noe-1
|
||||
}
|
||||
|
||||
[domain_realm]
|
||||
.a.com = A.COM
|
||||
a.com = A.COM
|
||||
```
|
||||
|
||||
##### 配置参数
|
||||
|
||||
- default_realm: 默认realm,例如当我们使用kinit而不指定principal时使用的就是这里配置的默认realm
|
||||
|
||||
- dns_lookup_realm:是否可以通过DNS查找使用什么样的realm
|
||||
|
||||
- ticket_lifetime: 设定ticket的有效期
|
||||
|
||||
- forwardable: 用于指定ticket是否可以被转发,转发的含义是:如果一个用户已经有了一个TGT,当他登入到另一个远程系统,KDC会为他自动重新创建一个TGT,而不需要让用户重新进行身份认证。
|
||||
|
||||
- 然后是[realms]部分,这一部分会列出所有的realm,kdc和admin_server两个配置是在告诉客户端哪台服务器在运行KDC以及kadmin进程。这两项配置可以在服务器上追加端口,如果不指定,则使用默认端口,KDC是88,admin server是749.
|
||||
|
||||
- 最后一部分[domain_realm]是配置DNS名称和Kerberos Realm映射的。 .http://a.com = A.COM是在说:所有在a.com域下的主机都会被映射到A.COM这个realm下,而a.com = A.COM是说a.com它自己也会映射到A.COM这个realm。
|
||||
|
||||
|
||||
|
||||
#### 修改/var/kerberos/krb5kdc/kadm5.acl配置
|
||||
|
||||
```bash
|
||||
vi /var/kerberos/krb5kdc/kadm5.acl
|
||||
```
|
||||
|
||||
```bash
|
||||
*/admin@A.COM *
|
||||
```
|
||||
|
||||
#### 修改/var/kerberos/krb5kdc/kdc.conf配置
|
||||
|
||||
```bash
|
||||
vi /var/kerberos/krb5kdc/kdc.conf
|
||||
```
|
||||
|
||||
```bash
|
||||
[kdcdefaults]
|
||||
kdc_ports = 88
|
||||
kdc_tcp_ports = 88
|
||||
|
||||
[realms]
|
||||
A.COM = {
|
||||
#master_key_type = aes256-cts
|
||||
max_renewable_life= 7d 0h 0m 0s
|
||||
acl_file = /var/kerberos/krb5kdc/kadm5.acl
|
||||
dict_file = /usr/share/dict/words
|
||||
admin_keytab = /var/kerberos/krb5kdc/kadm5.keytab
|
||||
supported_enctypes = aes256-cts:normal aes128-cts:normal des3-hmac-sha1:normal arcfour-hmac:normal camellia256-cts:normal camellia128-cts:normal des-hmac-sha1:normal des-cbc-md5:normal des-cbc-crc:normal
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### 创建Kerberos数据库
|
||||
|
||||
```bash
|
||||
kdb5_util create –r http://A.COM -s
|
||||
```
|
||||
|
||||
- 密码 hadoop
|
||||
|
||||
#### 创建Kerberos的管理账号
|
||||
|
||||
admin/admin@A.COM
|
||||
|
||||
```bash
|
||||
kadmin.local
|
||||
addprinc admin/admin@A.COM
|
||||
```
|
||||
|
||||
- 密码/账号 admin/admin@A.COM
|
||||
|
||||
#### 将Kerberos服务添加到自启动服务,并启动krb5kdc和kadmin服务
|
||||
|
||||
```bash
|
||||
systemctl enable krb5kdc
|
||||
systemctl enable kadmin
|
||||
systemctl start krb5kdc
|
||||
systemctl start kadmin
|
||||
```
|
||||
|
||||
#### 测试Kerberos的管理员账号
|
||||
|
||||
```bash
|
||||
kinit admin/admin@A.COM
|
||||
```
|
||||
|
||||
### 为集群安装所有Kerberos客户端,包括Cloudera Manager
|
||||
|
||||
```bash
|
||||
yum -y install krb5-libs krb5-workstation
|
||||
|
||||
```
|
||||
|
||||
### 将KDC Server上的krb5.conf文件拷贝到所有Kerberos客户端
|
||||
|
||||
```bash
|
||||
scp /etc/krb5.conf root@node-2:/etc/
|
||||
scp /etc/krb5.conf root@node-3:/etc/
|
||||
```
|
||||
|
||||
## CDH集群启用Kerberos
|
||||
|
||||
### 在KDC中给Cloudera Manager添加管理员账号
|
||||
|
||||
cloudera-scm/admin@A.COM
|
||||
|
||||
执行命令
|
||||
|
||||
```bash
|
||||
kadmin.local
|
||||
# 添加cloudera-scm 密码admin
|
||||
addprinc cloudera-scm/admin
|
||||
# 查询已有的用户
|
||||
list_principals
|
||||
```
|
||||
|
||||
### 进入Cloudera Manager的“管理”->“安全”界面
|
||||
|
||||
-
|
||||
|
||||
|
||||
# Kerberos 相关命令
|
||||
|
||||
## 生成kerberos密钥
|
||||
|
||||
- hdfs 账号生成秘钥
|
||||
|
||||
```bash
|
||||
kadmin.local -q "xst -k /tmp/hdfs.keytab hdfs@A.COM -norandkey"
|
||||
kadmin.local -q "xst -k /tmp/hdfs.keytab hdfs@A.COM"
|
||||
#生成密钥文件之后,密码失效,不想密码失效,加 '-norandkey’参数
|
||||
ktadd -k /tmp/hdfs.keytab -norandkey hdfs@A.COM
|
||||
# 或者执行上面的上面命令
|
||||
```
|
||||
|
||||
## 更新Kerberos票据credentials信息
|
||||
|
||||
```bash
|
||||
# 票据过期,无法正常登录,执行命令
|
||||
kinit -R
|
||||
```
|
||||
|
||||
## 测试Keytable是否可用
|
||||
|
||||
```bash
|
||||
kinit -k -t /root/admin.keytab admin/admin@A.COM
|
||||
kinit -k -t /tmp/admin.keytab admin/admin@A.COM
|
||||
```
|
||||
|
||||
## 修改用户的密码
|
||||
|
||||
```bash
|
||||
# 修改用户hdfs的密码为‘admin’
|
||||
kadmin.local -q "cpw -pw admin hdfs "
|
||||
```
|
||||
|
||||
## 查询证书状态
|
||||
|
||||
```bash
|
||||
# 获取hdfs@A.COM账号的信息
|
||||
getprinc hdfs@A.COM
|
||||
|
||||
modprinc -maxlife 720days -maxrenewlife 1000days +allow_renewable krbtgt/A.COM
|
||||
```
|
||||
|
||||
## 修改过期参数
|
||||
|
||||
### ticket lifetime
|
||||
|
||||
```bash
|
||||
ticket lifetime取决于以下5项设置中的最小值:
|
||||
Kerberos server上/var/kerberos/krb5kdc/kdc.conf中max_life
|
||||
内置principal krbtgt的maximum ticket life,可在kadmin命令行下用getprinc命令查看
|
||||
principal的maximum ticket life,可在kadmin命令行下用getprinc命令查看
|
||||
Kerberos client上/etc/krb5.conf的ticket_lifetime
|
||||
kinit -l 参数后面指定的时间
|
||||
```
|
||||
|
||||
|
||||
|
||||
### ticket renew lifetime
|
||||
|
||||
```bash
|
||||
ticket renew lifetime取决于以下5项设置中的最小值:
|
||||
Kerberos server上/var/kerberos/krb5kdc/kdc.conf中max_renewable_life
|
||||
内置principal krbtgt的maximum renewable life,可在kadmin命令行下用getprinc命令查看
|
||||
你的principal的maximum renewable life,可在kadmin命令行下用getprinc命令查看
|
||||
Kerberos client上/etc/krb5.conf的renew_lifetime
|
||||
kinit -r 参数后面指定的时间
|
||||
```
|
||||
|
||||
|
||||
|
||||
# hive的Ldap配置
|
||||
|
||||
## 无LDAP配置
|
||||
|
||||
```bash
|
||||
<property>
|
||||
<name>hive.server2.authentication</name>
|
||||
<value>NONE</value>
|
||||
<description>客户端身份认证方式</description>
|
||||
</property>
|
||||
```
|
||||
|
||||
## LDAP配置
|
||||
|
||||
``` xml
|
||||
<property>
|
||||
<name>hive.server2.authentication</name>
|
||||
<value>LDAP</value>
|
||||
<description>客户端身份认证方式</description>
|
||||
</property>
|
||||
<property>
|
||||
<name>hive.server2.authentication.ldap.url</name>
|
||||
<value>ldaps://ldap.yldev.net:636</value>
|
||||
<description>LDAP Url</description>
|
||||
</property>
|
||||
<property>
|
||||
<name>hive.server2.authentication.ldap.baseDN</name>
|
||||
<value>ou=project,dc=yldev,dc=net</value>
|
||||
<description>LDAP搜索的baseDN</description>
|
||||
</property>
|
||||
|
||||
```
|
||||
|
||||
- hive的权限认证的方式 hive.server2.authentication
|
||||
- **NONE**:不做认证;
|
||||
- **LDAP**: 使用基于 LDAP/AD 的用户身份校验;
|
||||
- **KERBEROS**: 使用 Kerberos/GSSAPI 做身份校验;
|
||||
- LDAP认证的服务器URL:hive.server2.authentication.ldap.url
|
||||
- 协议
|
||||
- 未做ssl的ldap协议
|
||||
- **ldap**://ldap.yldev.net
|
||||
- ssl的ldaps协议
|
||||
- **ldaps**://ldap.yldev.net:636
|
||||
- baseDN 登录用户组
|
||||
- project用户组可以登录hive
|
||||
- **ou=project**,dc=yldev,dc=net
|
@ -4,7 +4,7 @@
|
||||
|
||||
##### 安装编译环境
|
||||
|
||||
```
|
||||
```bash
|
||||
#GCC
|
||||
apt install -y build-essential
|
||||
|
||||
@ -59,7 +59,7 @@ https://nginx.org/download/nginx-1.22.0.tar.gz
|
||||
|
||||
|
||||
|
||||
```
|
||||
```bash
|
||||
./configure \
|
||||
--prefix=/usr/local/nginx \
|
||||
--user=www \
|
||||
@ -113,8 +113,20 @@ https://nginx.org/download/nginx-1.22.0.tar.gz
|
||||
make install
|
||||
```
|
||||
|
||||
##### make 常用命令
|
||||
|
||||
|
||||
```
|
||||
make #编译
|
||||
|
||||
make install #安装
|
||||
|
||||
make clean #清除上一次make命令生成的文件
|
||||
|
||||
make distclean #清除上一次make以及configure命令生成的文件
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 静态编译
|
||||
@ -170,7 +182,7 @@ curl 127.0.0.1
|
||||
|
||||
加到 http 节点中
|
||||
|
||||
```
|
||||
```nginx
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
@ -261,7 +273,7 @@ curl 127.0.0.1
|
||||
|
||||
#### 附1: `systemctl 操作`
|
||||
|
||||
```
|
||||
```bash
|
||||
#重载配置文件 添加新的 或者修改都要重载。
|
||||
systemctl daemon-reload
|
||||
|
||||
@ -334,7 +346,7 @@ curl 127.0.0.1
|
||||
|
||||
apt 卸载nginx
|
||||
|
||||
```
|
||||
```bash
|
||||
查询 nginx 相关软件
|
||||
|
||||
dpkg --get-selections|grep nginx
|
||||
@ -351,9 +363,8 @@ apt-get --purge remove nginx
|
||||
|
||||
###### nginx.conf
|
||||
|
||||
```
|
||||
|
||||
worker_processes auto;
|
||||
```nginx
|
||||
worker_processes auto;
|
||||
|
||||
worker_rlimit_nofile 51200;
|
||||
|
||||
@ -368,66 +379,54 @@ stream {
|
||||
|
||||
|
||||
events {
|
||||
use epoll;
|
||||
worker_connections 51200;
|
||||
multi_accept on;
|
||||
}
|
||||
use epoll;
|
||||
worker_connections 51200;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
server_names_hash_bucket_size 512;
|
||||
client_header_buffer_size 32k;
|
||||
large_client_header_buffers 4 32k;
|
||||
client_max_body_size 50m;
|
||||
server_names_hash_bucket_size 512;
|
||||
client_header_buffer_size 32k;
|
||||
large_client_header_buffers 4 32k;
|
||||
client_max_body_size 50m;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
|
||||
keepalive_timeout 60;
|
||||
keepalive_timeout 60;
|
||||
|
||||
tcp_nodelay on;
|
||||
tcp_nodelay on;
|
||||
|
||||
fastcgi_connect_timeout 300;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_read_timeout 300;
|
||||
fastcgi_buffer_size 64k;
|
||||
fastcgi_buffers 4 64k;
|
||||
fastcgi_busy_buffers_size 128k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
fastcgi_intercept_errors on;
|
||||
fastcgi_connect_timeout 300;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_read_timeout 300;
|
||||
fastcgi_buffer_size 64k;
|
||||
fastcgi_buffers 4 64k;
|
||||
fastcgi_busy_buffers_size 128k;
|
||||
fastcgi_temp_file_write_size 256k;
|
||||
fastcgi_intercept_errors on;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_comp_level 2;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
|
||||
gzip_vary on;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_buffers 4 16k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_comp_level 2;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
|
||||
gzip_vary on;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=perip:10m;
|
||||
limit_conn_zone $server_name zone=perserver:10m;
|
||||
limit_conn_zone $binary_remote_addr zone=perip:10m;
|
||||
limit_conn_zone $server_name zone=perserver:10m;
|
||||
|
||||
server_tokens off;
|
||||
access_log off;
|
||||
server_tokens off;
|
||||
access_log off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root html;
|
||||
}
|
||||
}
|
||||
include /etc/nginx/site-enable/*.conf;
|
||||
include /etc/nginx/site-enable/*.conf;
|
||||
}
|
||||
|
||||
```
|
||||
@ -436,7 +435,7 @@ include /etc/nginx/site-enable/*.conf;
|
||||
|
||||
###### web-site.conf
|
||||
|
||||
```
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name frp.plugin.dr1997.com;
|
||||
@ -461,7 +460,7 @@ server {
|
||||
|
||||
###### ssl
|
||||
|
||||
```
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl http2;
|
||||
@ -501,7 +500,7 @@ server {
|
||||
|
||||
###### 反向代理(http)
|
||||
|
||||
```
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl http2;
|
||||
@ -544,7 +543,7 @@ server {
|
||||
|
||||
###### 反向代理 tcn
|
||||
|
||||
```
|
||||
```nginx
|
||||
upstream tcp {
|
||||
server 127.0.0.1:3306;
|
||||
}
|
||||
|
110
debian11编译redis .md
Normal file
110
debian11编译redis .md
Normal file
@ -0,0 +1,110 @@
|
||||
### debian 11 安装 编译nginx
|
||||
|
||||
|
||||
|
||||
##### 安装编译环境
|
||||
|
||||
```bash
|
||||
#GCC
|
||||
apt install -y build-essential
|
||||
|
||||
#安装正则库
|
||||
apt install -y libpcre3 libpcre3-dev
|
||||
|
||||
#安装zlib库
|
||||
apt install -y zlib1g-dev
|
||||
|
||||
#openssl
|
||||
apt install -y openssl libssl-dev
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 下载解压源码
|
||||
|
||||
```bash
|
||||
wget https://download.redis.io/redis-stable.tar.gz
|
||||
tar -xzvf redis-stable.tar.gz
|
||||
cd redis-stable
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 编译安装
|
||||
|
||||
```bash
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 修改配置
|
||||
|
||||
```bash
|
||||
# 创建配置目录
|
||||
sudo mkdir /etc/redis
|
||||
sudo cp redis.conf /etc/redis/
|
||||
|
||||
# 编辑配置文件
|
||||
sudo vim /etc/redis/redis.conf
|
||||
|
||||
#修改下述配置
|
||||
daemonize yes
|
||||
logfile "/var/log/redis/redis.log"
|
||||
dir /var/lib/redis
|
||||
requirepass your_strong_password_here
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 添加用户
|
||||
|
||||
```bash
|
||||
useradd redis
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 创建进程守护
|
||||
|
||||
```
|
||||
vim /etc/systemd/system/redis.service
|
||||
|
||||
|
||||
|
||||
[Unit]
|
||||
Description=Redis In-Memory Data Store
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=redis
|
||||
Group=redis
|
||||
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf
|
||||
ExecStop=/usr/local/bin/redis-cli shutdown
|
||||
Type=forking # 改为 forking
|
||||
Restart=on-failure
|
||||
RestartSec=5s # 避免频繁重启
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
##### 启动
|
||||
|
||||
```bash
|
||||
systemctl enable redis
|
||||
systemctl restart redis
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 验证安装
|
||||
|
||||
```
|
||||
redis-cli ping
|
||||
```
|
@ -17,6 +17,7 @@
|
||||
| `thenCompose(Function<T, CompletableFuture<U>> fn)` | 返回一个新的 `CompletableFuture`,执行链式的异步任务。 | `CompletableFuture<Integer> future3 = future.thenCompose(result -> CompletableFuture.supplyAsync(() -> result * 2));` |
|
||||
| `exceptionally(Function<Throwable, T> fn)` | 当任务执行失败时,执行异常处理,并返回一个默认值。 | `future.exceptionally(ex -> { System.out.println("任务异常: " + ex.getMessage()); return -1; });` |
|
||||
| `handle(BiFunction<T, Throwable, T> fn)` | 无论任务成功还是失败,都处理结果或异常,并返回一个新的结果。 | `future.handle((result, ex) -> { if (ex != null) return -1; return result * 2; });` |
|
||||
| ` CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)` | 不改变结果 适合监听日志 处理异常 | future.whenComplete((result, ex) -> { if (ex == null) { System.out.println("结果: " + result); } else { System.err.println("异常: " + ex.getMessage()); }}); |
|
||||
| `allOf(CompletableFuture<?>... futures)` | 等待所有 `CompletableFuture` 完成,返回一个新的 `CompletableFuture<Void>`。 | `CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(future1, future2);` |
|
||||
| `anyOf(CompletableFuture<?>... futures)` | 等待任意一个 `CompletableFuture` 完成,返回一个新的 `CompletableFuture<Object>`。 | `CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(future1, future2);` |
|
||||
| `cancel(boolean mayInterruptIfRunning)` | 取消任务。如果任务还未开始,取消会返回 `true`;如果任务已开始,`mayInterruptIfRunning` 为 `true` 时可以中断任务。 | `future.cancel(true);` |
|
||||
|
637
java/Guava中的类型增强.md
Normal file
637
java/Guava中的类型增强.md
Normal file
@ -0,0 +1,637 @@
|
||||
# Guava中的类型增强
|
||||
|
||||
Map - Table、BiMap、Multimap、RangeMap、ClassToInstanceMap
|
||||
|
||||
|
||||
|
||||
#### 1. 简介[#](https://www.cnblogs.com/cao-lei/p/17806222.html#3979976375)
|
||||
|
||||
日常开发中使用Map时经常会遇到很多复杂的处理场景,例如:多个键的Map、不仅可以根据键获取值也可以根据值获取键且不用遍历、重复键的Map、数字等范围内映射相同的值、内存中缓存对象等,Guava提供了以上场景的解决方案。
|
||||
|
||||
| 场景 | 解决方案 | 具体实现 |
|
||||
| ------------------------------------------------ | ------------------ | ------------------------------------------------------------ |
|
||||
| 多个键的Map | Table | HashBasedTable、TreeBasedTable、ImmutableTable |
|
||||
| 不仅可以根据键获取值也可以根据值获取键且不用遍历 | BiMap | HashBiMap、ImmutableBiMap |
|
||||
| 重复键的Map | Multimap | ArrayListMultimap、LinkedListMultimap、LinkedHashMultimap、ImmutableListMultimap、ImmutableSetMultimap |
|
||||
| 数字等范围内映射相同的值 | RangeMap | TreeRangeMap、ImmutableRangeMap |
|
||||
| 内存中缓存对象 | ClassToInstanceMap | MutableClassToInstanceMap、ImmutableClassToInstanceMap |
|
||||
|
||||
本博客将详细描述具体的示例代码。
|
||||
|
||||
#### 2. 添加依赖[#](https://www.cnblogs.com/cao-lei/p/17806222.html#1443701749)
|
||||
|
||||
Maven项目pom.xml中添加依赖:
|
||||
|
||||
```xml
|
||||
Copy<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>32.0.0-jre</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
#### 3. Tbale - 表结构数据[#](https://www.cnblogs.com/cao-lei/p/17806222.html#848306248)
|
||||
|
||||
官方注释翻译:将一对有序键(称为行键和列键)与单个值相关联的集合。
|
||||
示例代码(需求: 记录各个公司每个部门的人数):
|
||||
|
||||
```swift
|
||||
Copy// HashMap
|
||||
Map<String, Integer> deptMap = new HashMap<>();
|
||||
deptMap.put("A部门", 10);
|
||||
deptMap.put("B部门", 20);
|
||||
Map<String, Map<String, Integer>> companyMap = new HashMap<>();
|
||||
companyMap.put("xx公司", deptMap);
|
||||
// HashMap 获取值
|
||||
Integer val = companyMap.get("xx公司").get("A部门");
|
||||
System.out.println("HashMap 获取值: " + val);
|
||||
|
||||
// 创建Hash类型Table, 基于Hash表实现
|
||||
// Table<R, C, V>中三个泛型: R-行, C-列, V-值
|
||||
Table<String, String, Integer> hashTable = HashBasedTable.create();
|
||||
hashTable.put("xx公司", "A部门", 10);
|
||||
hashTable.put("xx公司", "B部门", 20);
|
||||
hashTable.put("xx公司", "C部门", 30);
|
||||
|
||||
System.out.println("\nHash Table: " + hashTable);
|
||||
|
||||
// 创建Tree类型Table, 基于红黑树实现
|
||||
Table<String, String, Integer> treeTable = TreeBasedTable.create();
|
||||
treeTable.put("xx公司", "C部门", 30);
|
||||
treeTable.put("xx公司", "B部门", 20);
|
||||
treeTable.put("xx公司", "A部门", 10);
|
||||
|
||||
System.out.println("\nTree Table: " + treeTable);
|
||||
|
||||
// 创建不可变Table, 无法新增、更新或删除
|
||||
Table<String, String, Integer> immutableTable = ImmutableTable.<String, String, Integer>builder()
|
||||
.put("xx公司", "C部门", 30)
|
||||
.put("xx公司", "B部门", 20)
|
||||
.put("xx公司", "A部门", 10)
|
||||
.build();
|
||||
|
||||
System.out.println("\nImmutable Table: " + immutableTable);
|
||||
|
||||
// Table 获取值
|
||||
Integer val2 = hashTable.get("xx公司", "A部门");
|
||||
System.out.println("\nTable 获取值: " + val2);
|
||||
|
||||
// Table 删除值
|
||||
Integer remove = hashTable.remove("xx公司", "C部门");
|
||||
System.out.println("\nTable 删除值: " + remove);
|
||||
|
||||
// 根据行获取列和值映射
|
||||
Map<String, Integer> columnvalueMap = hashTable.row("xx公司");
|
||||
System.out.println("\nTable 列和值 映射: " + columnvalueMap);
|
||||
|
||||
// 根据列获取行和值映射
|
||||
Map<String, Integer> rowvalueMap = hashTable.column("A部门");
|
||||
System.out.println("\nTable 行和值 映射: " + rowvalueMap);
|
||||
|
||||
// 获取key集合
|
||||
Set<String> rowKeySet = hashTable.rowKeySet();
|
||||
System.out.println("\nTable Row key 集合: " + rowKeySet);
|
||||
Set<String> columnKeySet = hashTable.columnKeySet();
|
||||
System.out.println("\nTable Column key 集合: " + columnKeySet);
|
||||
|
||||
// 获取值集合
|
||||
Collection<Integer> values = hashTable.values();
|
||||
System.out.println("\nTable 值集合: " + values);
|
||||
|
||||
// 判断包含行
|
||||
boolean containsRow = hashTable.containsRow("xx公司");
|
||||
System.out.println("\nTable 包含行: " + containsRow);
|
||||
|
||||
// 判断包含列
|
||||
boolean containsColumn = hashTable.containsColumn("A部门");
|
||||
System.out.println("\nTable 包含列: " + containsColumn);
|
||||
|
||||
// 判断包含行和列
|
||||
boolean contains = hashTable.contains("xx公司", "A部门");
|
||||
System.out.println("\nTable 包含行和列: " + contains);
|
||||
|
||||
// 判断包含值
|
||||
boolean containsValue = hashTable.containsValue(10);
|
||||
System.out.println("\nTable 包含值: " + containsValue);
|
||||
|
||||
// 行和列转置 - 行 转 列
|
||||
Table<String, String, Integer> transposeTable = Tables.transpose(hashTable);
|
||||
|
||||
// 获取所有的行
|
||||
Set<Table.Cell<String, String, Integer>> cells = transposeTable.cellSet();
|
||||
|
||||
// 遍历输出
|
||||
System.out.println("\n遍历输出开始----------------------------");
|
||||
cells.forEach(cell -> System.out.println(cell.getRowKey() + ", " + cell.getColumnKey() + ", " + cell.getValue()));
|
||||
System.out.println("\n遍历输出结束----------------------------");
|
||||
|
||||
// 转换为嵌套的Map
|
||||
Map<String, Map<String, Integer>> rowMap = hashTable.rowMap();
|
||||
System.out.println("\nTable RowMap: " + rowMap);
|
||||
Map<String, Map<String, Integer>> columnMap = hashTable.columnMap();
|
||||
System.out.println("\nTable ColumnMap: " + columnMap);
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```yaml
|
||||
CopyHashMap 获取值: 10
|
||||
|
||||
Hash Table: {xx公司={A部门=10, B部门=20, C部门=30}}
|
||||
|
||||
Tree Table: {xx公司={A部门=10, B部门=20, C部门=30}}
|
||||
|
||||
Immutable Table: {xx公司={C部门=30, B部门=20, A部门=10}}
|
||||
|
||||
Table 获取值: 10
|
||||
|
||||
Table 删除值: 30
|
||||
|
||||
Table 列和值 映射: {A部门=10, B部门=20}
|
||||
|
||||
Table 行和值 映射: {xx公司=10}
|
||||
|
||||
Table Row key 集合: [xx公司]
|
||||
|
||||
Table Column key 集合: [A部门, B部门]
|
||||
|
||||
Table 值集合: [10, 20]
|
||||
|
||||
Table 包含行: true
|
||||
|
||||
Table 包含列: true
|
||||
|
||||
Table 包含行和列: true
|
||||
|
||||
Table 包含值: true
|
||||
|
||||
遍历输出开始----------------------------
|
||||
A部门, xx公司, 10
|
||||
B部门, xx公司, 20
|
||||
|
||||
遍历输出结束----------------------------
|
||||
|
||||
Table RowMap: {xx公司={A部门=10, B部门=20}}
|
||||
|
||||
Table ColumnMap: {A部门={xx公司=10}, B部门={xx公司=20}}
|
||||
```
|
||||
|
||||
#### 4. BiMap - 双向映射Map[#](https://www.cnblogs.com/cao-lei/p/17806222.html#2347438548)
|
||||
|
||||
官方注释翻译:双映射(或“双向映射”)是一种保留其值及其键的唯一性的映射。此约束使双映射能够支持“反向视图”,即另一个双映射,其中包含与此双映射相同的条目,但具有相反的键和值。
|
||||
示例代码(需求: 数组和英文翻译):
|
||||
|
||||
```csharp
|
||||
Copy// 创建BiMap, 底层为两个Hash表的Map
|
||||
BiMap<Integer, String> biMap = HashBiMap.create();
|
||||
biMap.put(1, "one");
|
||||
biMap.put(2, "two");
|
||||
biMap.put(3, "three");
|
||||
biMap.put(4, "four");
|
||||
biMap.put(5, "five");
|
||||
|
||||
System.out.println("BiMap: " + biMap);
|
||||
|
||||
// 创建不可变BiMap, 无法新增、更新或删除
|
||||
BiMap<Object, Object> immutableBiMap = ImmutableBiMap.builder()
|
||||
.put(1, "one")
|
||||
.put(2, "two")
|
||||
.put(3, "three")
|
||||
.put(4, "four")
|
||||
.put(5, "five")
|
||||
.build();
|
||||
|
||||
System.out.println("\nImmutable BiMap: " + immutableBiMap);
|
||||
|
||||
// 通过key获取value
|
||||
String value = biMap.get(1);
|
||||
System.out.println("\nBiMap 根据key获取value: " + value);
|
||||
|
||||
Integer key = biMap.inverse().get("one");
|
||||
System.out.println("\nBiMap 根据value获取key: " + key);
|
||||
|
||||
// 翻转后修改
|
||||
biMap.inverse().put("six", 6);
|
||||
// 返回双映射的逆视图, 并没有创建新对象, 还是之前的对象, 所以操作翻转后的BiMap会影响之前的BiMap
|
||||
System.out.println("\nBiMap 被影响: " + biMap);
|
||||
|
||||
// 底层是HashMap, key不可重复
|
||||
// value不可重复
|
||||
try {
|
||||
biMap.put(11, "one");
|
||||
} catch (Exception e) {
|
||||
System.err.println("BiMap 替换value异常: " + e.getMessage());
|
||||
}
|
||||
|
||||
// 翻转后key不能重复
|
||||
try {
|
||||
biMap.inverse().put("first", 1);
|
||||
} catch (Exception e) {
|
||||
System.err.println("BiMap 替换key异常: " + e.getMessage());
|
||||
}
|
||||
|
||||
// key和value可为null
|
||||
biMap.put(null, null);
|
||||
System.out.println("\nBiMap 根据Null key获取Null value: " + biMap.get(null));
|
||||
System.out.println("\nBiMap 根据Null value获取Null key: " + biMap.inverse().get(null));
|
||||
|
||||
// 强制替换key
|
||||
biMap.forcePut(11, "one");
|
||||
System.out.println("\nBiMap 获取新key: " + biMap.inverse().get("one"));
|
||||
|
||||
// values为Set集合
|
||||
Set<String> values = biMap.values();
|
||||
System.out.println("\nBiMap 不重复的value: " + values);
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```mipsasm
|
||||
CopyBiMap: {1=one, 2=two, 3=three, 4=four, 5=five}
|
||||
|
||||
Immutable BiMap: {1=one, 2=two, 3=three, 4=four, 5=five}
|
||||
|
||||
BiMap 根据key获取value: one
|
||||
|
||||
BiMap 根据value获取key: 1
|
||||
|
||||
BiMap 被影响: {1=one, 2=two, 3=three, 4=four, 5=five, 6=six}
|
||||
BiMap 替换value异常: value already present: one
|
||||
BiMap 替换key异常: key already present: 1
|
||||
|
||||
BiMap 根据Null key获取Null value: null
|
||||
|
||||
BiMap 根据Null value获取Null key: null
|
||||
|
||||
BiMap 获取新key: 11
|
||||
|
||||
BiMap 不重复的value: [two, three, four, five, six, null, one]
|
||||
```
|
||||
|
||||
#### 5. Multimap - 多重映射Map[#](https://www.cnblogs.com/cao-lei/p/17806222.html#819077997)
|
||||
|
||||
官方注释翻译将键映射到值的集合,类似于 Map,但其中每个键可能与 多个 值相关联。
|
||||
示例代码(需求: 学生和各科选修课成绩):
|
||||
|
||||
```swift
|
||||
Copy// 创建Multimap, key为HashMap, value为ArrayList
|
||||
Multimap<String, Integer> arrayListMultimap = ArrayListMultimap.create();
|
||||
arrayListMultimap.put("张三", 90);
|
||||
arrayListMultimap.put("张三", 80);
|
||||
arrayListMultimap.put("张三", 100);
|
||||
arrayListMultimap.put("李四", 88);
|
||||
|
||||
System.out.println("Multimap key为HashMap, value为ArrayList: " + arrayListMultimap);
|
||||
|
||||
// 创建Multimap, key为HashMap, value为HashSet
|
||||
Multimap<String, Integer> hashMultimap = HashMultimap.create();
|
||||
hashMultimap.put("张三", 90);
|
||||
hashMultimap.put("张三", 80);
|
||||
hashMultimap.put("张三", 100);
|
||||
hashMultimap.put("李四", 88);
|
||||
|
||||
System.out.println("\nMultimap key为HashMap, value为HashSet: " + hashMultimap);
|
||||
|
||||
// 创建Multimap, key为LinkedHashMap, value为LinkedList
|
||||
Multimap<String, Integer> linkedListMultimap = LinkedListMultimap.create();
|
||||
linkedListMultimap.put("张三", 90);
|
||||
linkedListMultimap.put("张三", 80);
|
||||
linkedListMultimap.put("张三", 100);
|
||||
linkedListMultimap.put("李四", 88);
|
||||
|
||||
System.out.println("\nMultimap key为LinkedHashMap, value为LinkedList: " + linkedListMultimap);
|
||||
|
||||
// 创建Multimap, key为LinkedHashMap, value为LinkedHashMap
|
||||
Multimap<String, Integer> linkedHashMultimap = LinkedHashMultimap.create();
|
||||
linkedHashMultimap.put("张三", 90);
|
||||
linkedHashMultimap.put("张三", 80);
|
||||
linkedHashMultimap.put("张三", 100);
|
||||
linkedHashMultimap.put("李四", 88);
|
||||
|
||||
System.out.println("\nMultimap key为LinkedHashMap, value为LinkedHashMap: " + linkedHashMultimap);
|
||||
|
||||
// 创建Multimap, key为TreeMap, value为TreeSet
|
||||
Multimap<String, Integer> treeMultimap = TreeMultimap.create();
|
||||
treeMultimap.put("张三", 90);
|
||||
treeMultimap.put("张三", 80);
|
||||
treeMultimap.put("张三", 100);
|
||||
treeMultimap.put("李四", 88);
|
||||
|
||||
System.out.println("\nMultimap key为TreeMap, value为TreeSet: " + treeMultimap);
|
||||
|
||||
// 创建不可变Multimap, 无法新增、更新或删除, key为ImmutableMap, value为ImmutableList
|
||||
Multimap<String, Integer> immutableListMultimap = ImmutableListMultimap.<String, Integer>builder()
|
||||
.put("张三", 90)
|
||||
.put("张三", 80)
|
||||
.put("张三", 100)
|
||||
.put("李四", 88)
|
||||
.build();
|
||||
|
||||
System.out.println("\nMultimap key为ImmutableMap, value为ImmutableList: " + immutableListMultimap);
|
||||
|
||||
// 创建不可变Multimap, 无法新增、更新或删除, key为ImmutableMap, value为ImmutableSet
|
||||
Multimap<String, Integer> immutableSetMultimap = ImmutableSetMultimap.<String, Integer>builder()
|
||||
.put("张三", 90)
|
||||
.put("张三", 80)
|
||||
.put("张三", 100)
|
||||
.put("李四", 88)
|
||||
.build();
|
||||
|
||||
System.out.println("\nMultimap key为ImmutableMap, value为ImmutableSet: " + immutableSetMultimap);
|
||||
|
||||
// 获取值
|
||||
Collection<Integer> values = arrayListMultimap.get("张三");
|
||||
System.out.println("\nMultimap 获取值集合: " + values);
|
||||
|
||||
// 获取不存在key的值, 返回的是空集合, 而不是null
|
||||
Collection<Integer> valuesByNotExistsKey = arrayListMultimap.get("王五");
|
||||
System.out.println("\nMultimap 获取不存在的Key值集合: " + valuesByNotExistsKey);
|
||||
|
||||
// 获取值集合添加值
|
||||
// 返回的是多重映射中关联的值的视图集合, 并没有创建新对象, 还是之前的对象, 所以操作值集合会影响之前的Multimap
|
||||
values.add(60);
|
||||
System.out.println("\nMultimap 被影响: " + arrayListMultimap);
|
||||
|
||||
// 获取大小
|
||||
System.out.println("\nMultimap 大小:" + arrayListMultimap.size());
|
||||
|
||||
// 判断是否为空
|
||||
System.out.println("\nMultimap 是否为空: " + arrayListMultimap.isEmpty());
|
||||
|
||||
// 包含key
|
||||
System.out.println("\nMultimap 包含key: " + arrayListMultimap.containsKey("张三"));
|
||||
|
||||
// 包含value
|
||||
System.out.println("\nMultimap 包含value: " + arrayListMultimap.containsValue(60));
|
||||
|
||||
// 包含key-value键值对
|
||||
System.out.println("\nMultimap 包含key-value对: " + arrayListMultimap.containsEntry("张三", 60));
|
||||
|
||||
// 替换value
|
||||
arrayListMultimap.replaceValues("张三", Arrays.asList(10, 20, 30));
|
||||
System.out.println("\nMultimap 替换value: " + arrayListMultimap);
|
||||
|
||||
// 根据key-value删除
|
||||
arrayListMultimap.remove("张三", 10);
|
||||
System.out.println("\nMultimap 根据key-value删除: " + arrayListMultimap);
|
||||
|
||||
// 根据key删除
|
||||
Collection<Integer> removeAll = arrayListMultimap.removeAll("张三");
|
||||
System.out.println("\nMultimap 根据key删除: " + removeAll);
|
||||
|
||||
// 获取key集合
|
||||
Set<String> keySet = arrayListMultimap.keySet();
|
||||
System.out.println("\nMultimap 获取key集合(HashSet): " + keySet);
|
||||
Multiset<String> keys = arrayListMultimap.keys();
|
||||
System.out.println("\nMultimap 获取key集合(MultiSet): " + keys);
|
||||
|
||||
// 获取所有的key-value
|
||||
Collection<Map.Entry<String, Integer>> entries = arrayListMultimap.entries();
|
||||
System.out.println("\n遍历key-value开始--------------------------");
|
||||
entries.forEach(entry -> System.out.println(entry.getKey() + " : " + entry.getValue()));
|
||||
System.out.println("\n遍历key-value结束--------------------------");
|
||||
|
||||
// 转换为Map<K, Collection<V>>
|
||||
Map<String, Collection<Integer>> collectionMap = arrayListMultimap.asMap();
|
||||
System.out.println("\nMultimap 转换为Map<K, Collection<V>>: " + collectionMap);
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```mipsasm
|
||||
CopyMultimap key为HashMap, value为ArrayList: {李四=[88], 张三=[90, 80, 100]}
|
||||
|
||||
Multimap key为HashMap, value为HashSet: {李四=[88], 张三=[80, 100, 90]}
|
||||
|
||||
Multimap key为LinkedHashMap, value为LinkedList: {张三=[90, 80, 100], 李四=[88]}
|
||||
|
||||
Multimap key为LinkedHashMap, value为LinkedHashMap: {张三=[90, 80, 100], 李四=[88]}
|
||||
|
||||
Multimap key为TreeMap, value为TreeSet: {张三=[80, 90, 100], 李四=[88]}
|
||||
|
||||
Multimap key为ImmutableMap, value为ImmutableList: {张三=[90, 80, 100], 李四=[88]}
|
||||
|
||||
Multimap key为ImmutableMap, value为ImmutableSet: {张三=[90, 80, 100], 李四=[88]}
|
||||
|
||||
Multimap 获取值集合: [90, 80, 100]
|
||||
|
||||
Multimap 获取不存在的Key值集合: []
|
||||
|
||||
Multimap 被影响: {李四=[88], 张三=[90, 80, 100, 60]}
|
||||
|
||||
Multimap 大小:5
|
||||
|
||||
Multimap 是否为空: false
|
||||
|
||||
Multimap 包含key: true
|
||||
|
||||
Multimap 包含value: true
|
||||
|
||||
Multimap 包含key-value对: true
|
||||
|
||||
Multimap 替换value: {李四=[88], 张三=[10, 20, 30]}
|
||||
|
||||
Multimap 根据key-value删除: {李四=[88], 张三=[20, 30]}
|
||||
|
||||
Multimap 根据key删除: [20, 30]
|
||||
|
||||
Multimap 获取key集合(HashSet): [李四]
|
||||
|
||||
Multimap 获取key集合(MultiSet): [李四]
|
||||
|
||||
遍历key-value开始--------------------------
|
||||
李四 : 88
|
||||
|
||||
遍历key-value结束--------------------------
|
||||
|
||||
Multimap 转换为Map<K, Collection<V>>: {李四=[88]}
|
||||
```
|
||||
|
||||
#### 6. RangeMap - 范围映射Map[#](https://www.cnblogs.com/cao-lei/p/17806222.html#684335507)
|
||||
|
||||
官方注释翻译:从不相交的非空范围到非 null 值的映射。查询查找与包含指定键的范围(如果有)关联的值。
|
||||
示例代码(需求:考试成绩分类):
|
||||
|
||||
```swift
|
||||
Copy// if-else
|
||||
int score = 88;
|
||||
String rank;
|
||||
if (0 <= score && score < 60) {
|
||||
rank = "不及格";
|
||||
} else if (60 <= score && score <= 84) {
|
||||
rank = "及格";
|
||||
} else if (84 < score && score <= 100) {
|
||||
rank = "优秀";
|
||||
} else {
|
||||
rank = "无效";
|
||||
}
|
||||
|
||||
System.out.println("if-else 获取值: " + rank);
|
||||
|
||||
// 创建RangeMap, 基于TreeMap(红黑树)实现
|
||||
RangeMap<Integer, String> treeRangeMap = TreeRangeMap.create();
|
||||
treeRangeMap.put(Range.closedOpen(0, 60), "不及格");
|
||||
treeRangeMap.put(Range.closed(60, 84), "及格");
|
||||
treeRangeMap.put(Range.openClosed(84, 100), "优秀");
|
||||
treeRangeMap.put(Range.lessThan(0), "无效");
|
||||
treeRangeMap.put(Range.greaterThan(100), "无效");
|
||||
|
||||
rank = treeRangeMap.get(score);
|
||||
System.out.println("\nRangeMap 获取值: " + rank);
|
||||
|
||||
// 创建不可变RangeMap, 无法新增、更新或删除
|
||||
ImmutableRangeMap<Integer, String> immutableRangeMap = ImmutableRangeMap.<Integer, String>builder()
|
||||
.put(Range.closedOpen(0, 60), "不及格")
|
||||
.put(Range.closed(60, 84), "及格")
|
||||
.put(Range.openClosed(84, 100), "优秀")
|
||||
.put(Range.lessThan(0), "无效")
|
||||
.put(Range.greaterThan(100), "无效")
|
||||
.build();
|
||||
|
||||
rank = immutableRangeMap.get(score);
|
||||
System.out.println("\nImmutableRangeMap 获取值: " + rank);
|
||||
|
||||
// 获取key-value对
|
||||
Map.Entry<Range<Integer>, String> entry = treeRangeMap.getEntry(88);
|
||||
System.out.println("\nRangeMap 获取key-value对: " + entry.getKey() + " : " + entry.getValue());
|
||||
|
||||
// 返回不可变的升序的Map
|
||||
Map<Range<Integer>, String> asMapOfRanges = treeRangeMap.asMapOfRanges();
|
||||
System.out.println("\nRangeMap 不可变的升序的Map: " + asMapOfRanges);
|
||||
|
||||
// 返回不可变的降序的Map
|
||||
Map<Range<Integer>, String> asDescendingMapOfRanges = treeRangeMap.asDescendingMapOfRanges();
|
||||
System.out.println("\nRangeMap 不可变的降序的Map: " + asDescendingMapOfRanges);
|
||||
|
||||
// 相连范围合并
|
||||
RangeMap<Integer, String> treeRangeMap2 = TreeRangeMap.create();
|
||||
treeRangeMap2.putCoalescing(Range.closedOpen(0, 60), "不及格");
|
||||
treeRangeMap2.putCoalescing(Range.closed(60, 84), "及格");
|
||||
treeRangeMap2.putCoalescing(Range.openClosed(84, 100), "及格"); // 或者 [60..84]范围合并
|
||||
treeRangeMap2.putCoalescing(Range.lessThan(0), "无效");
|
||||
treeRangeMap2.putCoalescing(Range.greaterThan(100), "无效");
|
||||
System.out.println("\nRangeMap 不合并相连范围: " + treeRangeMap.asMapOfRanges());
|
||||
System.out.println("RangeMap 合并相连范围: " + treeRangeMap2.asMapOfRanges());
|
||||
|
||||
// 最小范围
|
||||
Range<Integer> span = treeRangeMap.span();
|
||||
System.out.println("\nRangeMap 最小范围: " + span);
|
||||
|
||||
// 子范围Map
|
||||
RangeMap<Integer, String> subRangeMap = treeRangeMap.subRangeMap(Range.closed(70, 90));
|
||||
System.out.println("\nRangeMap 子范围Map: " + subRangeMap);
|
||||
|
||||
// 合并范围
|
||||
treeRangeMap.merge(Range.closed(60, 100), "及格", (s, s2) -> s2);
|
||||
System.out.println("\nRangeMap 合并Map: " + treeRangeMap);
|
||||
|
||||
// 移除范围
|
||||
treeRangeMap.remove(Range.open(90, 95));
|
||||
System.out.println("\nRangeMap 移除范围: " + treeRangeMap);
|
||||
|
||||
// 清除所有范围
|
||||
treeRangeMap.clear();
|
||||
System.out.println("\nRangeMap 清除所有范围: " + treeRangeMap);
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```javascript
|
||||
Copyif-else 获取值: 优秀
|
||||
|
||||
RangeMap 获取值: 优秀
|
||||
|
||||
ImmutableRangeMap 获取值: 优秀
|
||||
|
||||
RangeMap 获取key-value对: (84..100] : 优秀
|
||||
|
||||
RangeMap 不可变的升序的Map: {(-∞..0)=无效, [0..60)=不及格, [60..84]=及格, (84..100]=优秀, (100..+∞)=无效}
|
||||
|
||||
RangeMap 不可变的降序的Map: {(100..+∞)=无效, (84..100]=优秀, [60..84]=及格, [0..60)=不及格, (-∞..0)=无效}
|
||||
|
||||
RangeMap 不合并相连范围: {(-∞..0)=无效, [0..60)=不及格, [60..84]=及格, (84..100]=优秀, (100..+∞)=无效}
|
||||
RangeMap 合并相连范围: {(-∞..0)=无效, [0..60)=不及格, [60..100]=及格, (100..+∞)=无效}
|
||||
|
||||
RangeMap 最小范围: (-∞..+∞)
|
||||
|
||||
RangeMap 子范围Map: {[70..84]=及格, (84..90]=优秀}
|
||||
|
||||
RangeMap 合并Map: [(-∞..0)=无效, [0..60)=不及格, [60..84]=及格, (84..100]=及格, (100..+∞)=无效]
|
||||
|
||||
RangeMap 移除范围: [(-∞..0)=无效, [0..60)=不及格, [60..84]=及格, (84..90]=及格, [95..100]=及格, (100..+∞)=无效]
|
||||
|
||||
RangeMap 清除所有范围: []
|
||||
```
|
||||
|
||||
#### 7. ClassToInstanceMap - 类型映射到实例Map[#](https://www.cnblogs.com/cao-lei/p/17806222.html#85873049)
|
||||
|
||||
官方注释翻译:映射,其每个条目将一个 Java 原始类型 映射到该类型的实例。除了实现 Map之外,还提供额外的类型安全操作 putInstance 和 getInstance 。与任何其他 Map<Class, Object>映射一样,此映射可能包含基元类型的条目,并且基元类型及其相应的包装器类型可以映射到不同的值。
|
||||
示例代码(需求:缓存Bean(不交给Spring管理,自己管理Bean)):
|
||||
|
||||
```csharp
|
||||
Copyclass UserBean {
|
||||
private final Integer id;
|
||||
private final String username;
|
||||
|
||||
public UserBean(Integer id, String username) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserBean{" + "id=" + id + ", username='" + username + '\'' + '}';
|
||||
}
|
||||
}
|
||||
// 创建Bean
|
||||
UserBean userBean = new UserBean(1, "张三");
|
||||
|
||||
// HashMap
|
||||
HashMap<Class, Object> hashMap = new HashMap<>();
|
||||
hashMap.put(UserBean.class, userBean);
|
||||
|
||||
// 获取值,需要强转
|
||||
UserBean value = (UserBean) hashMap.get(UserBean.class);
|
||||
System.out.println("HashMap 获取对象实例: " + value);
|
||||
System.out.println("HashMap 获取对象实例等于创建的Bean: " + (value == userBean));
|
||||
|
||||
// 创建ClassToInstanceMap
|
||||
ClassToInstanceMap<Object> classToInstanceMap = MutableClassToInstanceMap.create();
|
||||
classToInstanceMap.putInstance(UserBean.class, userBean);
|
||||
|
||||
// 获取值,无需强转
|
||||
UserBean value2 = classToInstanceMap.getInstance(UserBean.class);
|
||||
System.out.println("\nClassToInstanceMap 获取对象实例: " + value2);
|
||||
System.out.println("ClassToInstanceMap 获取对象实例等于创建的Bean: " + (value2 == userBean));
|
||||
|
||||
// 创建不可变ClassToInstanceMap, 无法新增、更新或删除
|
||||
ClassToInstanceMap<UserBean> immutableClassToInstanceMap = ImmutableClassToInstanceMap.<UserBean>builder()
|
||||
.put(UserBean.class, userBean)
|
||||
.build();
|
||||
|
||||
// 获取值,无需强转
|
||||
UserBean value3 = immutableClassToInstanceMap.getInstance(UserBean.class);
|
||||
System.out.println("\nImmutableClassToInstanceMap 获取对象实例: " + value3);
|
||||
System.out.println("ImmutableClassToInstanceMap 获取对象实例等于创建的Bean: " + (value3 == userBean));
|
||||
|
||||
|
||||
// 限制类型,避免使用HashMap存储对象时,因为使用Object值类型而在添加缓存时需要今天类型校验
|
||||
ClassToInstanceMap<Collection> classToInstanceMap1 = MutableClassToInstanceMap.create();
|
||||
classToInstanceMap1.put(ArrayList.class, new ArrayList());
|
||||
classToInstanceMap1.put(HashSet.class, new HashSet());
|
||||
// 编译保存: 'put(java.lang.Class<? extends java.util.@org.checkerframework.checker.nullness.qual.NonNull Collection>, java.util.Collection)' in 'com.google.common.collect.MutableClassToInstanceMap' cannot be applied to '(java.lang.Class<java.util.HashMap>, java.util.HashMap)'
|
||||
// classToInstanceMap1.put(HashMap.class, new HashMap());
|
||||
```
|
||||
|
||||
执行结果:
|
||||
|
||||
```bash
|
||||
CopyHashMap 获取对象实例: UserBean{id=1, username='张三'}
|
||||
HashMap 获取对象实例等于创建的Bean: true
|
||||
|
||||
ClassToInstanceMap 获取对象实例: UserBean{id=1, username='张三'}
|
||||
ClassToInstanceMap 获取对象实例等于创建的Bean: true
|
||||
|
||||
ImmutableClassToInstanceMap 获取对象实例: UserBean{id=1, username='张三'}
|
||||
ImmutableClassToInstanceMap 获取对象实例等于创建的Bean: true
|
||||
```
|
198
java/Jsr303校验.md
198
java/Jsr303校验.md
@ -4,8 +4,6 @@
|
||||
|
||||
|
||||
|
||||
### 注解
|
||||
|
||||
### JSR303中含有的注解
|
||||
|
||||
```
|
||||
@ -24,6 +22,8 @@
|
||||
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### Hibernate Validator 附加的注解
|
||||
|
||||
```
|
||||
@ -36,142 +36,102 @@
|
||||
注:HIbernate Validator是JSR303的一个参考实现,除了支持所有标准的校验注解外,另外HIbernate Validator还有JSR-380的实现
|
||||
```
|
||||
|
||||
### SpringMVC中使用JSR303进行服务器端验证
|
||||
|
||||
|
||||
a. 添加相关依赖
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
<version>6.0.7.Final</version>
|
||||
</dependency>
|
||||
b. 给校验对象的指定属性添加校验规则
|
||||
public class User {
|
||||
private Long userId;
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String userName;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String userPwd;
|
||||
|
||||
............
|
||||
|
||||
}
|
||||
c. 在请求处理方法中,使用@Validated或@Valid注解要验证的对象,并根据BindingResult判断校验是否通过 另外,验证参数后必须紧跟BindingResult参数,否则spring会在校验不通过时直接抛出异常
|
||||
|
||||
public String login(@Validated @ModelAttribute User user,
|
||||
BindingResult bindingResult){
|
||||
// 服务器端校验不通过
|
||||
if (bindingResult.hasErrors()) {
|
||||
return "login";
|
||||
}
|
||||
....
|
||||
}
|
||||
|
||||
注1:@Valid和Validated的区别,随便使用哪个都行
|
||||
@Valid是使用hibernate validation的时候使用
|
||||
@Validated 是只用spring Validator校验机制使用
|
||||
|
||||
### @Validated @Valid 有什么区别
|
||||
|
||||
`@Validated` 是spring 中提供的注解用于在添加该注解的方法、类上触发bean 校验。
|
||||
|
||||
`@Valid` java自身的注解 用于校验嵌套对象(在spring 环境中,添加该注解同样会触发bean 校验)。
|
||||
|
||||
|
||||
|
||||
### 在JSP页面上通过form标签显示消息
|
||||
|
||||
|
||||
### @Validated @Valid 注解的位置
|
||||
|
||||
@Valid 注解在对象上用于嵌套注解
|
||||
|
||||
|
||||
|
||||
<form:errors path="*" /> 显示表单所有错误
|
||||
<form:errors path="user* /> 显示所有以user为前缀的属性对应的错误
|
||||
<form:errors path="username"/> 显示特定表单对象属性的错误
|
||||
|
||||
delimiter:如果一个属性有多个错误,错误信息的分隔符。默认是换行
|
||||
注1:errors标签要放到form标签中才能显示错误消息
|
||||
注2:如果使用form:errors标签不显示错误消息,请检查Model中是否已经添加了命令对象,没有是不会显示错误消息的
|
||||
注3:注意命名规范,Book-->book
|
||||
@ModelAttribute
|
||||
public void init(Model model) {
|
||||
System.out.println("init...");
|
||||
model.addAttribute("user", new User());
|
||||
}
|
||||
@Validated 注解在类上 启用全局的方法校验 (对@RequestBody **不生效**)
|
||||
|
||||
### 通过BindingResult和form:errors标签在JSP页面显示非验证消息
|
||||
@Validated 注解在方法上 启用该方法方法校验 覆盖类全局(对@RsequestBody **不生效**)
|
||||
|
||||
@Validated 注解在参数上 启用该参数校验 (对@RequestBody **生效**)
|
||||
|
||||
|
||||
|
||||
public String login(@Valid @ModelAttribute Yh yh, BindingResult bindingResult, Model model){
|
||||
bindingResult.rejectValue("yhzh", null, "帐号错误");
|
||||
...
|
||||
}
|
||||
bindingResult.rejectValue("email", "validate.email.empty", "邮箱不能为空");//这个函数有好几个重载的变体
|
||||
它们是可以支持国际化的。 比如,上面这个例子表示, 错误的字段(filed)是“email”, errorCode是“validate.email.empty”, 与资源文件对应, 第三个是defaultMessage
|
||||
|
||||
作业1:从BindingResult对象获得错误消息,生成基于json字符串返回到客户端
|
||||
### 对象嵌套校验
|
||||
|
||||
如何实现分组校验
|
||||
2.1 通过接口定义若干个分组
|
||||
2.2 给校验对象的指定属性添加校验规则,并指定校验组
|
||||
2.3 在请求处理方法中,使用@Validated标记要验证的对象,并指定校验组
|
||||
@Valid 校验嵌套对象
|
||||
|
||||
还可以对验证进行分组(@Validated)
|
||||
public class Book {
|
||||
// 书本验证分组
|
||||
public static interface ValidateGroups {
|
||||
// 新增/修改
|
||||
public static interface AddEdit {
|
||||
}
|
||||
```java
|
||||
|
||||
// 上架/下架
|
||||
public static interface DeployUndeploy {
|
||||
}
|
||||
public class Project {
|
||||
|
||||
// 更新封面
|
||||
public static interface UpdateBookImage {
|
||||
}
|
||||
@NotBlank(message = "Project title must be present")
|
||||
@Size(min = 3, max = 20, message = "Project title size not valid")
|
||||
private String title;
|
||||
@Valid // 校验嵌套的对象
|
||||
private User owner;
|
||||
|
||||
}
|
||||
|
||||
private Long bookId;
|
||||
|
||||
@NotBlank(message = "书名不能为空", groups = { AddEdit.class })
|
||||
private String bookName;
|
||||
|
||||
private String bookNamePinyin;
|
||||
|
||||
@NotNull(message = "类别不能为空", groups = { AddEdit.class })
|
||||
private Long bookCategoryId;
|
||||
|
||||
@NotBlank(message = "作者不能为空", groups = { AddEdit.class })
|
||||
private String bookAuthor;
|
||||
|
||||
@NotNull(message = "价格不能为空", groups = { AddEdit.class })
|
||||
@Min(value = 1, message = "价格不能为负")
|
||||
private Double bookPrice;
|
||||
|
||||
private Long bookImage;
|
||||
|
||||
@NotBlank(message = "出版社不能为空", groups = { AddEdit.class })
|
||||
private String publishing;
|
||||
|
||||
@NotBlank(message = "简介不能为空", groups = { AddEdit.class })
|
||||
private String bookDesc;
|
||||
|
||||
private Integer bookState;
|
||||
|
||||
private Date deployDatetime;
|
||||
|
||||
private Integer salesVolume;
|
||||
|
||||
public class User {
|
||||
// 校验规则
|
||||
@NotBlank(message = "User name must be present")
|
||||
@Size(min = 3, max = 50, message = "User name size not valid")
|
||||
private String name;
|
||||
|
||||
// 校验规则
|
||||
@NotBlank(message = "User email must be present")
|
||||
@Email(message = "User email format is incorrect")
|
||||
private String email;
|
||||
}
|
||||
|
||||
@RequestMapping("/add")
|
||||
public String add(
|
||||
@Validated(value = Book.BookGroups.Add.class) @ModelAttribute Book book,
|
||||
BindingResult bindingResult) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
return "admin/addBook";
|
||||
}
|
||||
return "admin/addBook";
|
||||
}
|
||||
|
||||
|
||||
3. 通过分组来指定顺序
|
||||
@GroupSequence({First.class, Second.class, User.class})
|
||||
```
|
||||
|
||||
@Valid 校验可迭代对象
|
||||
|
||||
```java
|
||||
// @Valid 定义在容器对象上
|
||||
@Valid
|
||||
private List<Task> tasks;
|
||||
|
||||
// @Valid (JSR303注解也可以)定义在泛型参数上
|
||||
private List<@Valid Task> tasks;
|
||||
|
||||
private Map<@Valid User, @Valid Task> assignedTasks;
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Validator Api 手动校验BEAN
|
||||
|
||||
|
||||
```java
|
||||
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
|
||||
Validator validator = factory.getValidator();
|
||||
|
||||
AirportDTO dto = new AirportDTO();
|
||||
dto.setIataCode("PKX");
|
||||
dto.setThroughput("-1"); // 符合条件
|
||||
// data.setValue(1234567.789); // 不符合条件,整数部分超过6位
|
||||
// data.setValue(123456.78901); // 不符合条件,小数部分超过4位
|
||||
// data.setValue(-123.45); // 不符合条件,小于0
|
||||
|
||||
Set<ConstraintViolation<AirportDTO>> validate = validator.validate(dto,Update.class);
|
||||
for (ConstraintViolation<AirportDTO> violation : validate) {
|
||||
System.out.println(violation.getMessage());
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
###
|
||||
|
38
java/Springboot配置文件与参数加载优先级.md
Normal file
38
java/Springboot配置文件与参数加载优先级.md
Normal file
@ -0,0 +1,38 @@
|
||||
## Springboot 配置文件 与参数加载优先级
|
||||
|
||||
|
||||
|
||||
### 加载顺序
|
||||
|
||||
在 Spring Boot 中,配置文件的加载顺序是按照以下规则:
|
||||
|
||||
1. **bootstrap.yml 或 bootstrap.properties**: 这是最先加载的配置文件,用于配置应用程序上下文的基础设施,例如外部配置源和加密/解密。
|
||||
2. **application.yml 或 application.properties**: 这是主配置文件,包含应用程序的常规配置。
|
||||
3. **application-{profile}.yml 或 application-{profile}.properties**: 针对不同的环境(profile)加载相应的配置文件。例如,`application-dev.yml` 用于开发环境,`application-prod.yml` 用于生产环境。
|
||||
|
||||
|
||||
|
||||
### 配置文件目录
|
||||
|
||||
SpringBoot配置文件可以放置在多种路径下,不同路径下的配置优先级有所不同。
|
||||
可放置目录**(优先级从高到低)**
|
||||
|
||||
- **file:./config/** (当前项目路径config目录下);
|
||||
- **file:./** (当前项目路径下);
|
||||
- **classpath:/config/** (类路径config目录下);
|
||||
- **classpath:/** (类路径config下).
|
||||
|
||||
优先级由高到底,高优先级的配置会覆盖低优先级的配置;
|
||||
SpringBoot会从这四个位置全部加载配置文件并互补配置;
|
||||
|
||||
|
||||
|
||||
### 命令行参数、环境变量
|
||||
|
||||
Spring Boot 的配置加载顺序(**后者覆盖前者**):
|
||||
|
||||
1. **`application.yml` / `application.properties`**(默认配置)
|
||||
2. **`-D` JVM 参数**(如 `-Dserver.port=8080`)
|
||||
3. **`--` 命令行参数**(如 `--server.port=8081`)
|
||||
4. **环境变量**(如 `SERVER_PORT=8082`)
|
||||
|
10
java/mybatis-plus.md
Normal file
10
java/mybatis-plus.md
Normal file
@ -0,0 +1,10 @@
|
||||
### mybtais-plus
|
||||
|
||||
mybtais-plus 默认空字段 不插入 导致生成的sql 字段不完整,
|
||||
|
||||
在批量插入时, 因为每个po 空值不确定 导致 sql 重写失效,
|
||||
|
||||
|
||||
|
||||
|
||||
&reWriteBatchedInserts=true
|
403
java/oauth-server.md
Normal file
403
java/oauth-server.md
Normal file
@ -0,0 +1,403 @@
|
||||
# 在 Spring Authorization Server 中动态注册客户端
|
||||
|
||||
2024-08-27
|
||||
|
||||
[教程](https://springdoc.cn/categories/教程/)
|
||||
|
||||
## 1、简介
|
||||
|
||||
[Spring Authorization Server](https://springdoc.cn/spring-authorization-server/)(授权服务器)自带一系列合理的默认设置,开箱即用。
|
||||
|
||||
但是,它还有一个功能,默认下没有启动:**态客户端注册**。本文将带你了解如何在客户端应用中启用和使用它。
|
||||
|
||||
## 2、为什么使用动态注册?
|
||||
|
||||
当基于 OAuth2 的客户端应用(在 OIDC 术语中称为依赖方)启动认证流程时,它将自己的客户端标识符发送给身份提供者(Provider)。
|
||||
|
||||
一般情况下,这个标识符是通过外部流程(如邮件发送等其他手段)发放给客户端的,客户端随后将其添加到配置中,并在需要时使用。
|
||||
|
||||
例如,在使用 Azure 的 EntraID 或 Auth0 等流行的身份提供商(Identity Provider)解决方案时,我们可以使用管理控制台或 API 来配置新客户端。在此过程中,我们需要告知应用名称、授权回调 URL、支持的作用域等信息。
|
||||
|
||||
提供所需信息后,我们会得到一个新的客户端标识符,对于所谓的 “secret” 客户端,还将得到一个 *client secret*。然后,我们将这些信息添加到应用的配置中,就可以开始部署了。
|
||||
|
||||
现在,当我们应用不多,或者总是使用单一的一个身份供应商时(Identity Provider),这种方式就能正常工作。但对于更复杂的情况,注册过程需要是动态的,这就是 [OpenID Connect 动态客户端注册规范](https://openid.net/specs/openid-connect-registration-1_0.html) 的用武之地。
|
||||
|
||||
在现实世界中,英国的 [OpenBanking](https://www.openbanking.org.uk/) 标准就是一个很好的例子,该标准将动态客户注册作为其核心协议之一。
|
||||
|
||||
## 3、动态注册是如何实现的?
|
||||
|
||||
OpenID Connect 标准使用一个注册 URL,客户端使用该 URL 注册自己。注册是通过 POST 请求完成的,该请求包含一个 JSON 对象,其中有执行注册所需的客户端元数据。
|
||||
|
||||
**重要的是,访问注册端点需要身份认证,通常是一个 \*Bearer Token\*。当然,这就引出了一个问题:想成为客户端的人如何获得用于此操作的 Token?**
|
||||
|
||||
遗憾的是,答案并不明确。一方面,规范指出端点是受保护的资源,因此需要某种形式的身份认证。另一方面,它也提到了开放注册端点的可能性。
|
||||
|
||||
对于 Spring 授权服务器来说,注册需要一个具有 `client.create` scope 的 *Bearer Token*。要创建该令牌,我们需要使用常规 OAuth2 的 Token 端点和基本凭证。
|
||||
|
||||
动态注册的流程如下:
|
||||
|
||||

|
||||
|
||||
客户端注册成功后,就可以使用返回的客户端 ID 和 secret *secret* 执行任何标准授权流程。
|
||||
|
||||
## 4、实现动态注册
|
||||
|
||||
了解了所需的步骤后,让我们使用两个 Spring Boot 应用创建一个测试场景。一个托管 Spring 授权服务器,另一个是一个简单的 WebMVC 应用程序,它使用 Spring Security Outh2 Login Starter 模块。
|
||||
|
||||
我们先从服务器开始。
|
||||
|
||||
## 5、授权服务器的实现
|
||||
|
||||
首先添加所需的 Maven 依赖:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
|
||||
<version>1.3.1</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
最新版本可从 [Maven Central](https://mvnrepository.com/artifact/org.springframework.security/spring-security-oauth2-authorization-server) 获取。
|
||||
|
||||
对于普通的 *Spring Authorization Server* 来说,只需要这个依赖。
|
||||
|
||||
出于安全考虑,默认情况下不会启用动态注册。此外,截至本文撰写时,还 **无法通过配置属性来启用动态注册**,这意味着我们要通过一些代码来进行配置。
|
||||
|
||||
### 5.1、启用动态注册
|
||||
|
||||
`OAuth2AuthorizationServerConfigurer` 是配置授权服务器所有方面的入口,包括注册端点。这个配置应该作为创建 `SecurityFilterChain` Bean 的一部分完成:
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(SecurityConfig.RegistrationProperties.class)
|
||||
public class SecurityConfig {
|
||||
@Bean
|
||||
@Order(1)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(oidc -> {
|
||||
oidc.clientRegistrationEndpoint(Customizer.withDefaults());
|
||||
});
|
||||
|
||||
http.exceptionHandling((exceptions) -> exceptions
|
||||
.defaultAuthenticationEntryPointFor(
|
||||
new LoginUrlAuthenticationEntryPoint("/login"),
|
||||
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
|
||||
)
|
||||
);
|
||||
|
||||
http.oauth2ResourceServer((resourceServer) -> resourceServer
|
||||
.jwt(Customizer.withDefaults()));
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
// 。。。 其他 Bean
|
||||
}
|
||||
```
|
||||
|
||||
如上,我们使用 `OAuth2AuthorizationServerConfigurer` 的 `oidc()` 方法来访问 `OidConfigurer` 实例,该方法允许我们控制与 OpenID Connect 标准相关的端点。要启用注册端点,我们使用带有默认配置的 `clientRegistrationEndpoint()` 方法。这将在 `/connect/register` 路径下启用注册端点,并使用 Bearer Token 授权。其他配置选项包括:
|
||||
|
||||
- 定义自定义认证
|
||||
- 对收到的注册数据进行自定义处理
|
||||
- 对发送给客户端的响应进行自定义处理
|
||||
|
||||
现在,由于我们提供的是自定义的 `SecurityFilterChain`,Spring Boot 默认的自动配置将不会生效,我们需要负责向配置中添加一些额外的部分。
|
||||
|
||||
尤其需要添加设置表单登录身份认证的逻辑:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
@Order(2)
|
||||
SecurityFilterChain loginFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.authorizeHttpRequests(r -> r.anyRequest().authenticated())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2、注册客户端配置
|
||||
|
||||
如上所述,注册机制本身要求客户端发送一个 Bearer Token。Spring 授权服务器要求客户端使用客户端凭证流(Client Credentials Flow)来生成该 Token,从而解决了这个先有鸡还是先有蛋的问题。
|
||||
|
||||
此 Token 请求所需的 scope 是 `client.create`,客户端必须使用服务器支持的认证方案之一。在这里,我们使用 [Basic 凭证](https://datatracker.ietf.org/doc/html/rfc7617),但在实际场景中,我们也可以使用其他方法。
|
||||
|
||||
从授权服务器的角度来看,这个注册客户端只是另一个客户端。因此,我们使用 `RegisteredClient` Fluent API 来创建它:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public RegisteredClientRepository registeredClientRepository(RegistrationProperties props) {
|
||||
RegisteredClient registrarClient = RegisteredClient.withId(UUID.randomUUID().toString())
|
||||
.clientId(props.getRegistrarClientId())
|
||||
.clientSecret(props.getRegistrarClientSecret())
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.clientSettings(ClientSettings.builder()
|
||||
.requireProofKey(false)
|
||||
.requireAuthorizationConsent(false)
|
||||
.build())
|
||||
.scope("client.create")
|
||||
.scope("client.read")
|
||||
.build();
|
||||
|
||||
RegisteredClientRepository delegate = new InMemoryRegisteredClientRepository(registrarClient);
|
||||
return new CustomRegisteredClientRepository(delegate);
|
||||
}
|
||||
```
|
||||
|
||||
我们使用 `@ConfigurationProperties` 类允许使用 Spring 的 `Environment` 来配置 *client ID* 和 *secret* 属性。
|
||||
|
||||
### 5.3、自定义 RegisteredClientRepository
|
||||
|
||||
Spring 授权服务器使用配置的 `RegisteredClientRepository` 实现将所有注册客户端存储在服务器中。开箱即用的是基于内存和 JDBC 的实现,涵盖了基本用例。
|
||||
|
||||
然而,这些实现在保存注册信息之前并没有提供任何自定义的能力。在我们的案例中,我们希望修改默认的 `ClientProperties` 设置,这样在授权用户时就不需要 *Consent* 或 [PKCE](https://www.baeldung.com/spring-security-pkce-secret-clients)。
|
||||
|
||||
我们的实现将大多数方法委托给构建时传递的实际 Repository。重要的例外是 `save()` 方法:
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void save(RegisteredClient registeredClient) {
|
||||
Set<String> scopes = ( registeredClient.getScopes() == null || registeredClient.getScopes().isEmpty())?
|
||||
Set.of("openid","email","profile"):
|
||||
registeredClient.getScopes();
|
||||
|
||||
// 禁用 PKCE 和 Consent
|
||||
RegisteredClient modifiedClient = RegisteredClient.from(registeredClient)
|
||||
.scopes(s -> s.addAll(scopes))
|
||||
.clientSettings(ClientSettings
|
||||
.withSettings(registeredClient.getClientSettings().getSettings())
|
||||
.requireAuthorizationConsent(false)
|
||||
.requireProofKey(false)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
delegate.save(modifiedClient);
|
||||
}
|
||||
```
|
||||
|
||||
如上,我们根据接收到的 `RegisteredClient` 创建一个新的 `RegisteredClient`,并根据需要更改客户端设置。然后,新注册的客户端将被传递到后台,并在需要时存储起来。
|
||||
|
||||
至此,服务器的实现就结束了。现在,开始客户端部分。
|
||||
|
||||
## 6、动态注册客户端的实现
|
||||
|
||||
我们的客户端也是一个标准的 Spring Web MVC 应用,只有一个页面显示当前用户信息。
|
||||
|
||||
Spring Security,或者更具体地说,其 *OAuth2* Login 模块,将处理所有安全方面的问题。
|
||||
|
||||
从所需的 Maven 依赖开始:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
这些依赖的最新版本可从 Maven Central 获取:
|
||||
|
||||
- *[spring-boot-starter-web](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web)*
|
||||
- *[spring-boot-starter-thymeleaf](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf)*
|
||||
- *[spring-boot-starter-oauth2-client](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client)*
|
||||
|
||||
### 6.1、Security 配置
|
||||
|
||||
默认情况下,Spring Boot 的自动配置机制使用来自可用 `PropertySources` 的信息来收集所需数据,以创建一个或多个 `ClientRegistration` 实例,然后将其存储在基于内存的 `ClientRegistrationRepository` 中。
|
||||
|
||||
例如,给定的 `application.yaml` 如下:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
provider:
|
||||
spring-auth-server:
|
||||
issuer-uri: http://localhost:8080
|
||||
registration:
|
||||
test-client:
|
||||
provider: spring-auth-server
|
||||
client-name: test-client
|
||||
client-id: xxxxx
|
||||
client-secret: yyyy
|
||||
authorization-grant-type:
|
||||
- authorization_code
|
||||
- refresh_token
|
||||
- client_credentials
|
||||
scope:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
```
|
||||
|
||||
Spring 将创建名为 `test-client` 的 `ClientRegistration` 并将其传递给 Repository。
|
||||
|
||||
之后,当需要启动身份认证流程时,OAuth2 引擎就会查询该 Repository,并根据其注册标识符(在我们的例子中为 `test-client`)恢复注册信息。
|
||||
|
||||
这里的关键点是,授权服务器应该已经知道此时返回的 `ClientRegistration`。这意味着,为了支持动态客户端,我们必须实现一个替代 Repository,并将其作为 `@Bean` 暴露。
|
||||
|
||||
这样,Spring Boot 的自动配置就会自动使用它,而不是默认配置。
|
||||
|
||||
### 6.2、动态 ClientRegistration Repository
|
||||
|
||||
我们必须实现 `ClientRegistration` 接口,而该接口只包含一个方法:`findByRegistrationId()`。这就有一个问题: OAuth2 引擎如何知道哪些注册信息是可用的?毕竟,它可以在默认登录页面上列出这些注册信息。
|
||||
|
||||
事实证明,Spring Security 也希望 Repository 也能实现 `Iterable<ClientRegistration>`,这样它就能枚举可用的客户端:
|
||||
|
||||
```java
|
||||
public class DynamicClientRegistrationRepository implements ClientRegistrationRepository, Iterable<ClientRegistration> {
|
||||
private final RegistrationDetails registrationDetails;
|
||||
private final Map<String, ClientRegistration> staticClients;
|
||||
private final RegistrationRestTemplate registrationClient;
|
||||
private final Map<String, ClientRegistration> registrations = new HashMap<>();
|
||||
|
||||
// 实现省略。。。
|
||||
}
|
||||
```
|
||||
|
||||
该类需要一些关键属性才可以运行:
|
||||
|
||||
- 一个 `RegistrationDetails`,其中包含执行动态注册所需的所有参数
|
||||
- 存储动态注册的 `ClientRegistration` 的 `Map`。
|
||||
- 用于访问授权服务器的 `RestTemplate`。
|
||||
|
||||
注意,在本例中,我们假设所有客户端都在同一授权服务器上进行注册。
|
||||
|
||||
另一个重要的设计决策是定义何时进行动态注册。这里,我们采取一种简单的方法,公开 `doRegistrations()` 方法,该方法将注册所有已知客户端,并保存返回的客户端标识符和 *secret*,以供以后使用:
|
||||
|
||||
```java
|
||||
public void doRegistrations() {
|
||||
staticClients.forEach((key, value) -> findByRegistrationId(key));
|
||||
}
|
||||
```
|
||||
|
||||
对于传递给构造函数的每个 *staticClients*,实现过程都会调用 `findByRegistrationId()`。该方法会检查给定标识符是否存在有效注册,如果没有,则会触发实际注册流程。
|
||||
|
||||
### 6.3、动态注册
|
||||
|
||||
`doRegistration()` 函数才是真正发挥作用的地方:
|
||||
|
||||
```java
|
||||
private ClientRegistration doRegistration(String registrationId) {
|
||||
String token = createRegistrationToken();
|
||||
var staticRegistration = staticClients.get(registrationId);
|
||||
|
||||
var body = Map.of(
|
||||
"client_name", staticRegistration.getClientName(),
|
||||
"grant_types", List.of(staticRegistration.getAuthorizationGrantType()),
|
||||
"scope", String.join(" ", staticRegistration.getScopes()),
|
||||
"redirect_uris", List.of(resolveCallbackUri(staticRegistration)));
|
||||
|
||||
var headers = new HttpHeaders();
|
||||
headers.setBearerAuth(token);
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
var request = new RequestEntity<>(
|
||||
body,
|
||||
headers,
|
||||
HttpMethod.POST,
|
||||
registrationDetails.registrationEndpoint());
|
||||
|
||||
var response = registrationClient.exchange(request, ObjectNode.class);
|
||||
// ... 省略异常处理
|
||||
return createClientRegistration(staticRegistration, response.getBody());
|
||||
}
|
||||
```
|
||||
|
||||
首先,我们必须获取调用注册端点所需的注册 Token。注意,我们必须为每次注册尝试获取一个新 Token,因为正如 Spring Authorization 的服务器文档所述,我们只能使用该 Token 一次。
|
||||
|
||||
接下来,使用静态注册对象中的数据构建注册 Payload,添加所需的 `authorization` 和 `content-type` Header,然后将请求发送到注册端点。
|
||||
|
||||
最后,使用响应数据创建最终的 `ClientRegistration`,并将其保存在 Repository 的缓存中,然后返回给 *OAuth2* 引擎。
|
||||
|
||||
### 6.4、注册 ClientRegistrationRepository @Bean
|
||||
|
||||
完成客户端的最后一步是将 `DynamicClientRegistrationRepository` 作为 `@Bean` 公开。
|
||||
|
||||
创建一个 `@Configuration` 类:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
ClientRegistrationRepository dynamicClientRegistrationRepository( DynamicClientRegistrationRepository.RegistrationRestTemplate restTemplate) {
|
||||
var registrationDetails = new DynamicClientRegistrationRepository.RegistrationDetails(
|
||||
registrationProperties.getRegistrationEndpoint(),
|
||||
registrationProperties.getRegistrationUsername(),
|
||||
registrationProperties.getRegistrationPassword(),
|
||||
registrationProperties.getRegistrationScopes(),
|
||||
registrationProperties.getGrantTypes(),
|
||||
registrationProperties.getRedirectUris(),
|
||||
registrationProperties.getTokenEndpoint());
|
||||
|
||||
Map<String,ClientRegistration> staticClients = (new OAuth2ClientPropertiesMapper(clientProperties)).asClientRegistrations();
|
||||
var repo = new DynamicClientRegistrationRepository(registrationDetails, staticClients, restTemplate);
|
||||
repo.doRegistrations();
|
||||
return repo;
|
||||
}
|
||||
```
|
||||
|
||||
`@Bean` 注解的 `dynamicClientRegistrationRepository()` 方法首先会根据可用属性填充 `RegistrationDetails` 记录,从而创建 Repository。
|
||||
|
||||
其次,它利用 Spring Boot 自动配置模块中的 `OAuth2ClientPropertiesMapper` 类创建 *staticClient* map。由于两者的配置结构相同,因此这种方法能让我们以最小的工作量快速从静态客户端(*staticClients*)切换到动态客户端,然后再切换回来。
|
||||
|
||||
## 7、测试
|
||||
|
||||
最后,进行一些集成测试。首先,启动服务器应用,将其配置为监听 *8080* 端口:
|
||||
|
||||
```txt
|
||||
[ server ] $ mvn spring-boot:run
|
||||
... lots of messages omitted
|
||||
[ main] c.b.s.s.a.AuthorizationServerApplication : Started AuthorizationServerApplication in 2.222 seconds (process running for 2.454)
|
||||
[ main] o.s.b.a.ApplicationAvailabilityBean : Application availability state LivenessState changed to CORRECT
|
||||
[ main] o.s.b.a.ApplicationAvailabilityBean : Application availability state ReadinessState changed to ACCEPTING_TRAFFIC
|
||||
```
|
||||
|
||||
接下来,在另一个 shell 中启动客户端:
|
||||
|
||||
```txt
|
||||
[client] $ mvn spring-boot:run
|
||||
// ... 省略其他消息
|
||||
[ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
|
||||
[ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8090 (http) with context path ''
|
||||
[ restartedMain] d.c.DynamicRegistrationClientApplication : Started DynamicRegistrationClientApplication in 2.063 seconds (process running for 2.425)
|
||||
```
|
||||
|
||||
这两个应用在运行时都设置了 *debug* 属性,因此会产生大量日志信息。重点是,我们可以看到对授权服务器 `/connect/register` 端点的调用:
|
||||
|
||||
```txt
|
||||
[nio-8080-exec-3] o.s.security.web.FilterChainProxy : Securing POST /connect/register
|
||||
// ... lots of messages omitted
|
||||
[nio-8080-exec-3] ClientRegistrationAuthenticationProvider : Retrieved authorization with initial access token
|
||||
[nio-8080-exec-3] ClientRegistrationAuthenticationProvider : Validated client registration request parameters
|
||||
[nio-8080-exec-3] s.s.a.r.CustomRegisteredClientRepository : Saving registered client: id=30OTlhO1Fb7UF110YdXULEDbFva4Uc8hPBGMfi60Wik, name=test-client
|
||||
```
|
||||
|
||||
在客户端,我们可以看到一条包含注册标识符(*test-client*)和相应 `client_id` 的信息:
|
||||
|
||||
```txt
|
||||
[ restartedMain] s.d.c.c.OAuth2DynamicClientConfiguration : Creating a dynamic client registration repository
|
||||
[ restartedMain] .c.s.DynamicClientRegistrationRepository : findByRegistrationId: test-client
|
||||
[ restartedMain] .c.s.DynamicClientRegistrationRepository : doRegistration: registrationId=test-client
|
||||
[ restartedMain] .c.s.DynamicClientRegistrationRepository : creating ClientRegistration: registrationId=test-client, client_id=30OTlhO1Fb7UF110YdXULEDbFva4Uc8hPBGMfi60Wik
|
||||
```
|
||||
|
||||
如果我们打开浏览器并访问 *`http://localhost:8090`*,就会被重定向到登录页面。注意,地址栏中的 URL 变成了 *`http://localhost:8080`*,这表明该页面来自授权服务器。
|
||||
|
||||
测试凭证为 `user1/password`。将其填入表单并发送后,就会返回客户端主页。由于我们现在已通过身份认证,我们可以看到一个页面,其中包含从 Authorization Token 中提取的一些详细信息。
|
||||
|
||||
## 8、总结
|
||||
|
||||
本文介绍了如何启用 *Spring Authorization Server* 的动态注册功能,并在基于 Spring Security 的客户端应用中使用该功能。
|
97
java/spring-security.md
Normal file
97
java/spring-security.md
Normal file
@ -0,0 +1,97 @@
|
||||
在Spring Security中,`SecurityFilterChain`和`WebSecurityCustomizer`分别用于不同的配置场景,以下是它们的核心区别和使用场景:
|
||||
|
||||
### 1. **SecurityFilterChain**
|
||||
|
||||
- **作用**:配置`HttpSecurity`,定义请求级别的安全规则(如URL访问控制、认证机制、CSRF、CORS等)。
|
||||
|
||||
- **适用场景**:
|
||||
|
||||
- 需要为特定URL路径设置访问权限(如`permitAll()`、`authenticated()`)。
|
||||
- 配置登录/注销行为、表单登录、OAuth2、JWT等认证机制。
|
||||
- 启用或禁用安全特性(如CSRF保护、Session管理)。
|
||||
|
||||
- **特点**:
|
||||
|
||||
- 请求会经过完整的Spring Security过滤器链。
|
||||
- 即使路径设置为`permitAll()`,请求仍会被安全过滤器处理(如记录日志、CSRF验证等)。
|
||||
|
||||
- **示例**:
|
||||
|
||||
java
|
||||
|
||||
复制
|
||||
|
||||
```
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.antMatchers("/public/**").permitAll() // 允许匿名访问
|
||||
.anyRequest().authenticated() // 其他请求需认证
|
||||
.and()
|
||||
.formLogin() // 启用表单登录
|
||||
.loginPage("/login")
|
||||
.permitAll();
|
||||
return http.build();
|
||||
}
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
### 2. **WebSecurityCustomizer**
|
||||
|
||||
- **作用**:配置`WebSecurity`,**全局忽略**某些路径,使其完全绕过Spring Security过滤器链。
|
||||
|
||||
- **适用场景**:
|
||||
|
||||
- 忽略静态资源(如CSS、JS、图片)或公开API路径。
|
||||
- 提升性能,避免不必要的安全处理。
|
||||
|
||||
- **特点**:
|
||||
|
||||
- 被忽略的路径不会经过任何安全过滤器。
|
||||
- 无法在此处配置细粒度的安全规则(如角色权限、认证方式)。
|
||||
|
||||
- **示例**:
|
||||
|
||||
java
|
||||
|
||||
复制
|
||||
|
||||
```
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring()
|
||||
.antMatchers("/static/**", "/resources/**"); // 完全绕过安全过滤器
|
||||
}
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
### **关键区别**
|
||||
|
||||
| 特性 | SecurityFilterChain | WebSecurityCustomizer |
|
||||
| :--------------- | :------------------------------------ | :----------------------------------- |
|
||||
| **配置对象** | `HttpSecurity`(请求级安全) | `WebSecurity`(全局Web安全) |
|
||||
| **请求处理** | 经过安全过滤器链 | 完全绕过安全过滤器链 |
|
||||
| **适用场景** | 细粒度权限控制、认证机制配置 | 忽略静态资源或无安全需求的路径 |
|
||||
| **性能影响** | 请求会经过过滤器链(可能有开销) | 无开销,直接跳过安全处理 |
|
||||
| **配置方法示例** | `.antMatchers("/api/**").permitAll()` | `.ignoring().antMatchers("/css/**")` |
|
||||
|
||||
------
|
||||
|
||||
### **何时选择?**
|
||||
|
||||
- **使用 `SecurityFilterChain`**:
|
||||
- 需要对路径应用安全规则(如允许匿名访问但仍需CSRF保护)。
|
||||
- 需要配置登录页、OAuth2、权限验证等复杂逻辑。
|
||||
- **使用 `WebSecurityCustomizer`**:
|
||||
- 静态资源(如`/css/**`, `/js/**`)完全无需安全处理。
|
||||
- 公开API(如健康检查`/actuator/health`)无需任何安全机制。
|
||||
|
||||
------
|
||||
|
||||
### **最佳实践**
|
||||
|
||||
- **优先用 `SecurityFilterChain`**:除非明确需要完全绕过安全,否则用`permitAll()`替代`ignoring()`,以确保安全特性(如CSRF)仍生效。
|
||||
- **谨慎使用 `ignoring()`**:被忽略的路径可能暴露敏感信息或成为安全漏洞。
|
94
java/spring.md
Normal file
94
java/spring.md
Normal file
@ -0,0 +1,94 @@
|
||||
# 通过 Spring 5 中 Supplier 来获取 Bean
|
||||
|
||||
|
||||
|
||||
Spring5 中开始提供了 `Supplier`,可以通过接口回调获取到一个 Bean 的实例,这种方式显然性能更好一些。
|
||||
|
||||
如下:
|
||||
|
||||
```java
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
GenericBeanDefinition definition = new GenericBeanDefinition();
|
||||
definition.setBeanClass(Book.class);
|
||||
definition.setInstanceSupplier((Supplier<Book>) () -> {
|
||||
Book book = new Book();
|
||||
book.setName("深入浅出 Spring Security");
|
||||
book.setAuthor("江南一点雨");
|
||||
return book;
|
||||
});
|
||||
ctx.registerBeanDefinition("b1", definition);
|
||||
ctx.refresh();
|
||||
Book b = ctx.getBean("b1", Book.class);
|
||||
System.out.println("b = " + b);
|
||||
```
|
||||
|
||||
关键就是通过调用 `BeanDefinition` 的 `setInstanceSupplier` 方法去设置回调。当然,上面这段代码还可以通过 *Lambda* 进一步简化:
|
||||
|
||||
```java
|
||||
public class BookSupplier {
|
||||
public Book getBook() {
|
||||
Book book = new Book();
|
||||
book.setName("深入浅出 Spring Security");
|
||||
book.setAuthor("江南一点雨");
|
||||
return book;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后调用这个方法即可:
|
||||
|
||||
```java
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
GenericBeanDefinition definition = new GenericBeanDefinition();
|
||||
definition.setBeanClass(Book.class);
|
||||
BookSupplier bookSupplier = new BookSupplier();
|
||||
definition.setInstanceSupplier(bookSupplier::getBook);
|
||||
ctx.registerBeanDefinition("b1", definition);
|
||||
ctx.refresh();
|
||||
Book b = ctx.getBean("b1", Book.class);
|
||||
System.out.println("b = " + b);
|
||||
```
|
||||
|
||||
这是不是更有一点 *Lambda* 的感觉了?
|
||||
|
||||
在 Spring 源码中,处理获取 Bean 实例的时候,有如下一个分支,就是处理 `Supplier` 这种情况的:
|
||||
|
||||
```
|
||||
AbstractAutowireCapableBeanFactory#createBeanInstance
|
||||
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
|
||||
// Make sure bean class is actually resolved at this point.
|
||||
Class<?> beanClass = resolveBeanClass(mbd, beanName);
|
||||
if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
|
||||
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
|
||||
"Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
|
||||
}
|
||||
Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
|
||||
if (instanceSupplier != null) {
|
||||
return obtainFromSupplier(instanceSupplier, beanName);
|
||||
}
|
||||
if (mbd.getFactoryMethodName() != null) {
|
||||
return instantiateUsingFactoryMethod(beanName, mbd, args);
|
||||
}
|
||||
//...
|
||||
return instantiateBean(beanName, mbd);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object obtainInstanceFromSupplier(Supplier<?> supplier, String beanName) {
|
||||
String outerBean = this.currentlyCreatedBean.get();
|
||||
this.currentlyCreatedBean.set(beanName);
|
||||
try {
|
||||
if (supplier instanceof InstanceSupplier<?> instanceSupplier) {
|
||||
return instanceSupplier.get(RegisteredBean.of((ConfigurableListableBeanFactory) this, beanName));
|
||||
}
|
||||
if (supplier instanceof ThrowingSupplier<?> throwableSupplier) {
|
||||
return throwableSupplier.getWithException();
|
||||
}
|
||||
return supplier.get();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上面 `obtainFromSupplier` 这个方法,最终会调用到第二个方法。第二个方法中的 `supplier.get();` 其实最终就调用到我们自己写的 `getBook` 方法了。
|
||||
|
||||
如上,这是从 Spring5 开始结合 Lamdba 的一种 Bean 注入方式。
|
764
java/在 Java 中优雅地操纵时间.md
Normal file
764
java/在 Java 中优雅地操纵时间.md
Normal file
@ -0,0 +1,764 @@
|
||||
# 在 Java 中优雅地操纵时间
|
||||
|
||||
2024-11-11
|
||||
|
||||
[教程](https://springdoc.cn/categories/教程/)
|
||||
|
||||
在开发时候,发现有很多需要用到时间的地方,例如记录操作的时间、比较时间判断产品是否有效等。总而言之,时间是我们业务开发必须关注、时刻注意的点。但目前工程的代码中使用了非常多时间的工具类,一会儿用 `java.util.Date` 记录时间,一会用 `java.time.LocalDateTime` 记录时间,怎么才能在 Java 中优雅的操纵时间呢,我整理了相关的概念和工具类,希望帮助大家在代码开发的过程中对对时间的使用更加优雅。
|
||||
|
||||
这里先写一个结论:
|
||||
|
||||
- 建议使用 java8 的时间 API,在安全性和易用性上都远高于 `java.util.Date`。
|
||||
- 目前比较流行的封装 java API 的时间工具类大都基于 `java.util.Date`,建议在开发过程中根据业务需要基于 `java.time.*` 的方法封装工具类(文末给出了一个简单的实现)。
|
||||
|
||||
## 时间在计算机中的存储和展示
|
||||
|
||||
时间以整数的方式进行存储:时间在计算机中存储的本质是一个整数,称为 Epoch Time(时间戳),计算从 1970 年 1 月 1 日零点(格林威治时间/GMT+00:00)到现在所经历的秒数。
|
||||
|
||||
在 java 程序中,时间戳通常使用 `long` 表示毫秒数,通过 `System.currentTimeMillis()` 可以获取时间戳。时间戳对我们人来说是不易理解的,因此需要将其转换为易读的时间,例如,*2024-10-7 20:21:59*(实际上说的是本地时间),而同一时刻不同时区的人看到的本地时间是不一样,所以在时间展示的时候需要加上时区的信息,才能精准的找到对应的时刻。
|
||||
|
||||
时区与世界时间标准相关:
|
||||
|
||||

|
||||
|
||||
世界时间的标准在 1972 年发生了变化,但我们在开发程序的时候可以忽略 **GMT** 和 **UTC** 的差异, 因为计算机的时钟在联网的时候会自动与时间服务器同步时间。 本地时间等于我们所在(或者所使用)时区内的当地时间,它由与世界标准时间(UTC)之间的偏移量来定义。这个偏移量可以表示为 *UTC-* 或 *UTC+*,后面接上偏移的小时和分钟数。 例如:*GMT+08:00* 或者 *UTC+08:00* 表示东八区,*2024-10-7 20:21:59 UTC+08:00* 便可以精准的定位一个时刻。
|
||||
|
||||
|
||||
|
||||
#### 时间容器
|
||||
|
||||
- `LocalDate`:代表日期,不包含时间或时区。
|
||||
- `LocalTime`:代表时间,不包含日期或时区。
|
||||
- `LocalDateTime`:包括了日期和时间,但不包括时区。
|
||||
- `ZonedDateTime`:包括了日期和时间以及时区。
|
||||
- `Instant`:代表时间轴上的一个特定点,类似于时间戳。
|
||||
|
||||
#### 时间操作
|
||||
|
||||
- `Duration`:表示基于时间的时间量(例如 “5 小时” 或 “30 秒”)。
|
||||
- `Period`:代表基于日期的时间量(如 “2 年 3 个月”)。
|
||||
- `TemporalAdjusters`:提供调整日期的方法(如查找下一个星期一)。
|
||||
- `Clock`:使用时区提供当前日期时间,并可进行时间控制。
|
||||
|
||||
#### 格式化和输出
|
||||
|
||||
- `DateTimeFormatter`:用于格式化和解析日期时间对象。
|
||||
|
||||
|
||||
|
||||
## 日期 API
|
||||
|
||||
JDK 以版本 8 为界,有两套处理日期/时间的 API。
|
||||
|
||||

|
||||
|
||||
简单的比较如下:
|
||||
|
||||
| 特性 | java.util.Date | java.util.Date.Calendar | java.time.LocalDateTime |
|
||||
| -------- | --------------------------------------------- | ---------------------------- | -------------------------- |
|
||||
| 线程安全 | ❌ | ❌ | ✅ |
|
||||
| 时间运算 | ❌ | ✅ | ✅ |
|
||||
| 可读性 | Tue Oct 08 00:11:16 CST 2024 易读性较低 | ❌不易读 | ✅ yyyy-MM-dd’T’HH:mm:ss |
|
||||
| 常量设计 | 需要对获取的年份(+1900)月份(0-11)进行处理 | 需要对获月份(0-11)进行处理 | ✅ 不需要额外处理,符合常识 |
|
||||
| 时间精度 | 精确到毫秒 | 精确到毫秒 | 精确到纳秒 |
|
||||
| 时区 | 具体的时间调用 | 不 | - |
|
||||
|
||||
| 特性 | java.text.SimpleDateFormat | java.time.DateTimeFormatter |
|
||||
| -------- | ------------------------------------------------------------ | -------------------------------------- |
|
||||
| 线程安全 | ❌ 在多线程环境下每个线程独立维护一份 SimpleDateFormat 对象实例,或者将 `SimpleDateFormat` 放到 `ThreadLocal` 中 | ✅ 不变对象,线程安全,可以使用单例存储 |
|
||||
| 使用场景 | `Date` | `LocalDateTime` |
|
||||
|
||||
### java.util
|
||||
|
||||
在 jdk8 之前,Java 使用 `java.util` 中的 API 对处理时间。 在获取年月日的时候,`Date` 和 `Calendar` 需要进行不同的转换 => 规则不统一。
|
||||
|
||||
#### Date
|
||||
|
||||
`java.util.Date` 用于表示一个日期和时间的对象,其实现很简单,实际上存储了一个 `long` 类型的以毫秒表示的时间戳,在通过 `new Date()` 获取当前时间的时候,实际上是通过 `System.currentTimeMillis()` 获取时间戳进行赋值。
|
||||
|
||||
```java
|
||||
public class Date {
|
||||
long fastTime;
|
||||
|
||||
public Date(long date) {
|
||||
fastTime = date;
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return fastTime;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`java.util.Date` 承载的功能有限,且在利用 `Date` 类获取具体年/月/日的时候需要注意:`getYear()` 返回的年份必须加上 `1900`,`getMonth()` 返回的月份是 *0-11* 分别表示 *1-12* 月,所以要加 *1*,而 `getDate()` 返回的日期范围是 *1~31*,又不能加 *1*。
|
||||
|
||||
#### Calendar
|
||||
|
||||
`Calendar` 可以用于获取并设置年、月、日、时、分、秒,它和 `Date` 比,主要多了一个可以做简单的日期和时间运算的功能,但代码粗糙,API 不好用,性能也不好。
|
||||
|
||||
`Calendar` 对象 `getTime()` 可以获得 `Date` 对象:
|
||||
|
||||
```java
|
||||
import java.util.*;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
// 获取当前时间:
|
||||
Calendar c = Calendar.getInstance();
|
||||
int y = c.get(Calendar.YEAR);//返回年份不用转换
|
||||
int m = 1 + c.get(Calendar.MONTH);//返回月份需要加1
|
||||
int d = c.get(Calendar.DAY_OF_MONTH);
|
||||
int w = c.get(Calendar.DAY_OF_WEEK);//返回的
|
||||
int hh = c.get(Calendar.HOUR_OF_DAY);
|
||||
int mm = c.get(Calendar.MINUTE);
|
||||
int ss = c.get(Calendar.SECOND);
|
||||
int ms = c.get(Calendar.MILLISECOND);
|
||||
System.out.println(y + "-" + m + "-" + d + " " + w + " " + hh + ":" + mm + ":" + ss + "." + ms);
|
||||
}
|
||||
}
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
// 当前时间:
|
||||
Calendar c = Calendar.getInstance();
|
||||
// 清除所有:
|
||||
c.clear();
|
||||
// 设置年月日时分秒:
|
||||
c.set(2019, 10 /* 11月 */, 20, 8, 15, 0);
|
||||
// 加5天并减去2小时:
|
||||
c.add(Calendar.DAY_OF_MONTH, 5);
|
||||
c.add(Calendar.HOUR_OF_DAY, -2);
|
||||
// 显示时间:
|
||||
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
Date d = c.getTime();
|
||||
System.out.println(sdf.format(d));
|
||||
// 2019-11-25 6:15:00
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### TimeZone
|
||||
|
||||
`Calendar` 和 `Date` 相比,它提供了时区转换的功能。时区用 `TimeZone` 对象表示。
|
||||
|
||||
时区的唯一标识是以字符串表示的 *ID*。获取指定 `TimeZone` 对象也是以这个 *ID* 为参数获取,*GMT+09:00*、*Asia/Shanghai* 都是有效的时区 *ID*。可以通过 `TimeZone.getAvailableIDs()` 获取系统支持的所有 *ID*。
|
||||
|
||||
```java
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
public class learnTime {
|
||||
public static void main(String[] args) {
|
||||
// 当前时间:
|
||||
Calendar c = Calendar.getInstance();
|
||||
// 清除所有字段:
|
||||
c.clear();
|
||||
// 设置为北京时区:
|
||||
c.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
|
||||
// 设置年月日时分秒:
|
||||
c.set(2024, 9 /* 10月 */, 10, 8, 15, 0);
|
||||
// 显示时间:
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
|
||||
System.out.println(sdf.format(c.getTime()));
|
||||
// 2024-10-09 20:15:00
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### java.text.SimpleDateFormat
|
||||
|
||||
`Date` 使用 `SimpleDateFormat` 解析和格式化时间:
|
||||
|
||||
```java
|
||||
// SimpleDateFormat线程不安全,每次使用都要构造新的,在初始的时候定义解析的字符串格式
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
// 将指定字符串String解析为Date
|
||||
Date date = format.parse("2024-10-07 16:10:22");
|
||||
|
||||
// 将Date格式化为String
|
||||
String str = format.format(date);
|
||||
```
|
||||
|
||||
由于 `SimpleDateFormat` 线程不安全,为了提升性能,可以使用 `ThreadLocalCache`。
|
||||
|
||||
如下:
|
||||
|
||||
```java
|
||||
static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_LOCAL
|
||||
= ThreadLocal.withInitial(
|
||||
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||
);
|
||||
```
|
||||
|
||||
### Java.time.*
|
||||
|
||||
> 开源社区开发了一个日期库 [Joda](https://www.joda.org/joda-time/),API 清晰,性能较好,提交了 *JSR-310*,在 java8 中称为 JDK 基础类库。
|
||||
|
||||
- 本地日期和时间:`LocalDateTime`(日期和时间),`LocalDate`(日期),`LocalTime`(时间)(因为没有时区,所以无法与时间戳转换);
|
||||
- 带时区的日期和时间:`ZonedDateTime`;
|
||||
- 时刻:`Instant`;
|
||||
- 时区:`ZoneId`,`ZoneOffset`;
|
||||
- 时间间隔:`Duration`。
|
||||
|
||||
以及一套新的用于取代 `SimpleDateFormat` 的格式化类型 `DateTimeFormatter`。
|
||||
|
||||
#### LocalDate/LocalTime/LocalDateTime
|
||||
|
||||
- 默认严格按照 *ISO 8601* 规定日期和时间格式进行打印(日期和时间的分隔符是 `T`)。
|
||||
|
||||
- 日期:`yyyy-MM-dd`; 时间 `HH:mm:ss`;
|
||||
- 日期和时间:`yyyy-MM-dd'T'HH:mm:ss`;
|
||||
|
||||
- 可以解析简单格式获取类型:
|
||||
|
||||
```java
|
||||
LocalDateTime localDayTime=LocalDateTime.of(2024, 10, 07, 8, 15, 0);
|
||||
LocalDate localDay=LocalDate.of(2024, 10, 07);
|
||||
LocalTime localTime=LocalTime.parse("08:15:07");
|
||||
```
|
||||
|
||||
- 有对日期和时间进行加减的非常简单的链式调用,通过 `plusXxx()`/`minusXxx()` 对时间进行变换:
|
||||
|
||||
```java
|
||||
public class learnTime {
|
||||
public static void main(String[] args) {
|
||||
LocalDateTime dt = LocalDateTime.of(2024, 10, 10, 20, 30, 59);
|
||||
System.out.println(dt);
|
||||
// 加5天减3小时:2024-10-10T20:30:59
|
||||
LocalDateTime dt2 = dt.plusDays(5).minusHours(3);
|
||||
System.out.println(dt2); // 2024-10-15T17:30:59
|
||||
// 减1月:
|
||||
LocalDateTime dt3 = dt2.minusMonths(1); //2024-09-15T17:30:59
|
||||
System.out.println(dt3); // 2019-09-30T17:30:59
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 对日期和时间进行调整使用 `withXxx()`,例如将月份调整为 9月: `dataLocalTime.withMonth(9)`;
|
||||
|
||||
- 复杂的操作:获取特殊时间
|
||||
|
||||
- `with` 和 `TemporalAdjusters` 配合使用找到特殊时间(当月的第一天)。
|
||||
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 获取本月第一天0:00时刻:
|
||||
System.out.println("当月第一天0:00时刻"+now.withDayOfMonth(1).atStartOfDay());
|
||||
//获取当月第一天
|
||||
System.out.println("当月第一天:"+now.with(TemporalAdjusters.firstDayOfMonth()));
|
||||
//获取下月第一天
|
||||
System.out.println("下月第一天:"+now.with(TemporalAdjusters.firstDayOfNextMonth()));
|
||||
//获取明年第一天
|
||||
System.out.println("明年第一天:"+now.with(TemporalAdjusters.firstDayOfNextYear()));
|
||||
//获取本年第一天
|
||||
System.out.println("本年第一天:"+now.with(TemporalAdjusters.firstDayOfYear()));
|
||||
//获取当月最后一天
|
||||
System.out.println("当月最后一天:"+now.with(TemporalAdjusters.lastDayOfMonth()));
|
||||
//获取本年最后一天
|
||||
System.out.println("本年最后一天:"+now.with(TemporalAdjusters.lastDayOfYear()));
|
||||
//获取当月第三周星期五
|
||||
System.out.println("当月第三周星期五:"+now.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY)));
|
||||
//获取上周一
|
||||
System.out.println("上周一:"+now.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)));
|
||||
//获取下周日
|
||||
System.out.println("下周日:"+now.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)));
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 比较可以使用 `isBefore()` 和 `isAfter()`。
|
||||
|
||||
#### Duration 和 Period
|
||||
|
||||
- Duration: 基于时间值(
|
||||
|
||||
```
|
||||
Instant
|
||||
```
|
||||
|
||||
/
|
||||
|
||||
```
|
||||
LocalDateTime
|
||||
```
|
||||
|
||||
),表示两个时刻时间的时间间隔,适合处理较短的时间,需要更高的精确性。
|
||||
|
||||
- 使用 `between()` 方法比较两个瞬间的差;
|
||||
- 使用 `getSeconds()` 或 `getNanosecends()` 方法获取时间单元的值;
|
||||
- 获得具体的粒度的间隔:`ofDays()`、`ofHours()`、`ofMillis()`、`ofMinutes()`、`ofNanos()`、`ofSeconds()`;
|
||||
- 通过文本创建 `Duration` 对象,格式为 “PnDTnHnMn.nS”,`Duration.parse("P1DT1H10M10.5S")`;
|
||||
- 使用 `toDays()`、`toHours()`、`toMillis()`、`toMinutes()` 方法把 `Duration` 对象可以转成其他时间单元;
|
||||
- 通过 `plusX()`、`minusX()` 方法增加或减少 `Duration` 对象,其中 X 表示 *days*, *hours*, *millis*, *minutes*, *nanos* 或 *seconds*。
|
||||
|
||||
- ```
|
||||
Period
|
||||
```
|
||||
|
||||
|
||||
|
||||
基于日期值,表示一段时间的年、月、日:
|
||||
|
||||
- 使用 `between()` 方法比较两个日期的差;
|
||||
- 使用 `getYears()`、`getMonhs()`、`getDays()` 方法获取具体粒度差距(返回的类型是 `int`);
|
||||
- 通过文本创建 `Period` 对象,格式为 “PnYnMnD”:`Period.parse("P2Y3M5D")`;
|
||||
- 可以通过 `plusX()`、`minusX()` 方法进行增加或减少,其中 `X` 表示日期单元;
|
||||
|
||||
#### ZonedDateTime
|
||||
|
||||
`ZonedDateTime` 是 `LocalDateTime` 加 `ZoneId`。
|
||||
|
||||
- `ZonedDateTime` 带时区时间的常见方法:
|
||||
|
||||
- `now()`:获取当前时区的ZonedDateTime对象。
|
||||
- `now(ZoneId zone)`:获取指定时区的 `ZonedDateTime` 对象。
|
||||
- `getYear`、`getMonthValue`、`getDayOfMonth` 等:获取年月日、时分秒、纳秒等。
|
||||
- `withXxx(时间)`:修改时间系列的方法。
|
||||
- `minusXxx(时间)`:减少时间系列的方法。
|
||||
- `plusXxx(时间)`:增加时间系列的方法。
|
||||
|
||||
- 时区转换
|
||||
|
||||
```java
|
||||
import java.time.*;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
// 以中国时区获取当前时间:
|
||||
ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
|
||||
// 转换为纽约时间:
|
||||
ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));
|
||||
System.out.println(zbj);
|
||||
System.out.println(zny);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ZoneId
|
||||
|
||||
时区类,功能和 `java.util.TimeZone` 类似。
|
||||
|
||||
`ZoneId` 支持两种类型格式初始化,一种是时区偏移的格式(基于 UTC/Greenwich 时),一种是地域时区的格式(eg:*Europe/Paris*)。`ZoneId` 是抽象类,具体的逻辑实现由来子类完成,`ZoneOffset` 处理时区偏移类型的格式,`ZoneRegion` 处理基于地域时区的格式:
|
||||
|
||||
- `getAvailableZoneIds()`:获取Java中支持的所有时区。
|
||||
- `systemDefault()`:获取系统默认时区。
|
||||
- `of(String zoneId)`:获取一个指定时区。
|
||||
|
||||
| 格式 | 描述 | 示例 |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------- |
|
||||
| Z, GMT, UTC, UT | 格林尼治标准时间,和中国相差8个小时 | `ZoneId.of("Z");` |
|
||||
| +h +hh +hh:mm -hh:mm +hhmm -hhmm +hh:mm:ss -hh:mm:ss +hhmmss -hhmmss | 表示从格林尼治标准时间偏移时间,中国用+8表示 | `ZoneId.of("+8");` |
|
||||
| 前缀:UTC+, UTC-, GMT+, GMT-, UT+ UT-, 后缀:-h +hh +hh:mm -hh:mm… | 表示从格林尼治标准时间偏移时间 | `ZoneId.of("UTC+8");` |
|
||||
| Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/GMT+8, Africa/Nairobi, America/Marigot… | 地区表示法,这些ID必须包含在getAvailableZoneIds集合中,否则会抛出异常 | `ZoneId.of("Asia/Shanghai");` |
|
||||
|
||||
#### Instant
|
||||
|
||||
> 时间线上的某个时刻/时间戳
|
||||
|
||||
通过获取 `Instant` 的对象可以拿到此刻的时间,该时间由两部分组成:从 *1970-01-01 00:00:00* 开始走到此刻的总秒数+不够 1 秒的纳秒数。
|
||||
|
||||
- 作用:可以用来记录代码的执行时间,或用于记录用户操作某个事件的时间点。
|
||||
- 传统的 `Date` 类,只能精确到毫秒,并且是可变对象。
|
||||
- 新增的 `Instant` 类,可以精确到纳秒,并且是不可变对象,推荐用 `Instant` 代替 `Date`。
|
||||
|
||||
```java
|
||||
//1、创建Instant的对象,获取此刻时间信息
|
||||
Instant now = Instant.now(); //不可变对象
|
||||
//2、获取总秒数
|
||||
long second = now.getEpochSecond();
|
||||
system.out.println(second) ;
|
||||
//3、不够1秒的纳秒数
|
||||
int nano = now.getNano();
|
||||
system.out.println(nano) ;
|
||||
|
||||
system.out.println(now);
|
||||
//可以进行加减法
|
||||
Instant instant = now.plusNanos(111);//将纳秒加111
|
||||
|
||||
// Instant对象的作用:做代码的性能分析,或者记录用户的操作时间点
|
||||
Instant now1 = Instant.now();
|
||||
//代码执行...
|
||||
Instant now2 = Instant.now();
|
||||
//用这两个时间点相减就可以知道这段代码运行了多少时间
|
||||
```
|
||||
|
||||
#### DateTimeFormatter
|
||||
|
||||
使用方式,传入格式化字符串,可以指定 `local`。
|
||||
|
||||
```java
|
||||
import java.time.*;
|
||||
import java.time.format.*;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
ZonedDateTime zdt = ZonedDateTime.now();
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
|
||||
System.out.println(formatter.format(zdt));
|
||||
|
||||
DateTimeFormatter zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
|
||||
System.out.println(zhFormatter.format(zdt));
|
||||
|
||||
DateTimeFormatter usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
|
||||
System.out.println(usFormatter.format(zdt));
|
||||
|
||||
//2024-10-08T00:25 GMT+08:00
|
||||
//2024 十月 08 星期二 00:25
|
||||
//Tue, October/08/2024 00:25
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 转换
|
||||
|
||||
#### `LocalTimeTime` 和 `Date` 的相互转换
|
||||
|
||||
`LocalDateTime` 不包括时区,而 `Date` 代表一个具体的时间瞬间,精度为毫秒。
|
||||
|
||||
为了从 `LocalDateTime` 转换到 `Date` 需要提供时区。
|
||||
|
||||
```java
|
||||
// LocalDateTime 转换为 Date
|
||||
LocalDateTime localDateTime = LocalDateTime.now();
|
||||
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
|
||||
Date date = Date.from(zonedDateTime.toInstant());
|
||||
// Date 转换为 LocalDateTime
|
||||
Date date = new Date();
|
||||
Instant instant = date.toInstant();
|
||||
LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
```
|
||||
|
||||
#### 数据库映射变化
|
||||
|
||||
- `java.util.Date` 和数据库映射
|
||||
|
||||
```xml
|
||||
<arg column="gmt_create" jdbcType="TIMESTAMP" javaType="java.util.Date"/>
|
||||
```
|
||||
|
||||
- `java.time.*` 和数据库映射
|
||||
|
||||
```xml
|
||||
<arg column="gmt_create" jdbcType="TIMESTAMP" javaType="java.time.LocalDateTime"/>
|
||||
```
|
||||
|
||||
- *mybatis 3.5.0* 以后已经支持,有 `LocalDateTimeTypeHandler` 等类型处理器支持,不需要额外操作。
|
||||
|
||||
- 比较老的 *mybatis* 版本可能会报错,需要添加相关的依赖。
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-typehandlers-jsr310</artifactId>
|
||||
<version>1.0.2</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
*Mybatis* 中和时间相关的 `jdbcType` 和 j`avaType`、`typeHandler` 的对照关系如下:
|
||||
|
||||
| TypeHandler | Java类型 | JDBC类型 |
|
||||
| ------------------------- | ----------------------------- | ---------------------- |
|
||||
| DateTypeHandler | java.util.Date | TIMESTAMP |
|
||||
| DateOnlyTypeHandler | java.util.Date | DATE |
|
||||
| TimeOnlyTypeHandler | java.util.Date | TIME |
|
||||
| InstantTypeHandler | java.time.Instant | TIMESTAMP |
|
||||
| LocalDateTimeTypeHandler | java.time.LocalDateTime | TIMESTAMP |
|
||||
| LocalDateTypeHandler | java.time.LocalDate | DATE |
|
||||
| LocalTimeTypeHandler | java.time.LocalTime | TIME |
|
||||
| OffsetDateTimeTypeHandler | java.time.OffsetDateTime | TIMESTAMP |
|
||||
| OffsetTimeTypeHandler | java.time.OffsetTime | TIME |
|
||||
| ZonedDateTimeTypeHandler | java.time.ZonedDateTime | TIMESTAMP |
|
||||
| YearTypeHandler | java.time.Year | INTEGER |
|
||||
| MonthTypeHandler | java.time.Month | INTEGER |
|
||||
| YearMonthTypeHandler | java.time.YearMonth | VARCHAR 或 LONGVARCHAR |
|
||||
| JapaneseDateTypeHandler | java.time.chrono.JapaneseDate | DATE |
|
||||
|
||||
### 操作时间相关的工具
|
||||
|
||||
有一些对基础的API进行了封装便于我们在开发中有效的处理时间。
|
||||
|
||||
- 蚂蚁时间工具类:
|
||||
|
||||
```
|
||||
com.iwallet.biz.common.util.DateUtil
|
||||
```
|
||||
|
||||
- 基于 `java.Util.Date`,提供了广泛的日期/时间处理方法,可满足绝大部分需求。
|
||||
|
||||
- ```
|
||||
org.apache.commons.lang3.time
|
||||
```
|
||||
|
||||
- 包括多种基于 `java.util.Date` 封装的工具类,提供了很多方便操作日期和时间的算法。
|
||||
|
||||
目前暂时没有发现基于 `java.time*` 封装的公共的时间工具类。
|
||||
|
||||
在很多情况下,因为已有的工具类不能满足当下的业务需求,工程内部需要自己实现类似 `DateUtil` 的工具类,建议基于 `java.time*` 实现相关的工具类。
|
||||
|
||||
```java
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public class DateUtils {
|
||||
|
||||
// 获取当前日期
|
||||
public static LocalDate getCurrentDate() {
|
||||
return LocalDate.now();
|
||||
}
|
||||
|
||||
// 获取当前时间
|
||||
public static LocalTime getCurrentTime() {
|
||||
return LocalTime.now();
|
||||
}
|
||||
|
||||
// 获取当前日期时间
|
||||
public static LocalDateTime getCurrentDateTime() {
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
|
||||
// 格式化日期为字符串
|
||||
public static String formatLocalDate(LocalDate date, String pattern) {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
|
||||
return date.format(formatter);
|
||||
}
|
||||
|
||||
// 解析字符串为LocalDate
|
||||
public static LocalDate parseLocalDate(String dateStr, String pattern) {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
|
||||
return LocalDate.parse(dateStr, formatter);
|
||||
}
|
||||
|
||||
// 增加指定天数
|
||||
public static LocalDate addDays(LocalDate date, long days) {
|
||||
return date.plusDays(days);
|
||||
}
|
||||
|
||||
// 减少指定天数
|
||||
public static LocalDate minusDays(LocalDate date, long days) {
|
||||
return date.minusDays(days);
|
||||
}
|
||||
|
||||
// 计算两个日期之间的天数差
|
||||
public static long getDaysBetween(LocalDate startDate, LocalDate endDate) {
|
||||
return ChronoUnit.DAYS.between(startDate, endDate);
|
||||
}
|
||||
|
||||
// 获取指定日期所在月份的第一天
|
||||
public static LocalDate getFirstDayOfMonth(LocalDate date) {
|
||||
return date.withDayOfMonth(1);
|
||||
}
|
||||
|
||||
// 获取指定日期所在月份的最后一天
|
||||
public static LocalDate getLastDayOfMonth(LocalDate date) {
|
||||
return date.withDayOfMonth(date.lengthOfMonth());
|
||||
}
|
||||
|
||||
// 判断两个日期是否相等
|
||||
public static boolean isSameDate(LocalDate date1, LocalDate date2) {
|
||||
return date1.isEqual(date2);
|
||||
}
|
||||
|
||||
// 判断日期是否在指定范围内
|
||||
public static boolean isDateInRange(LocalDate date, LocalDate startDate, LocalDate endDate) {
|
||||
return date.isAfter(startDate) && date.isBefore(endDate);
|
||||
}
|
||||
|
||||
// 获取指定日期的星期几
|
||||
public static DayOfWeek getDayOfWeek(LocalDate date) {
|
||||
return date.getDayOfWeek();
|
||||
}
|
||||
|
||||
// 判断是否为闰年
|
||||
public static boolean isLeapYear(int year) {
|
||||
return Year.of(year).isLeap();
|
||||
}
|
||||
|
||||
// 获取指定月份的天数
|
||||
public static int getDaysInMonth(int year, int month) {
|
||||
return YearMonth.of(year, month).lengthOfMonth();
|
||||
}
|
||||
|
||||
// 获取指定日期的年份
|
||||
public static int getYear(LocalDate date) {
|
||||
return date.getYear();
|
||||
}
|
||||
|
||||
// 获取指定日期的月份
|
||||
public static int getMonth(LocalDate date) {
|
||||
return date.getMonthValue();
|
||||
}
|
||||
|
||||
// 获取指定日期的天数
|
||||
public static int getDayOfMonth(LocalDate date) {
|
||||
return date.getDayOfMonth();
|
||||
}
|
||||
|
||||
// 获取指定日期的小时数
|
||||
public static int getHour(LocalDateTime dateTime) {
|
||||
return dateTime.getHour();
|
||||
}
|
||||
|
||||
// 获取指定日期的分钟数
|
||||
public static int getMinute(LocalDateTime dateTime) {
|
||||
return dateTime.getMinute();
|
||||
}
|
||||
|
||||
// 获取指定日期的秒数
|
||||
public static int getSecond(LocalDateTime dateTime) {
|
||||
return dateTime.getSecond();
|
||||
}
|
||||
|
||||
// 判断指定日期是否在当前日期之前
|
||||
public static boolean isBefore(LocalDate date) {
|
||||
return date.isBefore(LocalDate.now());
|
||||
}
|
||||
|
||||
// 判断指定日期是否在当前日期之后
|
||||
public static boolean isAfter(LocalDate date) {
|
||||
return date.isAfter(LocalDate.now());
|
||||
}
|
||||
|
||||
// 判断指定日期是否在当前日期之前或相等
|
||||
public static boolean isBeforeOrEqual(LocalDate date) {
|
||||
return date.isBefore(LocalDate.now()) || date.isEqual(LocalDate.now());
|
||||
}
|
||||
|
||||
// 判断指定日期是否在当前日期之后或相等
|
||||
public static boolean isAfterOrEqual(LocalDate date) {
|
||||
return date.isAfter(LocalDate.now()) || date.isEqual(LocalDate.now());
|
||||
}
|
||||
|
||||
// 获取指定日期的年龄
|
||||
public static int getAge(LocalDate birthDate) {
|
||||
LocalDate currentDate = LocalDate.now();
|
||||
return Period.between(birthDate, currentDate).getYears();
|
||||
}
|
||||
|
||||
// 获取指定日期的季度
|
||||
public static int getQuarter(LocalDate date) {
|
||||
return (date.getMonthValue() - 1) / 3 + 1;
|
||||
}
|
||||
|
||||
// 获取指定日期的下一个工作日
|
||||
public static LocalDate getNextWorkingDay(LocalDate date) {
|
||||
do {
|
||||
date = date.plusDays(1);
|
||||
} while (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY);
|
||||
return date;
|
||||
}
|
||||
|
||||
// 获取指定日期的上一个工作日
|
||||
public static LocalDate getPreviousWorkingDay(LocalDate date) {
|
||||
do {
|
||||
date = date.minusDays(1);
|
||||
} while (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY);
|
||||
return date;
|
||||
}
|
||||
|
||||
// 获取指定日期所在周的第一天(周一)
|
||||
public static LocalDate getFirstDayOfWeek(LocalDate date) {
|
||||
return date.with(DayOfWeek.MONDAY);
|
||||
}
|
||||
|
||||
// 获取指定日期所在周的最后一天(周日)
|
||||
public static LocalDate getLastDayOfWeek(LocalDate date) {
|
||||
return date.with(DayOfWeek.SUNDAY);
|
||||
}
|
||||
|
||||
// 获取指定日期所在年的第一天
|
||||
public static LocalDate getFirstDayOfYear(LocalDate date) {
|
||||
return date.withDayOfYear(1);
|
||||
}
|
||||
|
||||
// 获取指定日期所在年的最后一天
|
||||
public static LocalDate getLastDayOfYear(LocalDate date) {
|
||||
return date.withDayOfYear(date.lengthOfYear());
|
||||
}
|
||||
|
||||
// 获取指定日期所在季度的第一天
|
||||
public static LocalDate getFirstDayOfQuarter(LocalDate date) {
|
||||
int month = (date.getMonthValue() - 1) / 3 * 3 + 1;
|
||||
return LocalDate.of(date.getYear(), month, 1);
|
||||
}
|
||||
|
||||
// 获取指定日期所在季度的最后一天
|
||||
public static LocalDate getLastDayOfQuarter(LocalDate date) {
|
||||
int month = (date.getMonthValue() - 1) / 3 * 3 + 3;
|
||||
return LocalDate.of(date.getYear(), month, Month.of(month).maxLength());
|
||||
}
|
||||
|
||||
// 判断指定日期是否为工作日(周一至周五)
|
||||
public static boolean isWeekday(LocalDate date) {
|
||||
return date.getDayOfWeek() != DayOfWeek.SATURDAY && date.getDayOfWeek() != DayOfWeek.SUNDAY;
|
||||
}
|
||||
|
||||
// 判断指定日期是否为周末(周六或周日)
|
||||
public static boolean isWeekend(LocalDate date) {
|
||||
return date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY;
|
||||
}
|
||||
|
||||
// 获取指定日期所在月份的工作日天数
|
||||
public static int getWeekdayCountOfMonth(LocalDate date) {
|
||||
int weekdayCount = 0;
|
||||
LocalDate firstDayOfMonth = getFirstDayOfMonth(date);
|
||||
LocalDate lastDayOfMonth = getLastDayOfMonth(date);
|
||||
|
||||
while (!firstDayOfMonth.isAfter(lastDayOfMonth)) {
|
||||
if (isWeekday(firstDayOfMonth)) {
|
||||
weekdayCount++;
|
||||
}
|
||||
firstDayOfMonth = firstDayOfMonth.plusDays(1);
|
||||
}
|
||||
|
||||
return weekdayCount;
|
||||
}
|
||||
|
||||
// 获取指定日期所在月份的周末天数
|
||||
public static int getWeekendCountOfMonth(LocalDate date) {
|
||||
int weekendCount = 0;
|
||||
LocalDate firstDayOfMonth = getFirstDayOfMonth(date);
|
||||
LocalDate lastDayOfMonth = getLastDayOfMonth(date);
|
||||
|
||||
while (!firstDayOfMonth.isAfter(lastDayOfMonth)) {
|
||||
if (isWeekend(firstDayOfMonth)) {
|
||||
weekendCount++;
|
||||
}
|
||||
firstDayOfMonth = firstDayOfMonth.plusDays(1);
|
||||
}
|
||||
|
||||
return weekendCount;
|
||||
}
|
||||
|
||||
// 获取指定日期所在年份的工作日天数
|
||||
public static int getWeekdayCountOfYear(LocalDate date) {
|
||||
int weekdayCount = 0;
|
||||
LocalDate firstDayOfYear = getFirstDayOfYear(date);
|
||||
LocalDate lastDayOfYear = getLastDayOfYear(date);
|
||||
|
||||
while (!firstDayOfYear.isAfter(lastDayOfYear)) {
|
||||
if (isWeekday(firstDayOfYear)) {
|
||||
weekdayCount++;
|
||||
}
|
||||
firstDayOfYear = firstDayOfYear.plusDays(1);
|
||||
}
|
||||
|
||||
return weekdayCount;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
Ref:`https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&mid=2247542060&idx=1&sn=ebde870557f2f3002dacef8a43e04bfd`
|
||||
|
70
java/线程.md
Normal file
70
java/线程.md
Normal file
@ -0,0 +1,70 @@
|
||||
```
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class AlternatePrint {
|
||||
// 使用原子整数作为共享变量,初始值为1表示轮到线程1打印第一个数字
|
||||
private static AtomicInteger turn = new AtomicInteger(1);
|
||||
// 当前要打印的数字
|
||||
private static int currentNumber = 1;
|
||||
// 定义终止条件
|
||||
private static final int MAX_NUMBER = 100;
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 创建两个线程
|
||||
Thread thread1 = new Thread(() -> {
|
||||
while (currentNumber <= MAX_NUMBER) {
|
||||
// 线程1检查是否轮到自己
|
||||
if (turn.get() == 1) {
|
||||
System.out.println("线程1: " + currentNumber);
|
||||
// 切换到线程2
|
||||
turn.set(2);
|
||||
}
|
||||
// 短暂休眠以避免CPU过度使用
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Thread thread2 = new Thread(() -> {
|
||||
while (currentNumber <= MAX_NUMBER) {
|
||||
// 线程2检查是否轮到自己
|
||||
if (turn.get() == 2) {
|
||||
System.out.println("线程2: " + currentNumber);
|
||||
// 增加当前数字
|
||||
currentNumber++;
|
||||
// 切换回线程1
|
||||
turn.set(1);
|
||||
}
|
||||
// 短暂休眠
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 启动线程
|
||||
thread1.start();
|
||||
thread2.start();
|
||||
|
||||
// 等待线程结束
|
||||
try {
|
||||
thread1.join();
|
||||
thread2.join();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
System.out.println("所有数字打印完毕");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
@ -129,30 +129,4 @@ mount /dev/sdb1 /Public2
|
||||
|
||||
|
||||
|
||||
### lsblk 命令
|
||||
|
||||
查看硬盘信息
|
||||
```
|
||||
lsblk
|
||||
```
|
||||
```
|
||||
lsblk -f
|
||||
```
|
||||
```
|
||||
[root@worker ~]# lsblk
|
||||
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
|
||||
sda 8:0 0 30G 0 disk
|
||||
├─sda1 8:1 0 200M 0 part /boot/efi
|
||||
├─sda2 8:2 0 1G 0 part /boot
|
||||
└─sda3 8:3 0 28.8G 0 part
|
||||
├─centos-root 253:0 0 27G 0 lvm /
|
||||
└─centos-swap 253:1 0 1.8G 0 lvm [SWAP]
|
||||
sdb 8:16 0 1T 0 disk
|
||||
└─sdb1 8:17 0 1024G 0 part /opt
|
||||
sr0 11:0 1 1024M 0 rom
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -22,27 +22,21 @@
|
||||
|
||||
逻辑结构大概像是这个样子
|
||||
|
||||
```text
|
||||
|----------| |----------|
|
||||
| 逻辑卷 LV | | 逻辑卷...|
|
||||
|----------| |----------|
|
||||
| |
|
||||
\ /
|
||||
|-----------|
|
||||
| 卷组 VG |
|
||||
|-----------|
|
||||
/ \
|
||||
| |
|
||||
|----------| |----------|
|
||||
| 物理卷 PV | | 物理卷...|
|
||||
|----------| |----------|
|
||||
```mermaid
|
||||
graph TB
|
||||
A(逻辑卷 LV) --> B(卷组 VG)
|
||||
C(逻辑卷...) --> B
|
||||
B --> D(物理卷 PV)
|
||||
B --> E(物理卷...)
|
||||
```
|
||||
A(逻辑卷 LV) --> B(卷组 VG)
|
||||
C(逻辑卷...) --> B
|
||||
B --> D[物理卷 PV]
|
||||
B --> E[物理卷...]
|
||||
D --> F[物理区域 PE]
|
||||
D --> G[物理区域...]
|
||||
E --> H[物理区域 PE]
|
||||
E --> I[物理区域...]
|
||||
|
||||
classDef node fill:#f9f,stroke:#333,stroke-width:2px;
|
||||
classDef pe fill:#6af,stroke:#333,stroke-width:1.5px,dashed;
|
||||
class F,G,H,I pe;
|
||||
```
|
||||
> 我们并不需要太过关心 PE
|
||||
|
||||
记好这三层的缩写 我们待会要把这三层定义和创建出来。
|
||||
@ -107,6 +101,7 @@ vgscan
|
||||
|
||||
# 格式化逻辑卷
|
||||
mkfs -t ext4 /dev/vg_demo/lv_demo
|
||||
mkfs.xfs /dev/vg_demo/lv_demo
|
||||
|
||||
# 创建待挂载目录
|
||||
mkdir /demo
|
||||
@ -145,11 +140,35 @@ lvresize -L +20G /dev/vg_demo/lv_demo
|
||||
#### 调整分区大小
|
||||
|
||||
```sh
|
||||
resize2fs /dev/vg_demo/lv_demo
|
||||
# 扩展文件系统(根据实际文件系统类型选择命令)
|
||||
resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv # ext4
|
||||
# 或
|
||||
xfs_growfs /dev/mapper/ubuntu--vg-ubuntu--lv # XFS
|
||||
```
|
||||
|
||||
|
||||
|
||||
### lsblk 命令
|
||||
|
||||
查看硬盘信息
|
||||
```
|
||||
lsblk
|
||||
```
|
||||
```
|
||||
lsblk -f
|
||||
```
|
||||
```
|
||||
[root@worker ~]# lsblk
|
||||
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
|
||||
sda 8:0 0 30G 0 disk
|
||||
├─sda1 8:1 0 200M 0 part /boot/efi
|
||||
├─sda2 8:2 0 1G 0 part /boot
|
||||
└─sda3 8:3 0 28.8G 0 part
|
||||
├─centos-root 253:0 0 27G 0 lvm /
|
||||
└─centos-swap 253:1 0 1.8G 0 lvm [SWAP]
|
||||
sdb 8:16 0 1T 0 disk
|
||||
└─sdb1 8:17 0 1024G 0 part /opt
|
||||
sr0 11:0 1 1024M 0 rom
|
||||
```
|
||||
|
||||
|
||||
[wayne-programming-studio-wiki]: https://wiki-wayne.surge.sh/#/os/lvm "lvm"
|
164
nginx启用njs动态修改请求.md
Normal file
164
nginx启用njs动态修改请求.md
Normal file
@ -0,0 +1,164 @@
|
||||
### nginx 使用njs模块动态修改请求
|
||||
|
||||
|
||||
|
||||
##### 安装编译环境
|
||||
|
||||
```bash
|
||||
#GCC
|
||||
apt install -y build-essential
|
||||
|
||||
#安装正则库
|
||||
apt install -y libpcre3 libpcre3-dev
|
||||
|
||||
#安装zlib库
|
||||
apt install -y zlib1g-dev
|
||||
|
||||
#openssl
|
||||
apt install -y openssl libssl-dev
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 下载源码
|
||||
|
||||
```
|
||||
|
||||
# 创建源码目录
|
||||
mkdir -p ~/build && cd ~/build
|
||||
|
||||
# 下载最新稳定版 Nginx
|
||||
wget https://nginx.org/download/nginx-1.24.0.tar.gz
|
||||
tar -zxvf nginx-1.24.0.tar.gz
|
||||
|
||||
# 下载 njs 模块
|
||||
wget https://github.com/nginx/njs/archive/refs/tags/0.7.11.tar.gz -O njs-0.7.11.tar.gz
|
||||
tar -zxvf njs-0.7.11.tar.gz
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
##### 编译
|
||||
这里采用最小化 默认配置编译
|
||||
|
||||
```
|
||||
cd ~/build/nginx-1.24.0
|
||||
```
|
||||
|
||||
```
|
||||
./configure \
|
||||
--with-http_ssl_module \
|
||||
--with-http_v2_module \
|
||||
--with-http_realip_module \
|
||||
--with-http_gzip_static_module \
|
||||
--add-module=../njs-0.7.11/nginx
|
||||
|
||||
```
|
||||
```
|
||||
make && make install
|
||||
```
|
||||
|
||||
|
||||
##### 配置
|
||||
|
||||
编辑conf/nginx.conf
|
||||
```
|
||||
|
||||
|
||||
worker_processes 1;
|
||||
events {
|
||||
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
include /usr/local/nginx/conf.d/*.conf;
|
||||
include /usr/local/nginx/sites-enabled/*;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
创建子配置目录
|
||||
|
||||
```
|
||||
mkdir /usr/local/nginx/{conf.d,sites-enabled}
|
||||
|
||||
|
||||
```
|
||||
编辑子配置
|
||||
|
||||
```
|
||||
|
||||
js_import /usr/local/nginx/test.js;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /njs {
|
||||
return 200 test.version;
|
||||
}
|
||||
|
||||
location /hello {
|
||||
js_content test.hello;
|
||||
}
|
||||
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
|
||||
root html;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
编辑js 脚本
|
||||
```
|
||||
function hello(r) {
|
||||
r.return(200, "Hello from njs!\n");
|
||||
}
|
||||
|
||||
export default {hello, version: "1.0.0"};
|
||||
```
|
||||
|
||||
|
||||
##### 访问测试
|
||||
|
||||
测试配置是否正确
|
||||
|
||||
```
|
||||
sbin/nginx -t
|
||||
|
||||
```
|
||||
|
||||
启动nginx
|
||||
```
|
||||
sbin/nginx
|
||||
|
||||
```
|
||||
|
||||
访问测试
|
||||
```
|
||||
[root@cdh-node-3 nginx]# curl 127.0.0.1:80/njs
|
||||
test.version[root@cdh-node-3 nginx]# curl 127.0.0.1:80/hello
|
||||
Hello from njs!
|
||||
|
||||
```
|
||||
|
@ -8,151 +8,162 @@ FRR 动态路由软件包 组件
|
||||
|
||||
|
||||
|
||||
### 1. `frr`
|
||||
|
||||
|
||||
### FRR 支持的协议
|
||||
|
||||
1. `frr`
|
||||
|
||||
这是 FRRouting 的核心包,包含了 FRR 的基础框架和多个路由协议守护进程(如 RIP、OSPF、BGP 等)。安装该包后,你可以运行 FRR 的所有路由协议服务和工具。
|
||||
|
||||
### 2. `frr-babeld`
|
||||
2. `frr-babeld`
|
||||
|
||||
Babel 是一种适用于 IP 网络的动态路由协议。这个包安装的是 **Babel 路由协议守护进程**,它用于支持 Babel 协议。Babel 是一种自适应的距离矢量协议,适用于小型到中型的网络。
|
||||
|
||||
- **作用**:用于动态路由发现和更新,特别适用于无线网络和 ad-hoc 网络。
|
||||
用于动态路由发现和更新,特别适用于无线网络和 ad-hoc 网络。
|
||||
|
||||
### 3. `frr-bfdd`
|
||||
3. `frr-bfdd`
|
||||
|
||||
BFDD(BFD:Bidirectional Forwarding Detection)是一种快速检测路径故障的协议。这个包提供了 **BFD 守护进程**,可以帮助快速发现路由器之间的连接故障,并根据路由协议快速进行故障恢复。
|
||||
|
||||
- **作用**:用于加速故障检测,并使路由协议更快地重新计算路径。
|
||||
用于加速故障检测,并使路由协议更快地重新计算路径。
|
||||
|
||||
### 4. `frr-bgpd`
|
||||
4. `frr-bgpd`
|
||||
|
||||
BGP(Border Gateway Protocol)是 Internet 上主要的外部网关协议。该包安装的是 **BGP 路由协议守护进程**,用于 BGP 配置和管理,支持 IPv4 和 IPv6 的路由交换。
|
||||
|
||||
- **作用**:用于与其他自治系统(AS)交换路由信息,常用于大规模网络(如互联网骨干网络)中的路由控制。
|
||||
用于与其他自治系统(AS)交换路由信息,常用于大规模网络(如互联网骨干网络)中的路由控制。
|
||||
|
||||
### 5. `frr-eigrpd`
|
||||
5. `frr-eigrpd`
|
||||
|
||||
EIGRP(Enhanced Interior Gateway Routing Protocol)是 Cisco 开发的一种内部网关协议(IGP)。这个包安装的是 **EIGRP 路由协议守护进程**。
|
||||
|
||||
- **作用**:用于在同一自治系统内交换路由信息,EIGRP 是 Cisco 特有的协议,适用于企业网络。
|
||||
用于在同一自治系统内交换路由信息,EIGRP 是 Cisco 特有的协议,适用于企业网络。
|
||||
|
||||
### 6. `frr-fabricd`
|
||||
6. `frr-fabricd`
|
||||
|
||||
这是 **Fabric 路由协议守护进程**,用于支持 **数据中心网络中的 Fabric 路由协议**,主要用于网络设备间的直接连接,简化网络架构和拓扑管理。
|
||||
|
||||
- **作用**:通常用于大型数据中心的网络结构中,提供更高效的流量路由和管理。
|
||||
通常用于大型数据中心的网络结构中,提供更高效的流量路由和管理。
|
||||
|
||||
### 7. `frr-isisd`
|
||||
7. `frr-isisd`
|
||||
|
||||
IS-IS(Intermediate System to Intermediate System)是一种内部网关协议(IGP),主要用于大规模的服务提供商网络。这个包安装的是 **IS-IS 路由协议守护进程**。
|
||||
|
||||
- **作用**:用于在大型网络中进行高效的路由选择,IS-IS 主要在运营商和大规模网络中使用。
|
||||
用于在大型网络中进行高效的路由选择,IS-IS 主要在运营商和大规模网络中使用。
|
||||
|
||||
### 8. `frr-ldpd`
|
||||
8. `frr-ldpd`
|
||||
|
||||
LDP(Label Distribution Protocol)用于 MPLS 网络中,帮助路由器之间分配标签。该包安装的是 **LDP 守护进程**。
|
||||
|
||||
- **作用**:在 MPLS 网络中,帮助管理标签分发和路由决策,优化数据包的转发路径。
|
||||
在 MPLS 网络中,帮助管理标签分发和路由决策,优化数据包的转发路径。
|
||||
|
||||
### 9. `frr-libfrr`
|
||||
9. `frr-libfrr`
|
||||
|
||||
这是 **FRR 的库包**,它提供了 FRR 所需的共享库。安装该包可以为其他 FRR 组件提供支持。
|
||||
|
||||
- **作用**:作为 FRR 的核心库文件,其他 FRR 组件依赖于它来提供基本的路由处理功能。
|
||||
作为 FRR 的核心库文件,其他 FRR 组件依赖于它来提供基本的路由处理功能。
|
||||
|
||||
### 10. `frr-nhrpd`
|
||||
10. `frr-nhrpd`
|
||||
|
||||
NHRP(Next Hop Resolution Protocol)是用于 IP 网络中查找下一跳信息的协议,特别适用于 VPN 和 IP 网络互联。这个包安装的是 **NHRP 守护进程**。
|
||||
|
||||
- **作用**:用于基于 NHRP 协议查找 IP 地址的下一跳,通常用于 VPN 和其他隧道协议中。
|
||||
用于基于 NHRP 协议查找 IP 地址的下一跳,通常用于 VPN 和其他隧道协议中。
|
||||
|
||||
### 11. `frr-ospf6d`
|
||||
11. `frr-ospf6d`
|
||||
|
||||
OSPFv3(Open Shortest Path First version 3)是用于 IPv6 网络的路由协议。这个包安装的是 **OSPFv3 路由协议守护进程**。
|
||||
|
||||
- **作用**:提供对 IPv6 网络的路由支持,OSPFv3 是 OSPF 的扩展版本,支持 IPv6 地址族。
|
||||
提供对 IPv6 网络的路由支持,OSPFv3 是 OSPF 的扩展版本,支持 IPv6 地址族。
|
||||
|
||||
### 12. `frr-ospfd`
|
||||
12. `frr-ospfd`
|
||||
|
||||
OSPF(Open Shortest Path First)是一个广泛使用的内部网关协议(IGP),它使用链路状态协议来计算最佳路径。这个包安装的是 **OSPF 路由协议守护进程**。
|
||||
|
||||
- **作用**:用于基于链路状态协议的路由计算,适用于大型企业网络中的路由配置。
|
||||
用于基于链路状态协议的路由计算,适用于大型企业网络中的路由配置。
|
||||
|
||||
### 13. `frr-pbrd`
|
||||
13. `frr-pbrd`
|
||||
|
||||
PBR(Policy-Based Routing)是基于策略的路由协议,允许根据数据包的内容(如源 IP、目标 IP 等)决定路由路径。这个包安装的是 **PBR 守护进程**。
|
||||
|
||||
- **作用**:提供基于策略的路由决策,允许对流量进行更细粒度的控制。
|
||||
提供基于策略的路由决策,允许对流量进行更细粒度的控制。
|
||||
|
||||
### 14. `frr-pimd`
|
||||
14. `frr-pimd`
|
||||
|
||||
PIM(Protocol Independent Multicast)是用于组播路由的协议,适用于需要多播的应用。这个包安装的是 **PIM 守护进程**。
|
||||
|
||||
- **作用**:为网络提供组播支持,适用于需要组播(如视频流、广播等)的网络应用。
|
||||
为网络提供组播支持,适用于需要组播(如视频流、广播等)的网络应用。
|
||||
|
||||
### 15. `frr-ripd`
|
||||
15. `frr-ripd`
|
||||
|
||||
RIP(Routing Information Protocol)是一个距离矢量协议,适用于小型或中型的网络。这个包安装的是 **RIP 路由协议守护进程**。
|
||||
|
||||
- **作用**:实现 RIP 协议,进行路由信息的传播,适用于简单的内部网络。
|
||||
实现 RIP 协议,进行路由信息的传播,适用于简单的内部网络。
|
||||
|
||||
### 16. `frr-ripngd`
|
||||
16. `frr-ripngd`
|
||||
|
||||
RIPng(RIP next generation)是 RIP 协议的扩展,支持 IPv6 网络。这个包安装的是 **RIPng 路由协议守护进程**。
|
||||
|
||||
- **作用**:提供 RIP 协议的 IPv6 支持,适用于 IPv6 网络中的动态路由。
|
||||
提供 RIP 协议的 IPv6 支持,适用于 IPv6 网络中的动态路由。
|
||||
|
||||
### 17. `frr-staticd`
|
||||
17. `frr-staticd`
|
||||
|
||||
Staticd 组件用于管理 **静态路由**,这些路由是手动配置的,并不通过动态路由协议传播。这个包安装的是 **静态路由守护进程**。
|
||||
|
||||
- **作用**:管理静态路由的配置,适用于不需要动态路由的网络部分。
|
||||
管理静态路由的配置,适用于不需要动态路由的网络部分。
|
||||
|
||||
### 18. `frr-vrrpd`
|
||||
18. `frr-vrrpd`
|
||||
|
||||
VRRP(Virtual Router Redundancy Protocol)是一个用于提供虚拟网关冗余的协议。这个包安装的是 **VRRP 守护进程**。
|
||||
|
||||
- **作用**:提供网关冗余服务,保证网络的高可用性,当主网关故障时,自动切换到备用网关。
|
||||
提供网关冗余服务,保证网络的高可用性,当主网关故障时,自动切换到备用网关。
|
||||
|
||||
### 19. `frr-vtysh`
|
||||
19. `frr-vtysh`
|
||||
|
||||
VTYSH 是 **FRR 的命令行界面工具**,用于与 FRR 进程进行交互,配置和管理各种路由协议。
|
||||
|
||||
- **作用**:提供一个统一的命令行界面(CLI)来管理 FRR 配置,包括所有路由协议和 Zebra。
|
||||
提供一个统一的命令行界面(CLI)来管理 FRR 配置,包括所有路由协议和 Zebra。
|
||||
|
||||
### 20. `frr-watchfrr`
|
||||
20. `frr-watchfrr`
|
||||
|
||||
Watchfrr 组件是一个监控工具,用于检测 FRR 路由协议进程的健康状态并进行管理。
|
||||
|
||||
- **作用**:监控 FRR 各个路由协议守护进程的运行状态,确保路由协议的稳定性。
|
||||
监控 FRR 各个路由协议守护进程的运行状态,确保路由协议的稳定性。
|
||||
|
||||
### 21. `frr-zebra`
|
||||
21. `frr-zebra`
|
||||
|
||||
Zebra 是 FRR 的核心组件之一,负责将路由协议的路由信息传递给内核,并管理网络接口和静态路由。这个包安装的是 **Zebra 路由守护进程**。
|
||||
|
||||
- **作用**:与操作系统内核进行交互,管理路由表、接口配置等。
|
||||
与操作系统内核进行交互,管理路由表、接口配置等。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
rip 配置
|
||||
|
||||
|
||||
### rip 配置
|
||||
|
||||
```bash
|
||||
#协议
|
||||
router rip
|
||||
#在这个网段上广播
|
||||
network 192.168.123.0/24
|
||||
network 192.168.192.0/24
|
||||
|
||||
#将这个路由广播出去
|
||||
route 192.168.1.0/24
|
||||
|
||||
no passive-interface eth0 # 启用 eth0 接口的 RIP 收发
|
||||
passive-interface br-lan # 禁用 br-lan 接口上的 发送,可以接收。
|
||||
|
||||
!
|
||||
|
||||
#指定这个接口上协议版本
|
||||
interface eth0
|
||||
# send 发送的协议
|
||||
ip rip send version 2
|
||||
# receive 接收的协议
|
||||
ip rip receive version 2
|
||||
!
|
||||
|
||||
#在这个接口上广播路由
|
||||
interface eth1
|
||||
ip rip send version 2
|
||||
ip rip receive version 2
|
||||
@ -160,43 +171,41 @@ interface eth1
|
||||
|
||||
|
||||
|
||||
第一段申明一个rip 路由
|
||||
### 常用命令
|
||||
|
||||
宣告两个子网
|
||||
```bash
|
||||
|
||||
```
|
||||
network 192.168.123.0/24
|
||||
network 192.168.192.0/24
|
||||
#静态路由
|
||||
route 10.147.17.0/24
|
||||
# 全全局配置
|
||||
configure terminal
|
||||
|
||||
#在全局配置下进行RIP 配置
|
||||
route rip
|
||||
|
||||
#配置好后 输入 end 结束配置, write 保存配置
|
||||
end
|
||||
write
|
||||
|
||||
|
||||
|
||||
#查看rip 状态
|
||||
show ip rip status
|
||||
|
||||
#查看路由
|
||||
show ip route
|
||||
|
||||
#查看RIP 路由
|
||||
show ip route rip
|
||||
```
|
||||
|
||||
|
||||
|
||||
申请监听来自那个接口的 rip 路由包
|
||||
|
||||
```
|
||||
interface eth0
|
||||
ip rip send version 2
|
||||
ip rip receive version 2
|
||||
!
|
||||
```
|
||||
|
||||
|
||||
`show ip rip` 命令输出的内容主要显示了 RIP 协议的路由表信息,包括每个网络的下一跳、度量值、路由来源等。下面是对输出的逐行解析:
|
||||
|
||||
`ip rip send version 2` 发送V2协议包
|
||||
**输出内容**
|
||||
|
||||
`ip rip receive version 2` 接收V2协议包
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
根据你提供的 `show ip rip` 命令输出,可以逐行分析每条路由的意义。输出的内容主要显示了 RIP 协议的路由表信息,包括每个网络的下一跳、度量值、路由来源等。下面是对输出的逐行解析:
|
||||
|
||||
### 输出内容
|
||||
|
||||
```
|
||||
```bash
|
||||
Codes: R - RIP, C - connected, S - Static, O - OSPF, B - BGP
|
||||
Sub-codes:
|
||||
(n) - normal, (s) - static, (d) - default, (r) - redistribute,
|
||||
@ -209,7 +218,7 @@ R(n) 192.168.124.0/24 10.147.17.219 2 10.147.17.219 0 02:27
|
||||
C(i) 192.168.192.0/24 0.0.0.0 1 self 0
|
||||
```
|
||||
|
||||
### **Codes 和 Sub-codes 说明**
|
||||
**Codes 和 Sub-codes 说明**
|
||||
|
||||
- **Codes**: 每条路由的类型。
|
||||
- `R`:RIP协议路由
|
||||
@ -224,23 +233,31 @@ C(i) 192.168.192.0/24 0.0.0.0 1 self 0
|
||||
- `(r)`:由其他协议重分发的路由
|
||||
- `(i)`:接口路由(指向接口的路由)
|
||||
|
||||
### 路由信息解析
|
||||
**路由信息解析**
|
||||
|
||||
#### 1. **C(i) 10.147.17.0/24**
|
||||
**1. C(i) 10.147.17.0/24**
|
||||
|
||||
```bash
|
||||
C(i) 10.147.17.0/24 0.0.0.0 1 self 0
|
||||
```
|
||||
|
||||
- **C(i)**:这是一条 "连接路由",即直接连接的网络。 `(i)` 表示它是一个通过接口直接连接的网络。
|
||||
|
||||
- **10.147.17.0/24**:这是网络地址,表示该设备有一个直接连接到 `10.147.17.0/24` 子网的接口。
|
||||
|
||||
- **Next Hop**:`0.0.0.0`,表示这是一个直接连接的网络,因此不需要下一跳地址。
|
||||
|
||||
- **Metric**:度量值为 `1`,表示该路由的度量值,这里是直接连接的网络,度量值是最小的。
|
||||
|
||||
- **From**:`self`,表示该路由是从本设备的接口直接学习到的。
|
||||
|
||||
- **Tag**:`0`,这是一个路由标签,用于标识或分类路由。
|
||||
|
||||
- **Time**:`0`,表示此路由是立即学习到的,没有过期时间。
|
||||
|
||||
#### 2. **C(i) 192.168.123.0/24**
|
||||
|
||||
|
||||
**2. C(i) 192.168.123.0/24**
|
||||
|
||||
```bash
|
||||
C(i) 192.168.123.0/24 0.0.0.0 1 self 0
|
||||
@ -248,27 +265,38 @@ C(i) 192.168.123.0/24 0.0.0.0 1 self 0
|
||||
|
||||
- 这条路由的解析与第一条相似,是一个直接连接的网络 `192.168.123.0/24`,度量值为 `1`,并且由本机接口学习到。
|
||||
|
||||
#### 3. **R(n) 192.168.124.0/24**
|
||||
|
||||
|
||||
**3. R(n) 192.168.124.0/24**
|
||||
|
||||
```bash
|
||||
R(n) 192.168.124.0/24 10.147.17.219 2 10.147.17.219 0 02:27
|
||||
```
|
||||
|
||||
- **R(n)**:这是一个由 RIP 协议学习到的路由,`(n)` 表示正常的 RIP 路由。
|
||||
|
||||
- **192.168.124.0/24**:目标网络是 `192.168.124.0/24`,这是一个通过 RIP 协议学到的路由。
|
||||
|
||||
- **Next Hop**:`10.147.17.219`,表示该路由的下一跳地址是 `10.147.17.219`,即该路由是通过 `10.147.17.219` 路由器到达的。
|
||||
|
||||
- **Metric**:度量值为 `2`,表示到达该网络的路径的跳数或成本,`2` 表示通过该路径的跳数为 2。
|
||||
|
||||
- **From**:`10.147.17.219`,表示该路由是从 `10.147.17.219` 路由器学到的。
|
||||
|
||||
- **Tag**:`0`,路由标签。
|
||||
|
||||
- **Time**:`02:27`,表示该路由自学习以来已经存在了 2 分 27 秒。
|
||||
|
||||
#### 4. **C(i) 192.168.192.0/24**
|
||||
|
||||
|
||||
**4. C(i) 192.168.192.0/24**
|
||||
|
||||
```bash
|
||||
C(i) 192.168.192.0/24 0.0.0.0 1 self 0
|
||||
```
|
||||
|
||||
- 这条路由表示 `192.168.192.0/24` 是通过本机接口直接连接的网络,度量值为 `1`。
|
||||
-
|
||||
|
||||
### 总结
|
||||
|
||||
@ -277,71 +305,3 @@ C(i) 192.168.192.0/24 0.0.0.0 1 self 0
|
||||
- 所有路由的下一跳信息和度量值都提供了路由信息的详细描述,表明如何到达不同的网络。
|
||||
|
||||
这些路由信息可以帮助你了解 FRR 当前的 RIP 路由表,具体到哪些路由是直接连接的(如 `C(i)` 路由)以及哪些路由是通过 RIP 协议学习到的(如 `R(n)` 路由)。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
如果你希望在 FRR 中 **指定具体的接口** 上启用 RIP 协议并广播路由信息,你可以通过 `interface` 命令来配置。与 `network` 命令不同,`interface` 命令可以直接指定哪些接口参与 RIP,而不必依赖于网络地址范围。
|
||||
|
||||
### 使用 `interface` 命令指定接口
|
||||
|
||||
在 FRR 中,如果你想在具体的接口上启用 RIP 协议并广播路由信息,步骤如下:
|
||||
|
||||
1. **进入 RIP 配置模式**: 你首先需要进入 RIP 配置模式:
|
||||
|
||||
```plaintext
|
||||
router rip
|
||||
```
|
||||
|
||||
2. **使用 `interface` 命令指定接口**: 你可以使用 `interface` 命令来为特定接口启用 RIP。比如,如果你想在接口 `eth0` 和 `tun0` 上启用 RIP,配置如下:
|
||||
|
||||
```plaintext
|
||||
router rip
|
||||
network 192.168.123.0 # 启用 RIP 广播 192.168.123.0 网络
|
||||
network 10.147.17.0 # 启用 RIP 广播 10.147.17.0 网络
|
||||
|
||||
# 通过 interface 命令启用 RIP
|
||||
interface eth0
|
||||
ip rip enable # 在 eth0 上启用 RIP
|
||||
|
||||
interface tun0
|
||||
ip rip enable # 在 tun0 上启用 RIP
|
||||
```
|
||||
|
||||
上面的配置会确保 `eth0` 和 `tun0` 接口参与 RIP 协议。
|
||||
|
||||
### 另一种方法:通过 `passive-interface` 或 `no passive-interface`
|
||||
|
||||
如果你只希望在某些接口上启用 RIP 而在其他接口上禁用,你可以使用 `passive-interface` 或 `no passive-interface` 命令。
|
||||
|
||||
- **`no passive-interface`**:启用该接口上的 RIP 协议。
|
||||
- **`passive-interface`**:禁用该接口上的 RIP 协议(该接口不会发送 RIP 更新,但会接收 RIP 更新)。
|
||||
|
||||
例如:
|
||||
|
||||
```plaintext
|
||||
router rip
|
||||
network 192.168.123.0 # 启用 RIP 广播 192.168.123.0 网络
|
||||
network 10.147.17.0 # 启用 RIP 广播 10.147.17.0 网络
|
||||
|
||||
# 默认情况下,所有接口都可能是 passive(禁用 RIP),需要手动启用
|
||||
no passive-interface eth0 # 启用 eth0 接口的 RIP
|
||||
no passive-interface tun0 # 启用 tun0 接口的 RIP
|
||||
|
||||
passive-interface br-lan # 禁用 br-lan 接口上的 RIP
|
||||
```
|
||||
|
||||
### 配置的解释:
|
||||
|
||||
- `no passive-interface`:启用该接口的 RIP 协议,可以发送和接收 RIP 更新。
|
||||
- `passive-interface`:禁用该接口的 RIP 协议,只接收 RIP 更新,而不会发送。
|
||||
|
||||
### 总结:
|
||||
|
||||
1. 使用 `interface` 命令来指定具体的接口启用 RIP。
|
||||
2. 使用 `no passive-interface` 来启用接口上的 RIP,使用 `passive-interface` 来禁用接口上的 RIP 更新。
|
||||
|
||||
这种方式可以确保你有更多的控制,确保 RIP 只在你指定的接口上广播。
|
29
openwrt/openVPN.md
Normal file
29
openwrt/openVPN.md
Normal file
@ -0,0 +1,29 @@
|
||||
## openWrt 路由器中的 openVPN 组件
|
||||
|
||||
| **连接类型** | 配置文件说明 | **模式** | **网络类型** | **适用场景** | **对应配置文件示例** |
|
||||
| :-------------------- | ----------------------------------------------------------- | :------- | :---------------- | :----------------------------------------------------------- | :------------------- |
|
||||
| **client_tap_bridge** | Client configuration for an ethernet bridge VPN | 客户端 | TAP(以太网桥接) | 客户端需完全接入服务器局域网(如访问 SMB 共享、网络打印机等) | `client-bridge.conf` |
|
||||
| **client_tun** | Client configuration for a routed multi-client VPN | 客户端 | TUN(IP 路由) | 标准多客户端 VPN,适用于远程办公、访问内网服务 | `client-routed.conf` |
|
||||
| **client_tun_ptp** | Simple client configuration for a routed point-to-point VPN | 客户端 | TUN(点对点路由) | 仅 1 对 1 连接,适合设备间专用加密隧道(如远程管理) | `client-p2p.conf` |
|
||||
| **server_tap_bridge** | Server configuration for an ethernet bridge VPN | 服务器 | TAP(以太网桥接) | 服务器提供桥接模式 VPN,客户端像本地设备一样访问整个 LAN | `server-bridge.conf` |
|
||||
| **server_tun** | Server configuration for a routed multi-client VPN | 服务器 | TUN(IP 路由) | 标准多客户端 VPN 服务器,适用于企业远程访问 | `server-routed.conf` |
|
||||
| **server_tun_ptp** | Simple server configuration for a routed point-to-point VPN | 服务器 | TUN(点对点路由) | 仅支持 1 对 1 连接的 VPN 服务器(如站点间 VPN) | `server-p2p.conf` |
|
||||
|
||||
### **关键说明**:
|
||||
|
||||
1. **配置文件名**:
|
||||
- 通常 OpenVPN 会根据 `option value` 生成对应的配置文件(如 `client_tun` → `client-routed.conf`)。
|
||||
- 实际文件名可能因系统不同有所变化,但逻辑一致。
|
||||
2. **TAP vs. TUN**:
|
||||
- **TAP(`tap_bridge`)**:模拟以太网设备,适合需要广播/组播的应用(如网络游戏、局域网发现)。
|
||||
- **TUN(`tun`)**:仅路由 IP 流量,更高效,适合大多数 VPN(如网页访问、SSH)。
|
||||
3. **多客户端 vs. 点对点(P2P)**:
|
||||
- **普通模式(`server_tun`/`client_tun`)**:支持多个客户端同时连接。
|
||||
- **点对点(`_ptp`)**:仅限两个节点直接通信,延迟更低。
|
||||
|
||||
### **典型应用**:
|
||||
|
||||
- **企业远程办公** → `server_tun` + `client_tun`
|
||||
- **家庭局域网扩展** → `server_tap_bridge` + `client_tap_bridge`
|
||||
- **服务器间加密通道** → `server_tun_ptp` + `client_tun_ptp`
|
||||
|
@ -4,28 +4,6 @@
|
||||
|
||||
个人使用ZeroTier体验不错,
|
||||
|
||||
|
||||
|
||||
### 名词解释
|
||||
|
||||
**ZeroTier**
|
||||
|
||||
自行了解
|
||||
|
||||
**rip**
|
||||
|
||||
rip 是一种动态路由协议 可以自动学习路由
|
||||
|
||||
**frr**
|
||||
|
||||
frr 是一个 实现了 rip bgp 等多种 路由协议的软件包,同类型的还有 Quagga 等
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 网络拓扑图
|
||||
|
||||
```mermaid
|
||||
|
@ -1,4 +1,4 @@
|
||||
### oracle 数据库 job 操作
|
||||
## oracle 数据库 job 操作
|
||||
|
||||
|
||||
|
||||
@ -58,3 +58,61 @@ BEGIN
|
||||
END;
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 关闭sesseion
|
||||
|
||||
```
|
||||
SELECT
|
||||
s.sid,
|
||||
s.serial#,
|
||||
s.username,
|
||||
s.osuser,
|
||||
s.machine,
|
||||
s.program,
|
||||
s.sql_id,
|
||||
l.type,
|
||||
l.lmode,
|
||||
l.request
|
||||
FROM
|
||||
v$session s
|
||||
JOIN v$lock l ON s.sid = l.sid
|
||||
WHERE
|
||||
l.id1 = (SELECT object_id FROM dba_objects WHERE object_name = 'MKT_TEST')
|
||||
AND l.type = 'TM'; -- TM 锁表示表锁
|
||||
|
||||
|
||||
|
||||
SELECT
|
||||
s.sid,
|
||||
s.serial#,
|
||||
s.username,
|
||||
s.osuser,
|
||||
s.machine,
|
||||
s.program,
|
||||
s.sql_id,
|
||||
q.sql_text
|
||||
FROM
|
||||
v$session s
|
||||
JOIN v$sql q ON s.sql_id = q.sql_id
|
||||
WHERE
|
||||
s.sid =250 -- 替换为实际的 SID
|
||||
AND s.serial# = 53879; -- 替换为实际的 SERIAL#
|
||||
|
||||
--关闭
|
||||
ALTER SYSTEM KILL SESSION '250,53879';
|
||||
--强制关闭
|
||||
ALTER SYSTEM DISCONNECT SESSION '250,53879' IMMEDIATE;
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 数据库重启
|
||||
|
||||
sqlplus / as sysdba
|
||||
SQL> shutdown immediate;
|
||||
SQL> startup;
|
||||
|
120
postgresql_and_edb/edb表空间查询.md
Normal file
120
postgresql_and_edb/edb表空间查询.md
Normal file
@ -0,0 +1,120 @@
|
||||
```sql
|
||||
SELECT nspname || '.' || relname AS "relation",
|
||||
pg_size_pretty(pg_total_relation_size(C.oid)) AS "total_size"
|
||||
FROM pg_class C
|
||||
LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
|
||||
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
AND C.relkind = 'r'
|
||||
ORDER BY pg_total_relation_size(C.oid) DESC;
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```sql
|
||||
SELECT schemaname || '.' || relname AS "relation",
|
||||
pg_size_pretty(pg_total_relation_size(relid)) AS "total_size",
|
||||
pg_size_pretty(pg_table_size(relid) - pg_indexes_size(relid)) AS "table_size",
|
||||
pg_size_pretty(pg_indexes_size(relid)) AS "index_size",
|
||||
n_dead_tup
|
||||
FROM pg_stat_user_tables
|
||||
WHERE n_dead_tup > 0
|
||||
ORDER BY n_dead_tup DESC;
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
```sql
|
||||
|
||||
SELECT
|
||||
table_schema || '.' || table_name AS 完整表名,
|
||||
table_type AS 表类型,
|
||||
table_catalog AS 数据库名,
|
||||
-- 使用 format() 函数正确引用表名
|
||||
pg_size_pretty(pg_relation_size(format('%I.%I', table_schema, table_name))) AS 数据大小,
|
||||
pg_size_pretty(pg_indexes_size(format('%I.%I', table_schema, table_name))) AS 索引大小,
|
||||
pg_size_pretty(pg_total_relation_size(format('%I.%I', table_schema, table_name))) AS 总大小,
|
||||
pg_total_relation_size(format('%I.%I', table_schema, table_name)) AS 总字节数
|
||||
FROM
|
||||
information_schema.tables
|
||||
WHERE
|
||||
table_type IN ('BASE TABLE', 'PARTITIONED TABLE')
|
||||
AND table_schema NOT IN ('pg_catalog', 'information_schema', 'edb_sys')
|
||||
ORDER BY
|
||||
总字节数 DESC;
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
```sql
|
||||
|
||||
SELECT
|
||||
pid,
|
||||
relid::regclass AS table_name,
|
||||
phase,
|
||||
heap_blks_total,
|
||||
heap_blks_scanned,
|
||||
heap_blks_scanned/heap_blks_total rate,
|
||||
heap_blks_vacuumed,
|
||||
index_vacuum_count,
|
||||
max_dead_tuples,
|
||||
num_dead_tuples
|
||||
FROM
|
||||
pg_stat_progress_vacuum;
|
||||
|
||||
SELECT
|
||||
pid,
|
||||
usename,
|
||||
datname,
|
||||
query,
|
||||
state,
|
||||
query_start,
|
||||
now() - query_start AS running_time
|
||||
FROM
|
||||
pg_stat_activity
|
||||
WHERE
|
||||
query ILIKE '%VACUUM%'
|
||||
ORDER BY
|
||||
query_start;
|
||||
|
||||
|
||||
SELECT
|
||||
schemaname,
|
||||
relname,
|
||||
n_live_tup AS 活元组数量,
|
||||
n_dead_tup AS 死元组数量,
|
||||
round(100 * n_dead_tup / (n_live_tup + 1), 2) AS 死元组占比
|
||||
FROM pg_stat_user_tables
|
||||
WHERE n_live_tup > 0
|
||||
AND round(100 * n_dead_tup / (n_live_tup + 1), 2) > 20 -- 死元组占比超20%
|
||||
ORDER BY 死元组占比 DESC;
|
||||
|
||||
|
||||
|
||||
SELECT
|
||||
schemaname,
|
||||
relname AS 索引名,
|
||||
pg_size_pretty(pg_relation_size(relid)) AS 索引大小,
|
||||
idx_scan AS 扫描次数, -- EDB 中直接通过 pg_stat_user_indexes 的 idx_scan 字段获取扫描次数
|
||||
-- 索引膨胀率(索引实际大小 / 有效数据大小,>1.5 表示碎片严重)
|
||||
round(
|
||||
pg_relation_size(relid)::numeric /
|
||||
pg_indexes_size(relid::regclass)::numeric, -- 修正类型转换,去掉多余的 text 转换
|
||||
2
|
||||
) AS 膨胀率
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE
|
||||
pg_indexes_size(relid::regclass) > 0 -- 排除无效索引
|
||||
AND round(
|
||||
pg_relation_size(relid)::numeric /
|
||||
pg_indexes_size(relid::regclass)::numeric,
|
||||
2
|
||||
) > 1.5 -- 筛选膨胀率 >1.5 的索引
|
||||
ORDER BY 膨胀率 DESC;
|
||||
|
||||
```
|
68
postgresql_and_edb/一次数据库硬盘扩容.md
Normal file
68
postgresql_and_edb/一次数据库硬盘扩容.md
Normal file
@ -0,0 +1,68 @@
|
||||
## 数据库硬盘扩容
|
||||
|
||||
### 前言
|
||||
|
||||
数据库硬盘不够用了 ,数据分区使用的普通分区,
|
||||
|
||||
新加一块硬盘 和原来的一块硬盘 组成lvm 分区
|
||||
|
||||
|
||||
|
||||
### 备份数据
|
||||
|
||||
1. 停机数据
|
||||
|
||||
2. 备份数据库文件,这里采用tar 配合zstd 压缩归档
|
||||
|
||||
`tar -I zstd -cvf archive.tar.zst /data/`
|
||||
|
||||
### lvm卷组创建
|
||||
|
||||
#### 一、卸载相关操作
|
||||
|
||||
1. `lsof +D /data` # 查看/data目录下打开的文件
|
||||
2. `umount /data` # 卸载/data挂载点
|
||||
3. `df -hT /data` #确定卸载情况
|
||||
|
||||
|
||||
#### 二、磁盘及分区查看
|
||||
1. `df -lh` # 查看磁盘分区的使用情况
|
||||
2. `fdisk /dev/nvme0n1p1` # 对nvme0n1p1分区进行操作(如查看、分区等)
|
||||
3. 使用 fdisk 删除 原有分区(备份好数据)
|
||||
|
||||
|
||||
#### 三、LVM相关配置([详情看](/linux/linux使用lvm.md))
|
||||
1. `pvcreate /dev/nvme0n1 /dev/sdb` # 创建物理卷(可以为分区)
|
||||
2. `pvdisplay` # 显示物理卷的信息
|
||||
3. `vgcreate vg_data /dev/nvme0n1 /dev/sdb` # 创建卷组vg_data
|
||||
4. `vgdisplay` # 显示卷组的信息
|
||||
5. `lvcreate -l 100%FREE -n lv_data_all vg_data` # 在vg_data卷组上创建逻辑卷lv_data_all,使用全部空闲空间
|
||||
6. `lvdisplay /dev/vg_data/lv_data_all` # 显示vg_data卷组下lv_data_all逻辑卷的信息
|
||||
|
||||
|
||||
#### 四、文件系统及挂载配置
|
||||
1. `mkfs.xfs /dev/vg_data/lv_data_all` # 为逻辑卷创建xfs文件系统
|
||||
3. `mkdir /data` # 创建/data目录作为挂载点(原有目录存在则不创建)
|
||||
4. `mount /dev/vg_data/lv_data_all /data` # 将逻辑卷挂载到/data目录
|
||||
5. `df -lh` # 查看挂载后的磁盘使用情况( 如不生效则配置fstab重启)
|
||||
|
||||
|
||||
#### 五、自动挂载配置及重启
|
||||
1. `vim /etc/fstab` # 编辑fstab文件,配置开机自动挂载
|
||||
2. `/dev/vg_data/lv_data_all /data xfs defaults 0 0 `
|
||||
3. `mount -a` # 挂载fstab文件中所有未挂载的文件系统,验证配置是否正确
|
||||
4. `reboot` # 重启系统
|
||||
|
||||
### 数据恢复
|
||||
|
||||
提供两种方法 恢复文件
|
||||
|
||||
```
|
||||
zstd -d archive.tar.zst && tar -xvf archive.tar -C /path/to/extract
|
||||
```
|
||||
|
||||
```
|
||||
zstd -dc archive.tar.zst | tar -xvf -
|
||||
```
|
||||
|
||||
启动数据库
|
BIN
resources/java/oauth2-server.assets/PhquBVnerb.webp
(Stored with Git LFS)
Normal file
BIN
resources/java/oauth2-server.assets/PhquBVnerb.webp
(Stored with Git LFS)
Normal file
Binary file not shown.
414
私有CA.md
Normal file
414
私有CA.md
Normal file
@ -0,0 +1,414 @@
|
||||
### 一、安装CFSSL工具
|
||||
|
||||
CFSSL(CloudFlare's PKI Toolkit)是一个开源的PKI工具集,可用于创建私有CA和证书。
|
||||
|
||||
1. **Linux/macOS**:
|
||||
```bash
|
||||
# 下载cfssl和cfssljson工具
|
||||
curl -o cfssl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
|
||||
curl -o cfssljson https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
|
||||
chmod +x cfssl cfssljson
|
||||
sudo mv cfssl cfssljson /usr/local/bin/
|
||||
```
|
||||
|
||||
2. **Windows**:
|
||||
- 从 [CFSSL Releases](https://github.com/cloudflare/cfssl/releases) 下载对应版本的 `cfssl.exe` 和 `cfssljson.exe`
|
||||
- 将可执行文件添加到系统PATH路径
|
||||
|
||||
### 二、创建私有CA
|
||||
|
||||
#### 1. 配置CA证书
|
||||
创建一个名为 `ca-config.json` 的文件,定义证书的有效期和使用策略:
|
||||
```json
|
||||
{
|
||||
"signing": {
|
||||
"default": {
|
||||
"expiry": "87600h" // 10年有效期
|
||||
},
|
||||
"profiles": {
|
||||
"server": {
|
||||
"expiry": "87600h",
|
||||
"usages": ["signing", "key encipherment", "server auth"]
|
||||
},
|
||||
"client": {
|
||||
"expiry": "87600h",
|
||||
"usages": ["signing", "key encipherment", "client auth"]
|
||||
},
|
||||
"peer": {
|
||||
"expiry": "87600h",
|
||||
"usages": ["signing", "key encipherment", "server auth", "client auth"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 创建CA证书签名请求(CSR)配置
|
||||
创建 `ca-csr.json` 文件:
|
||||
```json
|
||||
{
|
||||
"CN": "My Private CA",
|
||||
"key": {
|
||||
"algo": "rsa",
|
||||
"size": 4096
|
||||
},
|
||||
"names": [
|
||||
{
|
||||
"C": "CN",
|
||||
"ST": "Shanghai",
|
||||
"L": "Shanghai",
|
||||
"O": "My Organization",
|
||||
"OU": "IT Department"
|
||||
}
|
||||
],
|
||||
"ca": {
|
||||
"expiry": "87600h" // CA证书有效期10年
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 生成CA证书和私钥
|
||||
执行以下命令生成自签名CA证书:
|
||||
```bash
|
||||
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
|
||||
```
|
||||
这将生成三个文件:
|
||||
- `ca.pem`:CA公钥证书
|
||||
- `ca-key.pem`:CA私钥(妥善保管,不要泄露)
|
||||
- `ca.csr`:CA证书签名请求
|
||||
|
||||
### 三、使用CA签署服务器证书
|
||||
|
||||
#### 1. 创建服务器证书CSR配置
|
||||
创建 `server-csr.json` 文件:
|
||||
```json
|
||||
{
|
||||
"CN": "server.example.com",
|
||||
"hosts": [
|
||||
"server.example.com",
|
||||
"192.168.1.100",
|
||||
"localhost",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"key": {
|
||||
"algo": "rsa",
|
||||
"size": 2048
|
||||
},
|
||||
"names": [
|
||||
{
|
||||
"C": "CN",
|
||||
"ST": "Shanghai",
|
||||
"L": "Shanghai",
|
||||
"O": "My Organization",
|
||||
"OU": "IT Department"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
> 注意:`hosts` 字段必须包含服务器的域名、IP地址以及任何需要访问的别名
|
||||
|
||||
#### 2. 生成服务器证书和私钥
|
||||
使用CA直接签署服务器证书:
|
||||
```bash
|
||||
cfssl gencert \
|
||||
-ca=ca.pem \
|
||||
-ca-key=ca-key.pem \
|
||||
-config=ca-config.json \
|
||||
-profile=server \
|
||||
server-csr.json | cfssljson -bare server
|
||||
```
|
||||
这将生成:
|
||||
- `server.pem`:服务器公钥证书
|
||||
- `server-key.pem`:服务器私钥
|
||||
|
||||
### 四、使用CA签署客户端证书
|
||||
|
||||
#### 1. 创建客户端证书CSR配置
|
||||
创建 `client-csr.json` 文件:
|
||||
```json
|
||||
{
|
||||
"CN": "client.example.com",
|
||||
"hosts": [], // 客户端证书通常不需要指定hosts
|
||||
"key": {
|
||||
"algo": "rsa",
|
||||
"size": 2048
|
||||
},
|
||||
"names": [
|
||||
{
|
||||
"C": "CN",
|
||||
"ST": "Shanghai",
|
||||
"L": "Shanghai",
|
||||
"O": "My Organization",
|
||||
"OU": "IT Department"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 生成客户端证书和私钥
|
||||
```bash
|
||||
cfssl gencert \
|
||||
-ca=ca.pem \
|
||||
-ca-key=ca-key.pem \
|
||||
-config=ca-config.json \
|
||||
-profile=client \
|
||||
client-csr.json | cfssljson -bare client
|
||||
```
|
||||
这将生成:
|
||||
- `client.pem`:客户端公钥证书
|
||||
- `client-key.pem`:客户端私钥
|
||||
|
||||
### 五、证书验证
|
||||
|
||||
#### 1. 验证服务器证书
|
||||
```bash
|
||||
openssl verify -CAfile ca.pem server.pem
|
||||
```
|
||||
如果输出 `server.pem: OK`,则证书有效
|
||||
|
||||
#### 2. 验证客户端证书
|
||||
```bash
|
||||
openssl verify -CAfile ca.pem client.pem
|
||||
```
|
||||
|
||||
### 六、证书使用示例
|
||||
|
||||
#### 1. 在HTTPS服务器中使用
|
||||
将 `server.pem` 和 `server-key.pem` 配置到你的Web服务器(如Nginx、Apache):
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name server.example.com;
|
||||
|
||||
ssl_certificate /path/to/server.pem;
|
||||
ssl_certificate_key /path/to/server-key.pem;
|
||||
ssl_client_certificate /path/to/ca.pem; # 客户端证书验证(可选)
|
||||
ssl_verify_client on; # 启用客户端证书验证(可选)
|
||||
|
||||
# 其他配置...
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 在客户端应用中使用
|
||||
在需要验证服务器证书的客户端应用中,导入 `ca.pem` 作为信任的根证书。例如,使用curl访问HTTPS服务器:
|
||||
```bash
|
||||
curl --cacert ca.pem https://server.example.com
|
||||
```
|
||||
|
||||
### 七、安全注意事项
|
||||
|
||||
1. **私钥保护**:
|
||||
- `ca-key.pem`、`server-key.pem` 和 `client-key.pem` 是敏感文件,应存储在安全位置
|
||||
- 限制访问权限:`chmod 400 *.key.pem`
|
||||
|
||||
2. **证书备份**:
|
||||
- 定期备份CA证书和私钥
|
||||
- 考虑使用硬件安全模块(HSM)存储CA私钥
|
||||
|
||||
3. **证书撤销**:
|
||||
- 如需撤销证书,可使用CFSSL生成证书撤销列表(CRL)
|
||||
- 更新 `ca-config.json` 添加CRL配置
|
||||
|
||||
通过以上步骤,你已成功创建了一个有效期10年的私有CA,并使用它签署了服务器和客户端证书。
|
||||
|
||||
### 八、附快速签发脚本
|
||||
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# 证书快速签发脚本
|
||||
# 依赖: cfssl, cfssljson 已安装并初始化好CA证书
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 默认配置
|
||||
CA_KEY="/root/ckw/cfssl/ca/ca-ecdsa-key.pem"
|
||||
CA_CERT="/root/ckw/cfssl/ca/ca-ecdsa.pem"
|
||||
CA_CONFIG="/root/ckw/cfssl/ca/ca-config.json"
|
||||
OUTPUT_DIR="certs"
|
||||
MERGE_CA="true" # 默认合并根证书
|
||||
|
||||
# 使用帮助
|
||||
function show_help {
|
||||
echo -e "${GREEN}证书快速签发脚本${NC}"
|
||||
echo "用法: $0 [选项]"
|
||||
echo "选项:"
|
||||
echo " -h, --help 显示此帮助信息"
|
||||
echo " -n, --name NAME 证书名称 (必填)"
|
||||
echo " -t, --type TYPE 证书类型: server, client, peer (默认: server)"
|
||||
echo " -c, --ca CA_CERT CA证书路径 (默认: $CA_CERT)"
|
||||
echo " -k, --ca-key CA_KEY CA私钥路径 (默认: $CA_KEY)"
|
||||
echo " -C, --ca-config CONF CA配置文件路径 (默认: $CA_CONFIG)"
|
||||
echo " -o, --output DIR 输出目录 (默认: $OUTPUT_DIR)"
|
||||
echo " -d, --domains LIST 域名列表 (逗号分隔)"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 -n server1 -d example.com,www.example.com "
|
||||
}
|
||||
|
||||
# 参数解析
|
||||
NAME=""
|
||||
TYPE="server"
|
||||
DOMAINS=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-n|--name)
|
||||
NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--type)
|
||||
TYPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--ca)
|
||||
CA_CERT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-k|--ca-key)
|
||||
CA_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-C|--ca-config)
|
||||
CA_CONFIG="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--domains)
|
||||
DOMAINS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-merge-ca)
|
||||
MERGE_CA="false"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}未知参数: $1${NC}" >&2
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 验证必填参数
|
||||
if [[ -z "$NAME" ]]; then
|
||||
echo -e "${RED}错误: 必须指定证书名称 (-n/--name)${NC}" >&2
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 验证证书类型
|
||||
if [[ "$TYPE" != "server" && "$TYPE" != "client" && "$TYPE" != "peer" ]]; then
|
||||
echo -e "${RED}错误: 证书类型必须是 server, client 或 peer${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 验证文件是否存在
|
||||
for file in "$CA_CERT" "$CA_KEY" "$CA_CONFIG"; do
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo -e "${RED}错误: 文件 $file 不存在${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# 当类型为server且未指定域名时,默认将name作为域名
|
||||
if [[ "$TYPE" == "server" && -z "$DOMAINS" ]]; then
|
||||
DOMAINS="$NAME"
|
||||
echo -e "${YELLOW}注意: 证书类型为server且未指定域名,默认添加 ${NAME} 作为域名${NC}"
|
||||
fi
|
||||
|
||||
|
||||
# 创建证书单独目录
|
||||
CERT_DIR="$OUTPUT_DIR/$NAME"
|
||||
mkdir -p "$CERT_DIR" || { echo -e "${RED}无法创建证书目录: $CERT_DIR${NC}"; exit 1; }
|
||||
|
||||
# 生成证书签名请求配置
|
||||
CSR_CONFIG="$CERT_DIR/${NAME}-csr.json"
|
||||
cat > "$CSR_CONFIG" <<EOF
|
||||
{
|
||||
"CN": "$NAME",
|
||||
"key": {
|
||||
"algo": "ecdsa",
|
||||
"size": 256
|
||||
},
|
||||
"names": [
|
||||
{
|
||||
"C": "CN",
|
||||
"ST": "Beijing",
|
||||
"L": "Beijing",
|
||||
"O": "CUA",
|
||||
"OU": "IT"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# 生成SAN列表
|
||||
SAN_LIST=""
|
||||
if [[ -n "$DOMAINS" ]]; then
|
||||
SAN_LIST+="$DOMAINS,"
|
||||
fi
|
||||
|
||||
# 移除末尾逗号
|
||||
SAN_LIST="${SAN_LIST%,}"
|
||||
|
||||
# 根据证书类型选择profile
|
||||
case "$TYPE" in
|
||||
server)
|
||||
PROFILE="server"
|
||||
;;
|
||||
client)
|
||||
PROFILE="client"
|
||||
;;
|
||||
peer)
|
||||
PROFILE="peer"
|
||||
;;
|
||||
esac
|
||||
|
||||
# 生成证书
|
||||
echo -e "${YELLOW}正在生成 $TYPE 证书: $NAME${NC}"
|
||||
echo -e "${YELLOW}SAN列表: $SAN_LIST${NC}"
|
||||
|
||||
cfssl gencert \
|
||||
-ca="$CA_CERT" \
|
||||
-ca-key="$CA_KEY" \
|
||||
-config="$CA_CONFIG" \
|
||||
-profile="$PROFILE" \
|
||||
${SAN_LIST:+-hostname="$SAN_LIST"} \
|
||||
"$CSR_CONFIG" | cfssljson -bare "$CERT_DIR/$NAME"
|
||||
|
||||
# 验证生成结果
|
||||
if [[ -f "$CERT_DIR/${NAME}.pem" && -f "$CERT_DIR/${NAME}-key.pem" ]]; then
|
||||
echo -e "${GREEN}证书生成成功!${NC}"
|
||||
echo -e "${GREEN}证书路径: ${CERT_DIR}/${NAME}.pem${NC}"
|
||||
echo -e "${GREEN}私钥路径: ${CERT_DIR}/${NAME}-key.pem${NC}"
|
||||
echo -e "${GREEN}证书签名请求: ${CSR_CONFIG}${NC}"
|
||||
|
||||
# 合并根证书
|
||||
if [[ "$MERGE_CA" == "true" ]]; then
|
||||
FULL_CHAIN="${CERT_DIR}/${NAME}-fullchain.pem"
|
||||
cat "$CERT_DIR/${NAME}.pem" "$CA_CERT" > "$FULL_CHAIN"
|
||||
echo -e "${GREEN}合并后的完整证书链: ${FULL_CHAIN}${NC}"
|
||||
fi
|
||||
|
||||
# 显示证书信息
|
||||
echo -e "\n${YELLOW}证书信息:${NC}"
|
||||
openssl x509 -noout -text -in "$CERT_DIR/${NAME}.pem" | grep -A 5 "Subject Alternative Name"
|
||||
else
|
||||
echo -e "${RED}证书生成失败!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
```
|
Reference in New Issue
Block a user