自动化脚本安全最佳实践:构建可靠的自动化系统

自动化脚本安全最佳实践:构建可靠的自动化系统

前言

在 DevOps、AI 自动化和日常运维中,自动化脚本已成为提升效率的核心工具。然而,安全漏洞往往隐藏在看似简单的脚本中,可能导致数据泄露、系统入侵甚至供应链攻击。根据 2024 年 OWASP 报告,不安全的自动化脚本已成为企业安全事件的重要来源之一。

本文将面向中级开发者,系统性地介绍自动化脚本的安全最佳实践,结合可操作的代码示例,帮助你构建既高效又安全的自动化系统。

正文

一、输入验证与数据清理

自动化脚本最脆弱的一环往往是外部输入。无论是文件、命令行参数还是 API 响应,都必须经过严格验证。

危险示例:

1
2
3
# ❌ 危险:直接使用未验证的用户输入
filename=$1
cat "$filename" # 可能触发命令注入:rm -rf / etc/passwd

安全方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
SAFE_INPUT_REGEX="^[a-zA-Z0-9._-]+$"

validate_filename() {
if [[ ! "$1" =~ $SAFE_INPUT_REGEX ]]; then
echo "错误:文件名包含非法字符" >&2
return 1
fi

# 检查路径遍历
if [[ "$1" == *".."* ]]; then
echo "错误:禁止使用父目录引用" >&2
return 1
fi

# 确保文件在允许的目录内
local base_dir="/safe/data"
local abs_path=$(realpath "$base_dir/$1")
if [[ "$abs_path" != "$base_dir"* ]]; then
echo "错误:路径超出允许范围" >&2
return 1
fi
}

if validate_filename "$1"; then
cat "/safe/data/$1"
fi

对于 Python 脚本,使用 pydantic 进行结构化验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pydantic import BaseModel, validator
import re

class ScriptInput(BaseModel):
filename: str

@validator('filename')
def validate_filename(cls, v):
if not re.match(r'^[\w.\- ]+$', v):
raise ValueError('非法文件名')
if '..' in v:
raise ValueError('禁止路径遍历')
return v

# 使用
try:
input_data = ScriptInput(filename=user_input)
except ValueError as e:
print(f"验证失败: {e}")
exit(1)

二、权限最小化原则

脚本应以最低必要权限运行,避免使用 root 或管理员权限。

Linux 最佳实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

# 1. 立即降权(如果以root运行)
if [[ $EUID -eq 0 ]]; then
echo "请勿以root运行此脚本" >&2
exit 1
fi

# 2. 使用具体用户
RUN_AS_USER="deploy"
if [[ $USER != "$RUN_AS_USER" ]]; then
echo "必须以 $RUN_AS_USER 用户运行" >&2
exit 1
fi

# 3. 限制可执行文件权限
chmod 700 /path/to/script.sh

文件系统权限配置:

1
2
3
4
5
# 创建专用目录并设置权限
mkdir -p /var/log/myapp
chown deploy:deploy /var/log/myapp
chmod 750 /var/log/myapp
setfacl -m u:deploy:rwx /var/log/myapp

三、密钥与敏感信息管理

绝对禁止的实践:

1
2
3
# ❌ 危险:硬编码密钥
API_KEY = "sk_live_1234567890abcdef" # 提交到GitHub就完了
DATABASE_PASSWORD = "P@ssw0rd123!"

安全的密钥管理方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 方案1:环境变量(配合.env文件,gitignore保护)
import os
from dotenv import load_dotenv

load_dotenv() # 加载 .env 文件(不提交到Git)

API_KEY = os.environ.get('API_KEY')
if not API_KEY:
raise EnvironmentError("未设置API_KEY环境变量")

# 方案2:使用密钥管理服务(以HashiCorp Vault为例)
import hvac

vault = hvac.Client(
url=os.environ['VAULT_ADDR'],
token=os.environ['VAULT_TOKEN']
)

secret = vault.secrets.kv.v2.read_secret_version(
path='myapp/api-key'
)
API_KEY = secret['data']['data']['key']

# 方案3:云平台密钥管理(AWS Secrets Manager)
import boto3
from botocore.exceptions import ClientError

def get_secret(secret_name):
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name='us-east-1'
)

try:
response = client.get_secret_value(SecretId=secret_name)
return response['SecretString']
except ClientError as e:
print(f"密钥获取失败: {e}")
exit(1)

环境变量最佳实践:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# 使用一个集中化的配置加载器
source /etc/myapp/env.conf # 系统级配置(root权限保护)

# 验证必需的环境变量
REQUIRED_VARS=("API_KEY" "DB_HOST" "LOG_LEVEL")
for var in "${REQUIRED_VARS[@]}"; do
if [[ -z "${!var}" ]]; then
echo "错误:未设置 $var 环境变量" >&2
exit 1
fi
done

四、错误处理与日志安全

不安全的错误处理:

1
2
3
4
5
6
# ❌ 危险:泄露敏感信息
try:
result = api_call()
except Exception as e:
print(f"错误详情: {e}") # 可能包含API密钥、数据库连接字符串
raise

安全日志实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import logging
import sys
from typing import Optional

class SecureLogger:
def __init__(self, log_file: str = '/var/log/myapp.log'):
self.logger = logging.getLogger('myapp')
self.logger.setLevel(logging.INFO)

# 文件处理器(限制访问)
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(logging.INFO)

