Compare commits

...

25 Commits

Author SHA1 Message Date
a8f567b17a update lvm 2025-07-30 23:25:03 +08:00
917f3bd894 update 2025-07-30 23:06:41 +08:00
53d2f55c21 update ca 2025-07-30 21:09:30 +08:00
f01421d2f6 update 2025-07-30 16:39:18 +08:00
8e8aba464b update 2025-07-29 10:59:12 +08:00
84648f971e update cdh 2025-07-28 09:19:32 +08:00
f5c97428a3 uodate njs 2025-07-21 21:38:30 +08:00
633e013f91 update 2025-07-21 20:59:36 +08:00
ead3d16257 更新 2025-05-16 18:15:38 +08:00
bc5d4921ef jsr303 2025-04-16 18:01:59 +08:00
5d70598113 添加 redis 安装 2025-03-31 16:35:20 +08:00
aa9b672d4f add springboot 配置文件j加载顺序 2025-03-25 09:45:03 +08:00
86542c0689 update 2025-03-21 15:35:41 +08:00
997d8a0ac9 update 2025-03-21 15:27:15 +08:00
133b32989c update jsr303 2025-02-18 18:33:43 +08:00
4cab80f831 update frr 2025-02-15 19:38:15 +08:00
3c2d75d367 update frr 2025-02-15 19:29:46 +08:00
39fea53675 update 2025-02-13 14:31:40 +08:00
8bfc885370 add edb 表大小查询 2025-02-04 23:51:44 +08:00
cb6c797863 update 2025-01-06 14:27:10 +08:00
47227f523a update 2025-01-06 13:34:03 +08:00
939c510f21 add oauth-server.md 2025-01-06 13:27:44 +08:00
8dd4593f58 重命名 ”使用 @Valid 注解校验嵌套对象“ 2024-12-30 16:04:02 +08:00
c2545377f1 Merge pull request '添加 java/使用@Valid注解校验嵌套对象' (#1) from chenkuangwei-patch-1 into main
Reviewed-on: #1
2024-12-30 16:09:50 +08:00
b5eb1661dc 添加 java/使用@Valid注解校验嵌套对象 2024-12-30 16:09:22 +08:00
25 changed files with 3672 additions and 398 deletions

4
.gitattributes vendored Normal file
View 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
View 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]部分这一部分会列出所有的realmkdc和admin_server两个配置是在告诉客户端哪台服务器在运行KDC以及kadmin进程。这两项配置可以在服务器上追加端口如果不指定则使用默认端口KDC是88admin 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认证的服务器URLhive.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

View File

@ -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
View 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
```

View File

@ -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);` |

View 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
```

View File

@ -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如果一个属性有多个错误错误信息的分隔符。默认是换行
注1errors标签要放到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());
}
```
###

View 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
View File

@ -0,0 +1,10 @@
### mybtais-plus
mybtais-plus 默认空字段 不插入 导致生成的sql 字段不完整,
在批量插入时, 因为每个po 空值不确定 导致 sql 重写失效,
&reWriteBatchedInserts=true

403
java/oauth-server.md Normal file
View 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 端点和基本凭证。
动态注册的流程如下:
![动态注册的流程](../resources/java/oauth2-server.assets/PhquBVnerb.webp)
客户端注册成功后,就可以使用返回的客户端 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
View 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
View 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 注入方式。

View 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*(实际上说的是本地时间),而同一时刻不同时区的人看到的本地时间是不一样,所以在时间展示的时候需要加上时区的信息,才能精准的找到对应的时刻。
时区与世界时间标准相关:
![时区与世界时间标准相关](https://springdoc.cn/upload/2024/11/11/SZLnsDrkmm.webp)
世界时间的标准在 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 时间 API](https://springdoc.cn/upload/2024/11/11/SZLwKK4biW.webp)
简单的比较如下:
| 特性 | java.util.Date | java.util.Date.Calendar | java.time.LocalDateTime |
| -------- | --------------------------------------------- | ---------------------------- | -------------------------- |
| 线程安全 | ❌ | ❌ | ✅ |
| 时间运算 | ❌ | ✅ | ✅ |
| 可读性 | Tue Oct 08 00:11:16 CST 2024 易读性较低 | ❌不易读 | ✅ yyyy-MM-ddTHH: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 000000* 开始走到此刻的总秒数+不够 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
View 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("所有数字打印完毕");
}
}
```

View File

@ -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
```

View File

@ -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"

View 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!
```

View File

@ -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`
BFDDBFDBidirectional Forwarding Detection是一种快速检测路径故障的协议。这个包提供了 **BFD 守护进程**,可以帮助快速发现路由器之间的连接故障,并根据路由协议快速进行故障恢复。
- **作用**用于加速故障检测,并使路由协议更快地重新计算路径。
用于加速故障检测,并使路由协议更快地重新计算路径。
### 4. `frr-bgpd`
4. `frr-bgpd`
BGPBorder Gateway Protocol是 Internet 上主要的外部网关协议。该包安装的是 **BGP 路由协议守护进程**,用于 BGP 配置和管理,支持 IPv4 和 IPv6 的路由交换。
- **作用**用于与其他自治系统AS交换路由信息常用于大规模网络如互联网骨干网络中的路由控制。
用于与其他自治系统AS交换路由信息常用于大规模网络如互联网骨干网络中的路由控制。
### 5. `frr-eigrpd`
5. `frr-eigrpd`
EIGRPEnhanced 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-ISIntermediate System to Intermediate System是一种内部网关协议IGP主要用于大规模的服务提供商网络。这个包安装的是 **IS-IS 路由协议守护进程**
- **作用**用于在大型网络中进行高效的路由选择IS-IS 主要在运营商和大规模网络中使用。
用于在大型网络中进行高效的路由选择IS-IS 主要在运营商和大规模网络中使用。
### 8. `frr-ldpd`
8. `frr-ldpd`
LDPLabel 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`
NHRPNext Hop Resolution Protocol是用于 IP 网络中查找下一跳信息的协议,特别适用于 VPN 和 IP 网络互联。这个包安装的是 **NHRP 守护进程**
- **作用**用于基于 NHRP 协议查找 IP 地址的下一跳,通常用于 VPN 和其他隧道协议中。
用于基于 NHRP 协议查找 IP 地址的下一跳,通常用于 VPN 和其他隧道协议中。
### 11. `frr-ospf6d`
11. `frr-ospf6d`
OSPFv3Open Shortest Path First version 3是用于 IPv6 网络的路由协议。这个包安装的是 **OSPFv3 路由协议守护进程**
- **作用**提供对 IPv6 网络的路由支持OSPFv3 是 OSPF 的扩展版本,支持 IPv6 地址族。
提供对 IPv6 网络的路由支持OSPFv3 是 OSPF 的扩展版本,支持 IPv6 地址族。
### 12. `frr-ospfd`
12. `frr-ospfd`
OSPFOpen Shortest Path First是一个广泛使用的内部网关协议IGP它使用链路状态协议来计算最佳路径。这个包安装的是 **OSPF 路由协议守护进程**
- **作用**用于基于链路状态协议的路由计算,适用于大型企业网络中的路由配置。
用于基于链路状态协议的路由计算,适用于大型企业网络中的路由配置。
### 13. `frr-pbrd`
13. `frr-pbrd`
PBRPolicy-Based Routing是基于策略的路由协议允许根据数据包的内容如源 IP、目标 IP 等)决定路由路径。这个包安装的是 **PBR 守护进程**
- **作用**提供基于策略的路由决策,允许对流量进行更细粒度的控制。
提供基于策略的路由决策,允许对流量进行更细粒度的控制。
### 14. `frr-pimd`
14. `frr-pimd`
PIMProtocol Independent Multicast是用于组播路由的协议适用于需要多播的应用。这个包安装的是 **PIM 守护进程**
- **作用**为网络提供组播支持,适用于需要组播(如视频流、广播等)的网络应用。
为网络提供组播支持,适用于需要组播(如视频流、广播等)的网络应用。
### 15. `frr-ripd`
15. `frr-ripd`
RIPRouting Information Protocol是一个距离矢量协议适用于小型或中型的网络。这个包安装的是 **RIP 路由协议守护进程**
- **作用**实现 RIP 协议,进行路由信息的传播,适用于简单的内部网络。
实现 RIP 协议,进行路由信息的传播,适用于简单的内部网络。
### 16. `frr-ripngd`
16. `frr-ripngd`
RIPngRIP next generation是 RIP 协议的扩展,支持 IPv6 网络。这个包安装的是 **RIPng 路由协议守护进程**
- **作用**提供 RIP 协议的 IPv6 支持,适用于 IPv6 网络中的动态路由。
提供 RIP 协议的 IPv6 支持,适用于 IPv6 网络中的动态路由。
### 17. `frr-staticd`
17. `frr-staticd`
Staticd 组件用于管理 **静态路由**,这些路由是手动配置的,并不通过动态路由协议传播。这个包安装的是 **静态路由守护进程**
- **作用**管理静态路由的配置,适用于不需要动态路由的网络部分。
管理静态路由的配置,适用于不需要动态路由的网络部分。
### 18. `frr-vrrpd`
18. `frr-vrrpd`
VRRPVirtual 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
View 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 | 客户端 | TUNIP 路由) | 标准多客户端 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 | 服务器 | TUNIP 路由) | 标准多客户端 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`

View File

@ -4,28 +4,6 @@
个人使用ZeroTier体验不错
### 名词解释
**ZeroTier**
自行了解
**rip**
rip 是一种动态路由协议 可以自动学习路由
**frr**
frr 是一个 实现了 rip bgp 等多种 路由协议的软件包,同类型的还有 Quagga 等
### 网络拓扑图
```mermaid

View File

@ -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;

View 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;
```

View 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

Binary file not shown.

414
私有CA.md Normal file
View File

@ -0,0 +1,414 @@
### 一、安装CFSSL工具
CFSSLCloudFlare'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
```