# 控制台处理器(开发环境)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.WARNING)

# 自定义格式化器,过滤敏感信息
class SecureFormatter(logging.Formatter):
SENSITIVE_PATTERNS = [
(r'password=([^&\s]+)', 'password=***'),
(r'key=([^&\s]+)', 'key=***'),
(r'token=([^&\s]+)', 'token=***'),
(r'Authorization: Bearer ([^\s]+)', 'Authorization: Bearer ***'),
]

def format(self, record):
msg = super().format(record)
for pattern, replacement in self.SENSITIVE_PATTERNS:
msg = re.sub(pattern, replacement, msg, flags=re.IGNORECASE)
return msg

formatter = SecureFormatter(
'%(asctime)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

self.logger.addHandler(file_handler)
self.logger.addHandler(console_handler)

def safe_log(self, level: int, message: str,
sensitive_data: Optional[dict] = None):
"""安全的日志记录方法"""
# 在日志前清理敏感数据
safe_message = self._sanitize(message)
self.logger.log(level, safe_message)

def _sanitize(self, text: str) -> str:
# 移除潜在的敏感信息
import re
patterns = [
(r'\b\d{4}[*-]\d{4}[*-]\d{4}[*-]\d{4}\b', '****-****-****-****'), # 信用卡
(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '***@***.***'), # 邮箱
(r'Bearer\s+[A-Za-z0-9\-._~+/]+=*', 'Bearer ***'), # JWT
]
for pattern, replacement in patterns:
text = re.sub(pattern, replacement, text)
return text

# 使用
logger = SecureLogger()
try:
process_data(api_key=os.getenv('API_KEY'))
except Exception as e:
logger.safe_log(logging.ERROR, f"处理失败: {str(e)}")
sys.exit(1) # 退出码而不是堆栈跟踪

五、依赖项安全管理

自动化的依赖项可能引入供应链攻击。

requirements.txt 管理:

1
2
3
4
5
6
7
8
9
10
11
# ✅ 良好实践:# 固定版本,便于审计
requests==2.31.0
boto3==1.34.0

# ✅ 使用哈希验证(pip 8.0+)
requests==2.31.0 \
--hash=sha256:abc123... \
--hash=sha256:def456...

# ❌ 避免:版本范围可能引入破坏性更新
requests>=2.0.0 # 可能升级到包含漏洞的版本

Python 依赖安全扫描:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# 定期检查依赖漏洞

# 1. 使用 pip-audit
pip install pip-audit
pip-audit --requirement requirements.txt

# 2. 使用 safety(需要API key)
safety check --file requirements.txt

# 3. 使用 GitHub Dependabot(自动创建PR)
# 在项目中添加 .github/dependabot.yml

自动化的依赖更新脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python3
"""
依赖安全检查与自动更新脚本
"""
import subprocess
import json
from packaging import version

def check_vulnerabilities():
"""检查已知漏洞"""
result = subprocess.run(
['pip-audit', '--format', 'json'],
capture_output=True,
text=True
)

vulnerabilities = json.loads(result.stdout)
if vulnerabilities:
print(f"发现 {len(vulnerabilities)} 个漏洞:")
for vuln in vulnerabilities:
print(f" - {vuln['name']} {vuln['version']}: {vuln['vuln_id']}")
return False
return True

def update_dependencies():
"""安全地更新依赖"""
# 使用 pip-review 或 pip-upgrader
subprocess.run(['pip-review', '--auto'], check=True)

# 更新后重新测试
subprocess.run(['pytest'], check=True)

if __name__ == '__main__':
if not check_vulnerabilities():
print("建议更新依赖项...")
response = input("是否立即更新?(y/N): ")
if response.lower() == 'y':
update_dependencies()

六、代码审查与静态分析

自动化脚本同样需要代码审查和静态分析。

GitHub Actions 工作流配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# .github/workflows/security-scan.yml
name: Security Scan

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

jobs:
security:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: 设置Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: 安装依赖检查工具
run: |
pip install bandit safety pip-audit
pip install -r requirements.txt

- name: Bandit安全扫描
run: bandit -r ./scripts/ -f json -o bandit-report.json

- name: Safety依赖检查
run: safety check --json

- name: 上传扫描结果
uses: actions/upload-artifact@v3
if: always()
with:
name: security-reports
path: |
bandit-report.json
safety-report.json

- name: 失败时阻止合并
if: failure()
run: |
echo "::error::安全检查未通过,请修复问题"
exit 1

Shell脚本安全检查:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# shellcheck 是个不错的Shell脚本检查工具

# 在CI中添加
shellcheck script.sh

# 或在本地预提交钩子中运行
echo "运行ShellCheck..."
shellcheck \
--exclude SC2086,SC2046 \
--severity=warning \
script.sh

七、运行时安全与沙箱隔离

某些自动化任务需要隔离执行。

Docker容器化方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Dockerfile - 安全的运行环境
FROM python:3.11-slim

# 创建非root用户
RUN useradd -m -u 1000 -s /bin/bash runner
USER runner
WORKDIR /home/runner

# 只复制必要的文件
COPY --chown=runner:runner requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=runner:runner scripts/ ./scripts/

# 运行脚本
CMD ["python", "scripts/my_automation.py"]

gVisor或Firecracker强化隔离:

1
2
3
4
5
6
7
8
9
# 使用 runsc (gVisor) 运行容器
docker run --runtime=runsc \
--read-only \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
my-automation-script:latest

# 或使用Firecracker微虚拟机
firecracker --config-file=vm-config.json

Python自动化的资源限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python3
"""
受限环境执行脚本
"""
import resource
import sys
import os

def set_resource_limits():
"""设置资源限制,防止DoS攻击"""
# CPU时间限制(秒)
resource.setrlimit(resource.RLIMIT_CPU, (30, 60))

# 内存限制(字节)
mem_limit = 256 * 1024 * 1024 # 256MB
resource.setrlimit(resource.RLIMIT_AS, (mem_limit, mem_limit))

# 文件大小限制
resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024, 10 * 1024 * 1024))

# 进程数限制
resource.setrlimit(resource.RLIMIT_NPROC, (10, 20))

# 在main之前调用
if __name__ == '__main__':
set_resource_limits()

# 设置安全的umask
os.umask(0o077)

# 执行主逻辑
main()

八、监控与审计

自动化脚本需要完整的审计追踪。

结构化日志配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import json
import logging
from datetime import datetime

class AuditLogger:
def __init__(self, log_file: str = '/var/log/audit.json'):
self.log_file = log_file

def log_event(self, event_type: str, user: str,
resource: str, action: str,
status: str, details: dict = None):
"""记录审计事件"""
event = {
'timestamp': datetime.utcnow().isoformat() + 'Z',
'event_type': event_type,
'user': user,
'resource': resource,
'action': action,
'status': status,
'details': details or {},
'source_ip': os.environ.get('REMOTE_ADDR', 'localhost')
}

with open(self.log_file, 'a') as f:
f.write(json.dumps(event) + '\n')

def log_api_call(self, service: str, endpoint: str,
params: dict, response_code: int):
"""记录API调用"""
# 清理参数中的敏感信息
safe_params = self._sanitize_dict(params)
self.log_event(
event_type='api_call',
user=os.getenv('USER', 'unknown'),
resource=f'{service}:{endpoint}',
action='call',
status='success' if 200 <= response_code < 300 else 'failed',
details={
'params': safe_params,
'response_code': response_code
}
)

# 使用示例
audit = AuditLogger()

try:
result = process_file(filename)
audit.log_event(
'file_operation',
os.getenv('USER'),
filename,
'process',
'success',
{'size': len(result)}
)
except Exception as e:
audit.log_event(
'file_operation',
os.getenv('USER'),
filename,
'process',
'failed',
{'error': str(e)}
)

结语

自动化脚本安全不是一次性任务,而是需要贯穿整个生命周期的持续实践。从编写、审核、部署到监控,每个环节都有安全隐患需要防范。

通过本文介绍的方法——输入验证、权限最小化、密钥管理、依赖安全、代码审查、沙箱隔离、审计监控——你可以显著提升自动化系统的安全性。

记住:自动化效率越高,安全漏洞的放大效应也越强。务必在追求效率的同时,把安全作为不可妥协的红线。


延伸阅读

OpenClaw 自动化配置完全指南:从零搭建高效 AI 工作流

前言

OpenClaw 是一个强大的 AI 助手框架,但要发挥最大效能,需要一套可靠的自动化配置。本文将手把手教你如何配置 OpenClaw 的核心自动化功能,包括:

  • 定时模型检查(避免模型失效导致意外)
  • 自动化任务推送(到飞书/Telegram/Discord)
  • 模型配置健康监控脚本
  • 常见问题与解决方案

一、OpenClaw 模型配置详解

OpenClaw 的模型配置文件位于 ~/.openclaw/config.yaml,这是你选择 AI 模型的核心配置。

1.1 基础配置结构

1
2
3
4
5
6
7
8
9
10
default_model: openrouter/auto
models:
# 主要模型(按优先级排序)
- openrouter/auto # 自动路由,智能选择可用模型
- openrouter/hunter-alpha # 1M context,最强推理
- openrouter/healer-alpha # Vision + Tools + Reasoning
- arcee-ai/trinity-large-preview:free # 131K, 创意写作强
- nvidia/nemotron-3-super-120b-a12b:free # 262K, 工具调用
- stepfun/step-3.5-flash:free # 256K, #1 最受欢迎
- xiaomi/mimo-v2-flash:free # 小米模型(需验证可用性)

1.2 模型选择策略

  • openrouter/auto:自动路由,优先使用可用免费模型中最高性能的
  • hunter-alpha:适合复杂推理、长文档分析(1M context)
  • healer-alpha:支持视觉理解,适合图片+文本混合任务
  • step-3.5-flash:StepFun 旗舰,综合性能最强,免费且稳定

配置建议:

  • 至少包含 openrouter/auto 作为 fallback
  • 保留 3-5 个高性能模型,OpenClaw 会自动负载均衡
  • 定期检查模型可用性(见下文脚本)

二、定时任务配置(Crontab)

OpenClaw 支持通过系统 crontab 定时执行任务并推送通知。但需要注意环境变量问题

2.1 常见坑:cron 找不到 node

如果你直接用 openclaw message send 作为 cron 命令,会失败,因为 cron 环境不包含 NVM 的 node 路径。

错误示例:

1
0 * * * * openclaw message send ...  # ❌ 会静默失败

正确做法:bash -l -c 包装,加载完整用户环境

1
0 * * * * /bin/bash -l -c "openclaw message send --channel feishu --target <chat_id> --message '内容'" 2>&1 | logger -t tag

2.2 我的 crontab 配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 每小时模型检查(已改为每天12:00)
0 12 * * * /bin/bash -l -c "openclaw message send --channel feishu --target ou_xxx --message '🕐 每日模型检查:正常'" 2>&1 | logger -t model-check

# 早间新闻(8:30)
30 8 * * * /bin/bash -l -c "openclaw message send ..." 2>&1 | logger -t morning-news

# 晚间AI新闻(21:00)
0 21 * * * /bin/bash -l -c "openclaw message send ..." 2>&1 | logger -t evening-news

# 每日收益报告(23:00)
0 23 * * * /bin/bash -l -c "openclaw message send ..." 2>&1 | logger -t revenue-report

# 工作日报(18:00)
0 18 * * * /bin/bash -l -c "openclaw message send ..." 2>&1 | logger -t daily-progress

关键点:

  • 使用绝对路径:/bin/bash -l -c "命令"
  • 2>&1 | logger -t tag 将输出重定向到系统日志,便于排查
  • 测试时先用 openclaw message send 手动运行确认成功,再加到 crontab

三、模型维护自动化脚本

我写了一个 model-maintenance.sh 脚本,定期检查 config.yaml 中配置的模型是否在最新的免费模型列表中,并推送报告。

3.1 脚本位置

/Users/zhaojingzhou/.openclaw/workspace/scripts/model-maintenance.sh

3.2 脚本逻辑

  1. 读取 config.yaml 中的模型列表
  2. 与已知的 28 个 OpenRouter 免费模型对比
  3. 识别可能失效或过时的模型
  4. 生成健康报告并推送
  5. 记录日志

3.3 核心代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/bin/bash
set -e
LOG_FILE="/path/to/model-maintenance.log"
CONFIG_FILE="/Users/zhaojingzhou/.openclaw/config.yaml"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
ISSUES_FOUND=0

# 读取配置的模型
CONFIG_MODELS=$(grep -E "^\s*-\s+" "$CONFIG_FILE" | sed 's/^\s*-\s*//' | grep -v '^#' | tr '\n' ' ')

# 已知免费模型列表(2026-03)
ALL_FREE_MODELS="openrouter/free openrouter/hunter-alpha ..." # 完整28个

# 检查每个模型
for model in $CONFIG_MODELS; do
if echo "$ALL_FREE_MODELS" | grep -q "$model"; then
echo "[$DATE] ✅ $model: in free list"
else
echo "[$DATE] ❌ $model: NOT in free list"
ISSUES_FOUND=1
fi
done

# 推送报告
MESSAGE="...(格式化报告内容)"
openclaw message send --channel feishu --target <chat_id> --message "$MESSAGE" 2>&1 | tee -a "$LOG_FILE"

3.4 使用方式

1
2
3
4
5
# 手动执行
./scripts/model-maintenance.sh

# 或加入 crontab(每天12:00执行)
0 12 * * * /bin/bash -l -c "/Users/xxx/workspace/scripts/model-maintenance.sh"

四、实际排查案例:cron 任务不执行

问题现象

  • crontab 有配置,但从未收到推送消息
  • 手动执行命令正常

排查过程

  1. 检查 crontab 列表:

    1
    crontab -l

    确认任务存在且时间正确。

  2. 检查系统日志:

    1
    sudo log show --predicate 'eventMessage contains "model-check"' --last 1h

    查看 logger 是否有输出。

  3. 核心发现:
    cron 运行时没有 NVM 环境,openclaw 依赖的 node 不在 PATH 中。

  4. 解决方案:
    将命令改为:

    1
    0 * * * * /bin/bash -l -c "openclaw message send ..." 2>&1 | logger -t model-check

    -l 表示 login shell,会加载 ~/.bash_profile~/.zprofile,从而引入 NVM 的路径。

五、完整自动化架构建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────┐
│ OpenClaw 自动化架构 │
├─────────────────────────────────────────────┤
│ 1. 模型健康检查 (model-maintenance.sh) │
│ - 频率:每天12:00 │
│ - 推送:配置健康报告 │
│ - 日志:/logs/model-maintenance.log │
│ │
│ 2. 定时消息推送 (crontab + openclaw) │
│ - 模型检查:每天12:00(已改) │
│ - 早间新闻:每天8:30 │
│ - 晚间新闻:每天21:00 │
│ - 收益报告:每天23:00(待完善内容) │
│ - 工作日报:每天18:00 │
│ │
│ 3. 被动心跳 (heartbeat-state.json) │
│ - 用户每次交互时自动检查模型可用性 │
│ - 超过1小时未检查则静默更新 MEMORY.md │
│ │
│ 4. 日志与监控 │
│ - 系统日志 logger 捕获所有 cron 输出 │
│ - 本地日志 /logs/*.log │
│ - MEMORY.md 记录关键变更 │
└─────────────────────────────────────────────┘

六、最佳实践与提醒

  1. 永远用 bash -l -c 包装 openclaw 命令

    • 避免环境变量问题
    • 确保 NVM node 可用
  2. 测试顺序:

    • 手动执行成功 → 加入 crontab → 观察日志 → 验证推送
  3. 日志管理:

    • 任务输出重定向到 logger
    • 脚本内部用 tee -a 记录本地日志
  4. 推送内容:

    • 简洁、具体、可操作
    • 避免空洞的”进展:进行中”
    • 包含量化指标和下一步计划
  5. 配置管理:

    • 修改 config.yaml 后立即更新 MEMORY.md 记录
    • 模型列表 Keeping 与最新公开数据同步(每月一次)

结语

自动化是 OpenClaw 高效运转的基石。正确配置 cron 环境、编写健壮的维护脚本、建立监控体系,能让你的 AI 助手 24/7 稳定运行,及时发现问题并推送告警。

如果遇到类似 “cron 任务不执行” 的问题,优先检查环境变量和 PATH,用 bash -l -c 包装命令几乎总能解决。


附录:我的 crontab 当前配置(2026-03-14)

1
2
3
4
5
0 12 * * * /bin/bash -l -c "/Users/zhaojingzhou/.nvm/versions/node/v22.22.0/bin/openclaw message send --channel feishu --target ou_985eeb07a18725cf9b5d5d9ae63a324d --message '🕐 每日模型检查(12:00):当前所有28个免费模型正常,stepfun/step-3.5-flash:free 仍为 #1。使用量: 1.26T tokens。'" 2>&1 | logger -t model-check
30 8 * * * /bin/bash -l -c "openclaw message send --channel feishu --target ou_985eeb07a18725cf9b5d5d9ae63a324d --message '🌅 早上好!今日热点新闻已更新。使用 exa-web-search-free 搜索最新资讯。'" 2>&1 | logger -t morning-news
0 21 * * * /bin/bash -l -c "openclaw message send --channel feishu --target ou_985eeb07a18725cf9b5d5d9ae63a324d --message '🌙 晚间AI/科技热点新闻:使用 exa-web-search-free 搜索今日最新AI/科技资讯。'" 2>&1 | logger -t evening-news
0 23 * * * /bin/bash -l -c "openclaw message send --channel feishu --target ou_985eeb07a18725cf9b5d5d9ae63a324d --message '💰 今日收益报告:检查所有创收渠道,汇总今日收益。'" 2>&1 | logger -t revenue-report
0 18 * * * /bin/bash -l -c "openclaw message send --channel feishu --target ou_985eeb07a18725cf9b5d5d9ae63a324d --message '📊 工作日报(18:00):今日进展:进行中...'" 2>&1 | logger -t daily-progress

本文基于真实部署经验整理,转载请注明出处。

MySQL多字段排序的问题

最近在一次开发过程中发现了一个问题,我们都知道MySQL在Order By的时候可以指定多列,会按照指定的顺序依次排序。

例如指定A,B,C列,MySQL会先按A排序,如何A值相同的再按B排序,B值相同的再按C排序。

然后其中还是有不少门道的,例如我就碰到了如下问题,我有一个表test,表结构如下:

1
2
3
4
5
6
7
8
9
10
11

CREATE TABLE `test` (

`A` varchar(128) NOT NULL COMMENT 'A',

`B` varchar(64) NOT NULL COMMENT 'B',

`C` varchar(64) NOT NULL COMMENT 'C'

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='TEST'

其中有如下数据:

A B C
C料 12321 123
c料 测试 SPHC
c料 测试 SPHC
C料 测试 测试
C料 测试 测试

接着执行如下查询

1
2
3

select * from test order by A,B,C

按照我们上面的说法最后出来的结果应该是:

A B C
C料 12321 123
C料 测试 测试
C料 测试 测试
c料 测试 SPHC
c料 测试 SPHC

然而实际出来而结果是:

A B C
C料 12321 123
c料 测试 SPHC
c料 测试 SPHC
C料 测试 测试
C料 测试 测试

可以看到的是A列明显乱序了(注意C大小写),不是说好A相同的排序在一起呢,怎么看起了不对了。

既然是按照列的顺序一个一个排序的,那我们就一个一个排除,看看到底是在哪一列排序出了问题。

执行如下查询:

1
2
3

select * from test order by A,B

得到如下结果:

A B C
C料 12321 123
C料 测试 测试
C料 测试 测试
c料 测试 SPHC
c料 测试 SPHC

也就是说在C列加入排序之前,A还是看着有序的,那到底是怎么回事呢?注意回到最开始我们说的MySQL的排序规则,A相同再按B排,B相同按C排。

所以C在加入排序之前,MySQL认为A,B排序后有相同的结果,也就是c料,测试=C料,测试了,所以C加入排序之后可以在前者相同的排序结果中在按C排序。

于是就产生了看起来错误的顺序,这是因为创建表的时候,没有对字段A,B,C指定collation(MYSQL排序依据),默认会使用对应字符集的Collation,

我这里字符集是utf8mb4,对应的默认collation通常是utf8mb4_general_ci,这个collation是大小写不敏感的。

解决办法有两种:

1.更改字段的collation,

1
2
3
4
5
6
7
8
9
10
11

CREATE TABLE `test` (

`A` varchar(128) NOT NULL COMMENT 'A' COLLATE utf8mb4_bin,

`B` varchar(64) NOT NULL COMMENT 'B' COLLATE utf8mb4_bin,

`C` varchar(64) NOT NULL COMMENT 'C' COLLATE utf8mb4_bin

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='TEST'

使用utf8mb4_bin,这个是大小写敏感的。

2.在排序的时候指定BINARY关键字

1
2
3

select * from test order by binary A,binary B, binary C

因为它会强制MySQL将字段作为二进制字符串对待。

由于这是一个历史存在的表,避免产生其它意向不到的结果,我使用了方法二。至此问题解决。

记一次线上Spring和Dubbo死锁排查

问题

前不久线上发现有系统间数据没有同步,排查一通下来发现应该是 MQ 消息没有被消费,通过 MQ Console 发现,未被消费的消息全都集中的一台机器上(消息投放的queue 以及 rebalance 关系),此时我有两个怀疑,一是消费者线程挂了,二是此服务分配到的消息队列出了什么莫名的问题。由于正值业务高峰,领导第一时间重启了服务,重启后一切恢复正常。排查由于一些其它事项,也到此中断。

然而没过多久,我听到其它需求项目组在测试环境出现了相同的问题,也是消息不消费了,导致业务异常。我立马找到对应服务 dump 了线程。

我将 dump 文件通过 visualVM 打开,然后得到了很明显的提示:Found one Java-level deadlock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
"ConsumeMessageThread_1":
waiting to lock monitor 0x00007f1350390f18 (object 0x000000008024ebc0, a java.util.concurrent.ConcurrentHashMap),
which is held by "main"
"main":
waiting to lock monitor 0x00007f1330071f18 (object 0x0000000080fb6318, a org.apache.dubbo.config.deploy.DefaultModuleDeployer),
which is held by "Thread-26"
"Thread-26":
waiting to lock monitor 0x00007f1350390f18 (object 0x000000008024ebc0, a java.util.concurrent.ConcurrentHashMap),
which is held by "main"

Java stack information for the threads listed above:
===================================================
"ConsumeMessageThread_1":
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:187)
- waiting to lock <0x000000008024ebc0> (a java.util.concurrent.ConcurrentHashMap)
at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch(AbstractBeanFactory.java:486)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:432)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:403)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:515)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:508)
at org.springframework.context.support.AbstractApplicationContext.getBeansOfType(AbstractApplicationContext.java:1186)
...
at org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService$ConsumeRequest.run(ConsumeMessageConcurrentlyService.java:411)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
"main":
at org.apache.dubbo.config.deploy.DefaultModuleDeployer.startSync(DefaultModuleDeployer.java)
- waiting to lock <0x0000000080fb6318> (a org.apache.dubbo.config.deploy.DefaultModuleDeployer)
at org.apache.dubbo.config.deploy.DefaultModuleDeployer.start(DefaultModuleDeployer.java:139)
at org.apache.dubbo.config.ReferenceConfig.get(ReferenceConfig.java:228)
...
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:162)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:588)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1173)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1067)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:513)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
- locked <0x000000008024ebc0> (a java.util.concurrent.ConcurrentHashMap)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
- locked <0x00000000804b98b0> (a java.lang.Object)
at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107)
at com.internet.saasbillmanager.ApplicationMain.main(ApplicationMain.java:46)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:51)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:52)
"Thread-26":
at org.springframework.context.event.AbstractApplicationEventMulticaster.getApplicationListeners(AbstractApplicationEventMulticaster.java:185)
- waiting to lock <0x000000008024ebc0> (a java.util.concurrent.ConcurrentHashMap)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:128)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:393)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:347)
at org.apache.dubbo.config.spring.context.DubboDeployApplicationListener.publishEvent(DubboDeployApplicationListener.java:91)
at org.apache.dubbo.config.spring.context.DubboDeployApplicationListener.access$000(DubboDeployApplicationListener.java:47)
at org.apache.dubbo.config.spring.context.DubboDeployApplicationListener$1.onStarted(DubboDeployApplicationListener.java:70)
at org.apache.dubbo.config.spring.context.DubboDeployApplicationListener$1.onStarted(DubboDeployApplicationListener.java:62)
at org.apache.dubbo.common.deploy.AbstractDeployer.setStarted(AbstractDeployer.java:121)
at org.apache.dubbo.config.deploy.DefaultApplicationDeployer.onStarted(DefaultApplicationDeployer.java:989)
at org.apache.dubbo.config.deploy.DefaultApplicationDeployer.checkState(DefaultApplicationDeployer.java:868)
- locked <0x0000000080fa6c10> (a java.lang.Object)
at org.apache.dubbo.config.deploy.DefaultApplicationDeployer.notifyModuleChanged(DefaultApplicationDeployer.java:851)
at org.apache.dubbo.config.deploy.DefaultModuleDeployer.onModuleStarted(DefaultModuleDeployer.java:264)
at org.apache.dubbo.config.deploy.DefaultModuleDeployer.startSync(DefaultModuleDeployer.java:171)
- locked <0x0000000080fb6318> (a org.apache.dubbo.config.deploy.DefaultModuleDeployer)
at org.apache.dubbo.config.deploy.DefaultModuleDeployer.start(DefaultModuleDeployer.java:139)
at org.apache.dubbo.config.ReferenceConfig.get(ReferenceConfig.java:228)
at org.apache.dubbo.config.spring.ReferenceBean.getCallProxy(ReferenceBean.java:346)
at org.apache.dubbo.config.spring.ReferenceBean.access$100(ReferenceBean.java:99)
at org.apache.dubbo.config.spring.ReferenceBean$DubboReferenceLazyInitTargetSource.createObject(ReferenceBean.java:353)
at org.springframework.aop.target.AbstractLazyCreationTargetSource.getTarget(AbstractLazyCreationTargetSource.java:86)
- locked <0x0000000085b49868> (a org.apache.dubbo.config.spring.ReferenceBean$DubboReferenceLazyInitTargetSource)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:192)
...
at com.xxl.job.core.thread.JobThread.run(JobThread.java:152)

Found 1 deadlock.

这里有 3 个线程:

  • “ConsumeMessageThread_1” MQ 消费者线程

  • “main”: 主线程

  • “Thread-26”: XXL-JOB 执行线程

其实主要是这两个线程引发的问题:

  • “main”: 主线程

  • “Thread-26”: XXL-JOB 执行线程

MQ 消费者线程是正好撞在枪口上了。

涉及两个锁:

  • DefaultModuleDeployer:object monitor lock

  • DefaultSingletonBeanRegistry.singletonObjects:object monitor lock

暂且将第一个称为 Dubbo 锁,第二个称为 Spring 锁。

“main”: 主线程做的事情是初始化一个基础服务(Dubbo Consumer)注册到 Spring 和 Dubbo 中, 而其先获取到了 Spring 锁,再去获取 Dubbo 锁。

“Thread-26”:job 线程是由 xxljob 触发后执行一个任务,需要调用一个远程的 Dubbo 服务,于是 先获取了 Dubbo 锁,再去获取 Spring 锁。

于是两个线程就互相锁死了,而 MQ 消费者线程,也要去Bean Container 中获取 Bean,也需要获取 Spring 锁,也就卡死了。

解决方案

这个问题由于是在启动过程中发生,所以我设想了两个解决方案(没有好的契机去解决这个问题,只能等到有相关需求了),其实思路是同一个,那就是将出问题的 Bean 给延迟加载:

  • 将“基础服务”Bean 给@Lazy。
  • 将“XxlJobSpringExecutor”在spring启动完成之后再注册进容器,避免启动过程中收到任务的执行命令。

老版本Dubbo的一个bug

线上有用户反应P 服务页面时不时就报个错,后来发现都涉及I服务的一个接口方法,这里暂且就叫F 接口。只是时不时报错,那说明可能是某些参数会导致异常,于是我开始查看日志,看看是不是有什么特殊参数导致隐藏 bug 被发现。
结果是报错和不报错的请求参数都一样,那是不是多台机器上运行的代码不一致呢?于是我看了F 接口的代码,发现最近没有迭代记录,而且所有机器上部署的代码均一致。
那有没有可能是环境问题?于是我上 Dubbo 控制台查看,发现 provider 和 consumer 都正常:
DubboAdmin
这个时候我开始有点懵逼了😅。

我又想到,如果是一直存在问题,那么不可能到今天才有用户反馈,所以还是跟近期的什么操作有关。于是我开始排查最早是什么时间出现的问题,发现是 10 月 10 日的 18:00:03,又发现F 接口所在的I服务10 月 10 日的 17:57:28有过发布记录。
release

这下就有点奇怪了,就算I服务所有机器同时发布,可能也就是报一会 no providers错误啊,更不用说是滚动发布的,加上 dubbo 的 failover不会一直报错的啊。

就在这时我看到了一个关键的错误信息:(之前只看到了 NPE 没具体位置):
Error

问题直指DubboInvoker:109。

我们 dubbo的版本是(2.6.6),我拉下dubbo 代码发现这块的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

@Override
public boolean isAvailable() {
if (!super.isAvailable())
return false;
for (ExchangeClient client : clients) {
//这一行是 109,就是这一行报错
if (client.isConnected() && !client.hasAttribute(Constants.CHANNEL_ATTRIBUTE_READONLY_KEY)) {
//cannot write == not Available ?
return true;
}
}
return false;
}

109 行报 NPE 莫非 client 是 null?带上猜测我开始看 client 是怎么来的,最终找到com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#getSharedClient:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private ExchangeClient getSharedClient(URL url) {
String key = url.getAddress();
ReferenceCountExchangeClient client = referenceClientMap.get(key);
if (client != null) {
if (!client.isClosed()) {
client.incrementAndGetCount();
return client;
} else {
//3
referenceClientMap.remove(key);//4
}
}

locks.putIfAbsent(key, new Object());
synchronized (locks.get(key)) {//2
if (referenceClientMap.containsKey(key)) {
return referenceClientMap.get(key);//5
}

ExchangeClient exchangeClient = initClient(url);
client = new ReferenceCountExchangeClient(exchangeClient, ghostClientMap);
referenceClientMap.put(key, client);
ghostClientMap.remove(key);
locks.remove(key);
//1
return client;
}
}

又是一段平平无奇的代码😅,看到这里有用到锁,说明开发者考虑到这里可能会存在并发调用。我又看了下日志,10 月 10 日的 18:00:03前后确实出现了并发调用的现象,难道是这里的并发控制有 bug?于是我开始思考各种场景,还真被我发现一种可能出问题的情况,下面我画了个图演示一下:

flow

最后 序号 5 处返回了一个 null 的 client。

由于2.6.6 版本已经有点老,指不定这个问题已经有人提过,于是我到 github 一番搜索,结果找到了另外一个问题:https://github.com/apache/dubbo/issues/6444

这块dubbo 后面也迭代了好几次,已经搞不清楚了。

最后,重启大法好!

关于 java 中的可用内存问题

最近看了《why 技术》的一篇文章《这个队列的思路是真的好,现在它是我简历上的亮点了。》。本来没什么问题,觉得这么好的东西不拿来用一用太可惜了。于是我就搞到了项目中,但是却发现一个问题,文中提到的:

1
2
MemoryUsage heapMemoryUsage = MX_BEAN.getHeapMemoryUsage();
long availableMemory = heapMemoryUsage.getCommitted();

我发现availableMemory在一段时间内没什么变化。

我是这么测试的:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws InterruptedException {
List<byte[]> objects = new ArrayList<>();
long _1MB = 1024 * 1024;
while (true) {
MemoryUsage heapMemoryUsage = MX_BEAN.getHeapMemoryUsage();
System.out.println("committed:"+heapMemoryUsage.getCommitted()/_1MB);
objects.add(new byte[1024 * 1024 * 50]);
Thread.sleep(100L);
}
}

讲道理应该实时变化才对,后来我点进去了why哥留下的PR链接,发现这个PR在2022-06-06有一个提交:
1
备注信息为:”fix bug”,具体的修改为:
2
可以看到原来通过MemoryUsage获取可用内存变成了:

1
Runtime.getRuntime().freeMemory()

也就是说之前使用的getCommited()获取的并不是可用内存,于是我看了下MemoryUsage的源码(jdk8),哈哈哈😂,发现图示的很清楚:
3
真正的可用空间是committed - used,于是我又做了下面的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws InterruptedException {
List<byte[]> objects = new ArrayList<>();
long _1MB = 1024 * 1024;
while (true) {
MemoryUsage heapMemoryUsage = MX_BEAN.getHeapMemoryUsage();
long freeMemory = Runtime.getRuntime().freeMemory();
System.out.println("freeMemory:"+freeMemory/_1MB);
System.out.println("committed:"+heapMemoryUsage.getCommitted()/_1MB);
System.out.println("committed - used:"+(heapMemoryUsage.getCommitted()-heapMemoryUsage.getUsed())/_1MB);
objects.add(new byte[1024 * 1024 * 50]);
Thread.sleep(100L);
}
}

4

可以看到确实如此。


20220615更新

why哥给我回复了,这个问题大佬也讨论过https://github.com/apache/dubbo/pull/10021#issuecomment-1147464751

其中一个大佬说的可用内存应该是:

1
MemoryUsage#getMax() - MemoryUsage#getUsed()

然后我在 github 上回复了一下大佬,大佬给我的答复是:
5

什么?Lombok有坑?

事情是这样的,前段时间有个线上bug需要我去排查:
1

首先看了CAT的调用链,并没有发现什么问题,所有的服务都是正常处理结束。

于是我要了一个traceId,去排查日志,也是没有发现什么问题。

我拉下了生产版本的代码,走查了一下逻辑,并没有发现什么大问题,所以这注定是一个细节问题。

由于这是一个可以复现的问题,所以让测试大佬在测试环境模拟了下有问题的数据,于是我开始Debug了。

一行一行代码的看,一个一个变量的watch,最终发现了问题。

2

首先是同事新增了如下代码:

3

然后数据的OwnerId都是null:

4

紧接着执行了这个removeAll:

5

可是这为什么会出问题呢?

原来是list中的对象,是同事新加的,继承了原有的对象:

6

并且使用了Lombok的Data注解。这个注解会生成equals方法:

7

removeAll是调用的equals判断对象是否相同,equals方法重写后,导致只要这两个对象的ownerId都为空就会认为相同。

可以看到一个@Data注解相当于加了6个注解:

8

怎么解决这个问题呢?既然问题出在equals,那么有没有什么办法让子类生成的equals中也能包含基类的属性呢?

是有的:

1
@EqualsAndHashCode(callSuper = true)

加上这个注解就可以了。

可以看到@Data注解做的事情太多了,会发生你意想不到的情况,实际是不太推荐无脑使用的,建议还是自己组合需要的注解。

Spotlight替代工具uTools及插件分享

Mac自带的Spotlight对于普通用户来说已经够用了,高阶一点的会使用alfred来增强,但是alfred交互方式比较单一,通常是列表方式,也不支持通过输入内容匹配wf,通常需要keyword唤醒。而且alfred是收费软件。

uTools

uTools 也是一款Spotlight增强应用,默认的唤醒快捷键是option+space,墙裂推荐改为double command。应用的主界面长这样:

uTools应用的逻辑有点像微信小程序,可以通过keyword呼出小程序,也可以通过剪切板内容自动识别出来。

阅读更多

Dubbo调用超时那些事儿

其实之前很早就看过dubbo源码中关于超时这部分的处理逻辑,但是没有记录下来,最近在某脉上看到有人问了这个问题,想着再回顾一下。

开始

从dubbo的请求开始,看看dubbo(2.6.6)在超时这块是怎么处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeChannel#request(java.lang.Object, int) 
@Override
public ResponseFuture request(Object request, int timeout) throws RemotingException {
if (closed) {
throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
}
// create request.
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setData(request);
DefaultFuture future = new DefaultFuture(channel, req, timeout);
try {
channel.send(req);
} catch (RemotingException e) {
future.cancel();
throw e;
}
return future;
}
阅读更多

Java双亲委派机制的妙用

最近在项目中看到一段通过easyexcel导出动态表头的实现,开始我以为是easyexcel官方的实现,其中有这样一段代码:

1
2
3
4
5
6
7
8
//将动态表头上传至ThreadLocal
saveToThreadLocal(clz, result);

private <T> void saveToThreadLocal(Class<T> clz, List<String> result) {
Map<Class,List<String>> paramMap = new ConcurrentHashMap<>();
paramMap.put(clz, result);
ThreadLocalUtil.FIELD_CACHE_MAP.set(paramMap);
}
阅读更多