2023年10月

本文详尽地探讨了Go语言的内建命令集,包括但不限于go build、go run、go get等。文章首先列举了所有常用的Go命令,并用表格形式简洁地解释了它们的功能。随后,我们逐一深入讲解了每个命令的使用说明、应用场景,以及实际操作中可能遇到的输出结果。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

file

一、Go命令全列表

在这部分,我们将通过一个表格来快速浏览Go语言的所有内建命令及其基本功能。这些命令涵盖了从代码构建、测试,到依赖管理和其他工具等方面。

命令 功能描述
go build 编译Go源文件
go run 编译并运行Go程序
go get 下载并安装依赖或项目
go mod Go模块支持
go list 列出包或模块
go fmt 格式化代码
go vet 静态检查代码
go test 运行测试
go doc 查看文档
go env 打印Go环境信息
go clean 删除编译生成的文件
go tool 运行指定的go工具
go version 打印Go当前版本
go install 编译和安装Go程序或库
go generate 通过处理源生成Go文件
go fix 更新包以使用新的API
go workspace 管理Go工作区(实验性)
go help 查看命令或主题的帮助信息

这个表格提供了一个快速参考,使你能更方便地理解每个命令的基本用途。


二、Go命令全使用展示

file
在这一部分,我们将逐一介绍上述表格中的Go命令。我们将探讨每个命令的详细说明、使用场景,以及命令使用后的实际返回案例。

go build

命令说明

go build
命令用于编译Go源文件。该命令会根据源代码生成可执行文件或库。

使用场景

  • 编译单个Go文件或整个项目
  • 创建库文件
  • 交叉编译

实际返回案例

$ go build hello.go
# 无输出,但会生成一个名为hello的可执行文件

go run

命令说明

go run
命令用于编译并运行Go程序。适用于快速测试代码片段。

使用场景

  • 快速测试小程序
  • 不需要生成持久的可执行文件

实际返回案例

$ go run hello.go
Hello, world!

go get

命令说明

go get
用于下载并安装依赖或项目。

使用场景

  • 下载第三方库
  • 更新项目依赖

实际返回案例

$ go get github.com/gin-gonic/gin
# 下载并安装gin库,无输出

go mod

命令说明

go mod
用于Go模块支持,包括初始化、添加依赖等。

使用场景

  • 初始化新项目
  • 管理项目依赖

实际返回案例

$ go mod init my-module
go: creating new go.mod: module my-module

go list

命令说明

go list
用于列出包或模块。

使用场景

  • 查看当前项目依赖
  • 查看全局安装的包

实际返回案例

$ go list ./...
# 列出当前项目所有包

go fmt

命令说明

go fmt
用于自动格式化Go源代码。

使用场景

  • 代码审查
  • 统一代码风格

实际返回案例

$ go fmt hello.go
# 格式化hello.go文件,返回格式化后的文件名
hello.go

go vet

命令说明

go vet
用于对Go代码进行静态分析,检查可能存在的错误。

使用场景

  • 代码质量检查
  • 发现潜在问题

实际返回案例

$ go vet hello.go
# 若代码无问题,则没有输出

go test

命令说明

go test
用于运行Go程序的测试。

使用场景

  • 单元测试
  • 性能测试

实际返回案例

$ go test
ok      github.com/yourusername/yourpackage 0.002s

go doc

命令说明

go doc
用于查看Go语言标准库或你的代码库中的文档。

使用场景

  • 查找库函数说明
  • 查看接口文档

实际返回案例

$ go doc fmt.Println
func Println(a ...interface{}) (n int, err error)

go env

命令说明

go env
用于打印Go的环境信息。

使用场景

  • 环境配置
  • 问题诊断

实际返回案例

$ go env
GOARCH="amd64"
GOBIN=""
...

go clean

命令说明

go clean
用于删除编译生成的文件。

使用场景

  • 清理项目目录
  • 回复到初始状态

实际返回案例

$ go clean
# 删除编译生成的文件,无输出

go tool

命令说明

go tool
用于运行指定的Go工具。

使用场景

  • 编译优化
  • 调试

实际返回案例

$ go tool compile hello.go
# 编译hello.go,生成中间文件

go version

命令说明

go version
用于打印当前Go的版本信息。

使用场景

  • 版本检查
  • 依赖分析

实际返回案例

$ go version
go version go1.17.1 linux/amd64

go install

命令说明

go install
用于编译和安装Go程序或库。

使用场景

  • 创建可分发的二进制文件
  • 安装库到系统路径

实际返回案例

$ go install hello.go
# 编译并安装hello程序,无输出

go generate

命令说明

go generate
用于通过处理源代码来生成Go文件。

使用场景

  • 代码生成
  • 模板处理

实际返回案例

$ go generate
# 运行生成指令,生成代码,无输出

go fix

命令说明

go fix
用于更新包以使用新的API。

使用场景

  • API迁移
  • 自动修复代码

实际返回案例

$ go fix oldpackage
# 更新oldpackage包的API调用,无输出

go workspace

命令说明

go workspace
用于管理Go工作区。这是一个实验性功能。

使用场景

  • 多项目管理
  • 环境隔离

实际返回案例

$ go workspace create myworkspace
# 创建名为myworkspace的工作区,无输出

go help

命令说明

go help
用于查看命令或主题的帮助信息。

使用场景

  • 查找命令用法
  • 学习Go工具链

实际返回案例

$ go help build
# 显示go build命令的详细帮助信息

以上便是Go命令的全使用展示。每个命令都有其特定的用途和使用场景,深入了解这些将极大地提高你的开发效率。希望这篇文章能为你的Go开发之旅提供有用的信息和实践指导。

I
file

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。
如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。

简介:

问题:因项目需要,软件需要读取授权文件中的密文与本机验证码做一定的逻辑比对,使用FileStream实现文件的读取,在本机调试没问题,但在其他同事电脑上有一些出现授权一直不通过的情况。

--MaQaQ 2023-10-24

分析:

1、首先怀疑是否授权文件生成出错,反复生成了几遍,还确认了下文件中的密文,出错的可能性不大,pass。

2、其次怀疑是部署的电脑环境问题,巧合的是,授权不通过的电脑刚好是win11,而我本机是win10,一度让我怀疑是操作系统问题,但这个也太玄学了,先搁置。

3、查看了下授权验证的逻辑,发现抛异常了也会导致验证失败,折腾了一下最后还是定位到异常的位置:

using (Stream stream = new FileStream(fullName, FileMode.Open))

获取到的异常信息类似:System.UnauthorizedAccessException:“对路径“xxx”的访问被拒绝。这个我就很熟悉了,一般是因为权限问题,右键点开授权文件的属性一看,果然只读被勾上了。

4、检查了下一开始生成的授权文件,只读属性是没有勾选的,所以在本机调试没问题。那么问题是出在发送和接收文件这块。

5、我们发送授权文件时是直接用的微信,但前面说到,测试时只是部分电脑出问题,于是我问了下同事的接收方法,有些是收到文件直接复制,有些是右键另存为,我自己测试了下,确实直接复制的会被改成只读,到此真相大白。

6、另外,如果我们对文件只需要读取,那么可以将上述抛异常的代码改为:

using (Stream stream = new FileStream(fullName, FileMode.Open, FileAccess.Read))

就可以打开只读的文件,如果需要的是读写的权限,那么还是需要手动将属性中的只读去掉勾选。

总结:

1、微信直接复制的文件是只读的,可以使用另存为

2、对于只读的文件,可以将访问模式设置为Read

一、需求

  1. 了解dumpsys原理,助于我们进一步了解Android系统的设计
  2. 帮助我们分析问题,定位系统状态
  3. 设计新功能的需要

二、环境

  1. 版本:Android 12
  2. 平台:SL8541E SPRD

三、相关概念

3.1 dumpsys

dumpsys 是一种在 Android 设备上运行的工具,可提供有关系统服务的信息。可以使用 Android 调试桥 (adb) 从命令行调用 dumpsys,获取在连接的设备上运行的所有系统服务的诊断输出。

3.2 Binder

Binder是Android提供的一套进程间相互通信框架。用来实现多进程间发送消息,同步和共享内存。

3.3 管道

管道是一种IPC通信方式,分为有名管道和无名管道,无论是有名管道还是无名管道其原理都是在内核开辟一块缓存空间,这段缓存空间的操作是通过文件读写方式进行的。
有名管道与无名管道:
有名管道:
有名管道的通信可以通过管道名进行通信,进程间不需要有关系。
无名管道:
无名管道就是匿名管道,匿名管道通信的进程必须是父子进程。
管道为分半双工和全双工:
半双工:
半双工管道是单向通信,进程1只能向管道写数据,进程2只能从管道读取数据。只有一个代表读或者写的FD(文件描述符)。
全双工:
全双工管道是双向通信,有两个文件描述符,代表读和写。

四、dumpsys指令的使用

4.1 dumpsys使用

如下为执行"adb shell dumpsys"指令,控制台打印的内容,其使用如下:

4.2 dumpsys指令语法

(1)使用 dumpsys 的一般语法如下:

adb shell dumpsys [-t timeout] [--help | -l | --skip services | service [arguments] | -c | -h]

(2)如需获取所连接设备的所有系统服务的诊断输出,请运行 adb shell dumpsys。不过,这样输出的信息比您通常想要的信息多得多。若要使输出更加可控,您可以通过在命令中添加相应服务来指定要检查的服务。例如,下面的命令会提供输入组件(如触摸屏或内置键盘)的系统数据:

adb shell dumpsys input

(3)如需查看可与 dumpsys 配合使用的系统服务的完整列表,请使用以下命令:

adb shell dumpsys -l

(4)命令行选项如下:

选项 说明
-t timeout 指定超时期限(秒)。如果未指定,默认值为 10 秒。
--help 输出 dumpsys 工具的帮助文本。
-l 输出可与 dumpsys 配合使用的系统服务的完整列表。
--skip services 指定您不希望包含在输出中的 services。
service [arguments] 指定您希望输出的 service。某些服务可能允许您传递可选 arguments。如需了解这些可选参数,请将 -h 选项与服务一起传递:
adb shell dumpsys procstats -h
-c 指定某些服务时,附加此选项能以计算机可读的格式输出数据。
-h 对于某些服务,附加此选项可查看该服务的帮助文本和其他选项。

五、详细设计

5.1 dumpsys流程图

5.2 dumpsys查看电池信息

5.2.1 dumpsys battery指令

5.2.2 service->dump打印函数

@frameworks\base\services\core\java\com\android\server\BatteryService.java
private final class BinderService extends Binder {
    @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
            if (args.length > 0 && "--proto".equals(args[0])) {
                dumpProto(fd);
            } else {
                dumpInternal(fd, pw, args);
            }
        }
    ...
}

private void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) {
    synchronized (mLock) {
        if (args == null || args.length == 0 || "-a".equals(args[0])) {
            pw.println("Current Battery Service state:");
            if (mUpdatesStopped) {
                pw.println("  (UPDATES STOPPED -- use 'reset' to restart)");
            }
            pw.println("  AC powered: " + mHealthInfo.chargerAcOnline);
            pw.println("  USB powered: " + mHealthInfo.chargerUsbOnline);
            pw.println("  Wireless powered: " + mHealthInfo.chargerWirelessOnline);
            pw.println("  Max charging current: " + mHealthInfo.maxChargingCurrent);
            pw.println("  Max charging voltage: " + mHealthInfo.maxChargingVoltage);
            pw.println("  Charge counter: " + mHealthInfo.batteryChargeCounter);
            pw.println("  status: " + mHealthInfo.batteryStatus);
            pw.println("  health: " + mHealthInfo.batteryHealth);
            pw.println("  present: " + mHealthInfo.batteryPresent);
            pw.println("  level: " + mHealthInfo.batteryLevel);
            pw.println("  scale: " + BATTERY_SCALE);
            pw.println("  voltage: " + mHealthInfo.batteryVoltage);
            pw.println("  temperature: " + mHealthInfo.batteryTemperature);
            pw.println("  technology: " + mHealthInfo.batteryTechnology);
        } else {
            Shell shell = new Shell();
            shell.exec(mBinderService, null, fd, null, args, null, new ResultReceiver(null));
        }
    }
}

5.3 dumpsys源码分析

5.3.1 dumpsys服务编译

dumpsys是个二进制可执行程序,其通过bp进行编译,并最终打包到system分区(system/bin/dumpsys)。

@frameworks\native\cmds\dumpsys\android.bp
cc_binary {
    name: "dumpsys",

    defaults: ["dumpsys_defaults"],

    srcs: [
        "main.cpp",
    ],
}

5.3.2 dumpsys入口函数

我们通过执行adb指令
"adb shell dumpsys"
,可以启动dumpsys服务,其对应的入口函数如下:

@frameworks\native\cmds\dumpsys\main.cpp
int main(int argc, char* const argv[]) {
    signal(SIGPIPE, SIG_IGN);
    sp<IServiceManager> sm = defaultServiceManager();//获取SM对象
    fflush(stdout);
    if (sm == nullptr) {
        ALOGE("Unable to get default service manager!");
        std::cerr << "dumpsys: Unable to get default service manager!" << std::endl;
        return 20;
    }

    Dumpsys dumpsys(sm.get());
    return dumpsys.main(argc, argv);//进入dumpsys服务
}

这边比较关键的点是获取
ServiceManager对象

大家通过打印可以发现,dumpsys指令打印的数据是java进程的dump函数,而dumpsys也是独立的一个进程,那么dumpsys进程又是怎么和多个java进程通信的呢?没错,就是通过ServiceManager对象。
那么,ServiceManager对象是什么呢?ServiceManager是
Binder IPC通信
的管家,本身也是一个Binder服务,他相当于 “DNS服务器”,内部存储了serviceName与其Binder Service的对应关系,管理Java层和native层的service,支持addService()、getService()、checkService、listServices()等功能。(Binder机制此处就不展开细说)

5.3.3 dumpsys服务打印

5.3.3.1 dumpsys解析参数

当我们使用dumpsys指令,打印的数据太过冗长,一般会配合相关参数进行使用,例如:"dumpsys -l"、"dumpsys -t 100 battery"、"dumpsys --help",第一步我们会先解析目标参数。

@frameworks\native\cmds\dumpsys\dumpsys.cpp
int Dumpsys::main(int argc, char* const argv[]) {
    ...
    while (1) {
        ...
        c = getopt_long(argc, argv, "+t:T:l", longOptions, &optionIndex);//获取指令参数
        ...
        switch (c) {
        case 0://长参数
            if (!strcmp(longOptions[optionIndex].name, "skip")) {//跳过某些服务打印
                skipServices = true;
            } else if (!strcmp(longOptions[optionIndex].name, "proto")) {
                asProto = true;
            } else if (!strcmp(longOptions[optionIndex].name, "help")) {//指令帮助
                usage();
                return 0;
            } else if (!strcmp(longOptions[optionIndex].name, "priority")) {
                ...
            } else if (!strcmp(longOptions[optionIndex].name, "pid")) {//只显示服务的pid
                type = Type::PID;
            } else if (!strcmp(longOptions[optionIndex].name, "thread")) {//仅显示进程使用情况
                type = Type::THREAD;
            }
            break;
        case 't'://超时时间设置,默认10秒
            ...
            break;
        case 'T'://超时时间设置,默认10秒
            ...
            break;
        case 'l'://显示支持的服务列表
            showListOnly = true;
            break;
        default://其他参数
            fprintf(stderr, "\n");
            usage();
            return -1;
        }
    }
    ...
}

5.3.3.2 skippedServices列表构造

dumpsys内部构造了skippedServices集合,用于记录需要忽略的服务。

@frameworks\native\cmds\dumpsys\dumpsys.cpp
int Dumpsys::main(int argc, char* const argv[]) {
    ...
    for (int i = optind; i < argc; i++) {
    if (skipServices) {
        skippedServices.add(String16(argv[i]));//配置待忽略的服务
    } else {
        ...
    }
    ...
}

5.3.3.3 获取支持服务列表

dumpsys通过ServiceManager获取支持的服务集合,并排序。

@frameworks\native\cmds\dumpsys\dumpsys.cpp
int Dumpsys::main(int argc, char* const argv[]) {
    ...
    if (services.empty() || showListOnly) {
        services = listServices(priorityFlags, asProto);
        setServiceArgs(args, asProto, priorityFlags);
    }
    ...
}

Vector<String16> Dumpsys::listServices(int priorityFilterFlags, bool filterByProto) const {
    Vector<String16> services = sm_->listServices(priorityFilterFlags);//通过sm获取服务集合
    services.sort(sort_func);//集合排序
    ...
    return services;
}

5.3.3.4 打印支持服务列表

在获取了服务集合后,会先检查服务是否存在,接着打印服务的名称,且如果当前指令设置了"-l"参数,仅打印服务集合,即流程结束。

@frameworks\native\cmds\dumpsys\dumpsys.cpp
int Dumpsys::main(int argc, char* const argv[]) {
    ...
    const size_t N = services.size();//获取支持的服务个数
    if (N > 1 || showListOnly) {
        // first print a list of the current services
        std::cout << "Currently running services:" << std::endl;

        for (size_t i=0; i<N; i++) {
            sp<IBinder> service = sm_->checkService(services[i]);//检查服务状态

            if (service != nullptr) {
                bool skipped = IsSkipped(skippedServices, services[i]);
                std::cout << "  " << services[i] << (skipped ? " (skipped)" : "") << std::endl;//打印服务名称
            }
        }
    }

    if (showListOnly) {//如果指令仅需要打印服务集合,则结束。
        return 0;
    }
    ...
}

5.3.3.5 打印目标服务

先遍历所有需要打印的服务,如果参数有指定服务名,即N为对应服务的数量,否则N为所有支持的服务数量。接着,开启线程,通过servicemanager调用远端的dump函数,利用管道和poll机制监听远端数据。最后如果超时或者dump结束,则关闭线程,释放相关资源。

@frameworks\native\cmds\dumpsys\dumpsys.cpp
int Dumpsys::main(int argc, char* const argv[]) {
    ...
    for (size_t i = 0; i < N; i++) {
        const String16& serviceName = services[i];
        if (IsSkipped(skippedServices, serviceName)) continue;//跳过部分服务

        if (startDumpThread(type, serviceName, args) == OK) {//step 1.创建dump打印的线程
            ...
            std::chrono::duration<double> elapsedDuration;
            size_t bytesWritten = 0;
            status_t status =
                writeDump(STDOUT_FILENO, serviceName, std::chrono::milliseconds(timeoutArgMs),
                          asProto, elapsedDuration, bytesWritten);//step 2.dump执行打印操作

            if (status == TIMED_OUT) {//打印超时
                std::cout << std::endl
                     << "*** SERVICE '" << serviceName << "' DUMP TIMEOUT (" << timeoutArgMs
                     << "ms) EXPIRED ***" << std::endl
                     << std::endl;
            }
            ...
            bool dumpComplete = (status == OK);
            stopDumpThread(dumpComplete);//step 3.结束dump打印线程
        }
    }
    ...
}

step 1. 创建dumpsys打印线程
创建了一条管道,接着开启了一个线程,通过ServiceManager对象读取目标服务的dump函数,即dump打印数据。

@frameworks\native\cmds\dumpsys\dumpsys.cpp
status_t Dumpsys::startDumpThread(Type type, const String16& serviceName,
                                  const Vector<String16>& args) {
    sp<IBinder> service = sm_->checkService(serviceName);//通过SM获取service对象
    int sfd[2];
    if (pipe(sfd) != 0) {//创建管道,用于读取service端数据
        ...
    }
    ...
    redirectFd_ = unique_fd(sfd[0]);
    unique_fd remote_end(sfd[1]);
    sfd[0] = sfd[1] = -1;

    // dump blocks until completion, so spawn a thread..
    activeThread_ = std::thread([=, remote_end{std::move(remote_end)}]() mutable {//创建线程
        status_t err = 0;

        switch (type) {
        case Type::DUMP:
            err = service->dump(remote_end.get(), args);//调用dump函数
            break;
        ...
        }
        ...
    });
    return OK;
}

step 2. dumpsys打印到终端
通过poll机制用来监听管道的数据,并将读取到的dump数据,打印至控制台。同时,通过计算剩余的时间,来判断当前是否读取超时。

@frameworks\native\cmds\dumpsys\dumpsys.cpp
status_t Dumpsys::writeDump(int fd, const String16& serviceName, std::chrono::milliseconds timeout,
                            bool asProto, std::chrono::duration<double>& elapsedDuration,
                            size_t& bytesWritten) const {
    ...
    int serviceDumpFd = redirectFd_.get();
    struct pollfd pfd = {.fd = serviceDumpFd, .events = POLLIN};

    while (true) {
        ...
        int rc = TEMP_FAILURE_RETRY(poll(&pfd, 1, time_left_ms()));//poll机制检测管道数据
        if (rc < 0) {
           ...
        } else if (rc == 0 || time_left_ms() == 0) {
            status = TIMED_OUT;//计算剩余时间,来决定是否超时
            break;
        }

        char buf[4096];
        rc = TEMP_FAILURE_RETRY(read(redirectFd_.get(), buf, sizeof(buf)));//读取远端的数据
        ...
        if (!WriteFully(fd, buf, rc)) {//打印至控制台
            ...
            break;
        }
        totalBytes += rc;
    }
    ...
    return status;
}

step 3. 关闭dumpsys打印线程
将dumpsys打印的线程detach掉,相关的fd句柄reset掉,释放资源。

六、dumpsys的应用

如后续有什么应用dumpsys,或者有助于日常开发调试的场景,再补充,未完待续。

6.1 dumpsys常用指令

服务名 类名 指令 功能
activity ActivityManagerService 获取某个应用的Activity信息:
adb shell dumpsys activity a packagename
获取某个应用的Service信息:
adb shell dumpsys activity s packagename
获取某个应用的Broadcast信息:
adb shell dumpsys activity b packagename
获取某个应用的Provider信息:
adb shell dumpsys activity prov packagename
获取某个应用的进程状态:
adb shell dumpsys activity p packagename
获取当前界面的Activity信息:
adb shell dumpsys activity top | grep ACTIVITY
AMS相关信息
package PackageManagerService adb shell dumpsys package PMS相关信息
window WindowManagerService adb shell dumpsys window WMS相关信息
input InputManagerService adb shell dumpsys input IMS相关信息
power PowerManagerService adb shell dumpsys power PMS相关信息
batterystats BatterystatsService adb shell dumpsys batterystats 电池统计信息
battery BatteryService adb shell dumpsys battery 电池信息
alarm AlarmManagerService adb shell dumpsys alarm 闹钟信息
dropbox DropboxManagerService adb shell dumpsys dropbox 调试相关
procstats ProcessStatsService adb shell dumpsys procstats 进程统计
cpuinfo CpuBinder adb shell dumpsys cpuinfo CPU
meminfo MemBinder adb shell dumpsys meminfo 内存
gfxinfo GraphicsBinder adb shell dumpsys gfxinfo 图像
dbinfo DbBinder adb shell dumpsys dbinfo 数据库

七、参考资料

dumpsys指令介绍:
https://developer.android.google.cn/studio/command-line/dumpsys?hl=zh-cn
管道:
https://www.cnblogs.com/naray/p/15365954.html
Binder:
https://blog.csdn.net/shenxiaolinil/article/details/128972302

Why?

为什么要对方法的返回值进行缓存呢?

简单来说是为了提升后端程序的性能和提高前端程序的访问速度。减小对db和后端应用程序的压力。

一般而言,缓存的内容都是不经常变化的,或者轻微变化对于前端应用程序是可以容忍的。

否则,不建议加入缓存,因为增加缓存会使程序复杂度增加,还会出现一些其他的问题,比如缓存同步,数据一致性,更甚者,可能出现经典的缓存穿透、缓存击穿、缓存雪崩问题。

HowDo

如何缓存方法的返回值?应该会有很多的办法,本文简单描述两个比较常见并且比较容易实现的办法:

  • 自定义注解
  • SpringCache

annotation

整体思路:

第一步:定义一个自定义注解,在需要缓存的方法上面添加此注解,当调用该方法的时候,方法返回值将被缓存起来,下次再调用的时候将不会进入该方法。其中需要指定一个缓存键用来区分不同的调用,建议为:类名+方法名+参数名

第二步:编写该注解的切面,根据缓存键查询缓存池,若池中已经存在则直接返回不执行方法;若不存在,将执行方法,并在方法执行完毕写入缓冲池中。方法如果抛异常了,将不会创建缓存

第三步:缓存池,首先需要尽量保证缓存池是线程安全的,当然了没有绝对的线程安全。其次为了不发生缓存臃肿的问题,可以提供缓存释放的能力。另外,缓存池应该设计为可替代,比如可以丝滑得在使用程序内存和使用redis直接调整。

MethodCache

创建一个名为MethodCache 的自定义注解


package com.ramble.methodcache.annotation;
import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MethodCache {

}


MethodCacheAspect

编写MethodCache注解的切面实现


package com.ramble.methodcache.annotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Aspect
@Component
public class MethodCacheAspect {

    private static final Map<String, Object> CACHE_MAP = new ConcurrentHashMap<>();
    
    @Around(value = "@annotation(methodCache)")
    public Object around(ProceedingJoinPoint jp, MethodCache methodCache) throws Throwable {
        String className = jp.getSignature().getDeclaringType().getSimpleName();
        String methodName = jp.getSignature().getName();
        String args = String.join(",", Arrays.toString(jp.getArgs()));
        String key = className + ":" + methodName + ":" + args;
        // key 示例:DemoController:findUser:[FindUserParam(id=1, name=c7)]
        log.debug("缓存的key={}", key);
        Object cache = getCache(key);
        if (null != cache) {
            log.debug("走缓存");
            return cache;
        } else {
            log.debug("不走缓存");
            Object value = jp.proceed();
            setCache(key, value);
            return value;
        }
    }
    
    private Object getCache(String key) {
        return CACHE_MAP.get(key);
    }
    
    private void setCache(String key, Object value) {
        CACHE_MAP.put(key, value);
    }
}


  • Around:对被MethodCache注解修饰的方法启用环绕通知
  • ProceedingJoinPoint:通过此对象获取方法所在类、方法名和参数,用来组装缓存key
  • CACHE_MAP:缓存池,生产环境建议使用redis等可以分布式存储的容器,直接放程序内存不利于后期业务扩张后多实例部署

controller


package com.ramble.methodcache.controller;
import com.ramble.methodcache.annotation.MethodCache;
import com.ramble.methodcache.controller.param.CreateUserParam;
import com.ramble.methodcache.controller.param.FindUserParam;
import com.ramble.methodcache.service.DemoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Tag(name = "demo - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo")
public class DemoController {

    private final DemoService demoService;
    
    @MethodCache
    @GetMapping("/{id}")
    public String getUser(@PathVariable("id") String id) {
        return demoService.getUser(id);
    }
    
    @Operation(summary = "查询用户")
    @MethodCache
    @PostMapping("/list")
    public String findUser(@RequestBody FindUserParam param) {
        return demoService.findUser(param);
    }
}


通过反复调用被@MethodCache注解修饰的方法,会发现若缓存池有数据,将不会进入方法体。

SpringCache

其实SpringCache的实现思路和上述方法基本一致,SpringCache提供了更优雅的编程方式,更丝滑的缓存池切换和管理,更强大的功能和统一规范。

EnableCaching

使用 @EnableCaching 开启SpringCache功能,无需引入额外的pom。

默认情况下,缓存池将由 ConcurrentMapCacheManager 这个对象管理,也就是默认是程序内存中缓存。其中用于存放缓存数据的是一个 ConcurrentHashMap,源码如下:


public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {

    private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
    
   
    ......
    
}


此外可选的缓存池管理对象还有:

  • EhCacheCacheManager

  • JCacheCacheManager

  • RedisCacheManager

  • ......

Cacheable


package com.ramble.methodcache.controller;
import com.ramble.methodcache.controller.param.FindUserParam;
import com.ramble.methodcache.service.DemoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;

@Tag(name = "user - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {

    private final DemoService demoService;
    
    @Cacheable(value = "userCache")
    @GetMapping("/{id}")
    public String getUser(@PathVariable("id") String id) {
        return demoService.getUser(id);
    }
    
    @Operation(summary = "查询用户")
    @Cacheable(value = "userCache")
    @PostMapping("/list")
    public String findUser(@RequestBody FindUserParam param) {
        return demoService.findUser(param);
    }
}


  • 使用@Cacheable注解修饰需要缓存返回值的方法
  • value必填,不然运行时报异常。类似一个分组,将不同的数据或者方法(当然也可以其他维度,主要看业务需要)放到一堆,便于管理
  • 可以修饰接口方法,但是不建议,IDEA会报一个提示Spring doesn't recommend to annotate interface methods with @Cache* annotation

常用属性:

  • value:缓存名称
  • cacheNames:缓存名称。value 和cacheNames都被AliasFor注解修饰,他们互为别名
  • key:缓存数据时候的key,默认使用方法参数的值,可以使用SpEL生产key
  • keyGenerator:key生产器。和key二选一
  • cacheManager:缓存管理器
  • cacheResolver:和caheManager二选一,互为别名
  • condition:创建缓存的条件,可用SpEL表达式(如#id>0,表示当入参id大于0时候才缓存方法返回值)
  • unless:不创建缓存的条件,如#result==null,表示方法返回值为null的时候不缓存

CachePut

用来更新缓存。被CachePut注解修饰的方法,在被调用的时候不会校验缓存池中是否已经存在缓存,会直接发起调用,然后将返回值放入缓存池中。

CacheEvict

用来删除缓存,会根据key来删除缓存中的数据。并且不会将本方法返回值缓存起来。

常用属性:

  • value/cacheeName:缓存名称,或者说缓存分组
  • key:缓存数据的键
  • allEntries:是否根据缓存名称清空所有缓存,默认为false。当此值为true的时候,将根据cacheName清空缓存池中的数据,然后将新的返回值放入缓存
  • beforeInvocation:是否在方法执行之前就清空缓存,默认为false

Caching

此注解用于在一个方法或者类上面,同时指定多个SpringCache相关注解。这个也是SpringCache的强大之处,可以自定义各种缓存创建、更新、删除的逻辑,应对复杂的业务场景。

属性:

  • cacheable:指定@Cacheable注解
  • put:指定@CachePut注解
  • evict:指定@CacheEvict注解

源码:


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
    Cacheable[] cacheable() default {};

    CachePut[] put() default {};

    CacheEvict[] evict() default {};
}

相当于就是注解里面套注解,用来完成复杂和多变的场景,这个设计相当的哇塞。

CacheConfig

放在类上面,那么类中所有方法都会被缓存

SpringCacheEnv

SpringCache内置了一些环境变量,可用于各个注解的属性中。

  • methodName:被修饰方法的方法名

  • method:被修饰方法的Method对象

  • target:被修饰方法所属的类对象的实例

  • targetClass:被修饰方法所属类对象

  • args:方法入参,是一个 object[] 数组

  • caches:这个对象其实就是ConcurrentMapCacheManager中的cacheMap,这个cacheMap呢就是一开头提到的ConcurrentHashMap,即缓存池。caches的使用场景尚不明了。

  • argumentName:方法的入参

  • result:方法执行的返回值

使用示例:


@Cacheable(value = "userCache", condition = "#result!=null",unless = "#result==null")
public String showEnv() { 
    return "打印各个环境变量";
 }

表示仅当方法返回值不为null的时候才缓存结果,这里通过result env 获取返回值。

另外,condition 和 unless 为互补关系,上述condition = "#result!=null"和unless = "#result==null"其实是一个意思。


@Cacheable(value = "userCache", key = "#name")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}

表示使用方法入参作为该条缓存数据的key,若传入的name为gg,则实际缓存的数据为:gg->打印各个环境变量

另外,如果name为空会报异常,因为缓存key不允许为null


@Cacheable(value = "userCache",key = "#root.args")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}

表示使用方法的入参作为缓存的key,若传递的参数为id=100,name=gg,则实际缓存的数据为:Object[]->打印各个环境变量,Object[]数组中包含两个值。

既然是数组,可以通过下标进行访问,root.args[1] 表示获取第二个参数,本例中即 取 name 的值 gg,则实际缓存的数据为:gg->打印各个环境变量。


@Cacheable(value = "userCache",key = "#root.targetClass")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}

表示使用被修饰的方法所属的类作为缓存key,实际缓存的数据为:Class->打印各个环境变量,key为class对象,不是全限定名,全限定名是一个字符串,这里是class对象。

可是,不是很懂这样设计的应用场景是什么......


@Cacheable(value = "userCache",key = "#root.target")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}

表示使用被修饰方法所属类的实例作为key,实际缓存的数据为:UserController->打印各个环境变量。

被修饰的方法就是在UserController中,调试的时候甚至可以获取到此实例注入的其它容器对象,如userService等。

可是,不是很懂这样设计的应用场景是什么......


@Cacheable(value = "userCache",key = "#root.method")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}

表示使用Method对象作为缓存的key,是Method对象,不是字符串。

可是,不是很懂这样设计的应用场景是什么......


@Cacheable(value = "userCache",key = "#root.methodName")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}

表示使用方法名作为缓存的key,就是一个字符串。

如何获取缓存的数据?

ConcurrentMapCacheManager的cacheMap是一个私有变量,所以没有办法可以打印缓存池中的数据,不过可以通过调试的方式进入对象内部查看。如下:


@Tag(name = "user - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {

    private final ConcurrentMapCacheManager cacheManager;
    
    /**
     * 只有调试才课可以查看缓存池中的数据
     */
    @GetMapping("/cache")
    public void showCacheData() {
        //需要debug进入
        Collection<String> cacheNames = cacheManager.getCacheNames();
    }
    
}


总结:

虽然提供了很多的环境变量,但是大多都无法找到对应的使用场景,其实在实际开发中,最常见的就是key的生产,一般而言使用类名+方法名+参数值足矣。

SqEL

参考:
https://juejin.cn/post/6987993458807930893

cite

注意:以下排序不要用于生产环境

1. 睡眠排序

1.1 简介

睡眠排序(Sleep Sort)是一个非常有趣且奇特的排序算法,第一次看到就大吃一惊。睡眠排序并不是一个实际可用于大规模数据排序的算法,而更像是一种编程趣味或者计算机科学的玩笑。原理基于多线程和睡眠的概念,不是传统的比较排序算法。

睡眠排序的主要思想是将待排序的一组整数分配给多个线程,每个线程负责处理一个整数,它们根据整数的值来设置睡眠时间。整数越小,睡眠时间越短,整数越大,睡眠时间越长。当所有线程都完成睡眠后,它们按照睡眠时间的长短排列,从而实现排序。

主要思路:

  1. 创建一个数组,其中包含待排序的整数。
  2. 对于数组中的每个整数,创建一个线程来处理它。
  3. 每个线程计算自己要休眠的时间,通常是整数值乘以一个常数,以确保休眠时间的差异。
  4. 所有线程同时开始休眠。
  5. 当一个线程醒来后,它将自己的整数放入一个结果数组中。
  6. 重复步骤4和步骤5,直到所有线程都完成。
  7. 最后,结果数组中的整数按照休眠时间的升序排列,即得到排序后的结果。

限制:

  1. 不稳定性:由于线程的调度和休眠不稳定,这个算法不保证排序的稳定性。
  2. 效率问题:算法的效率极低,它的时间复杂度可以达到O(n^2)级别,这在实际应用中不可接受。
  3. 负整数问题:如果待排序数组包含负整数,那么这个算法会在负整数的线程上休眠很长时间,导致等待时间非常长。

1.2 代码实现

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class SleepSort {
    public static void main(String[] args) throws InterruptedException {

        int count = 100;

        int[] nums = new int[count];
        Random random = new Random();
        // 限制程序不要过早中断
        CountDownLatch countDownLatch = new CountDownLatch(count);
        // 随机 100 个 1 - (10 * count) 的数
        for (int i = 0; i < count; i++) {
            nums[i] = random.nextInt(1, 10 * count);
        }
        // 开启虚拟线程,延迟一定时间后打印数字
        for (int i = 0; i < count; i++) {
            int num = nums[i];
            Thread.startVirtualThread(() -> {
                try {
                    Thread.sleep(10L * num);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(num);
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
    }
}

2. 随机排序(猴子排序)

2.1 简介

随机排序,也称为乱序排序(Random Sort)、洗牌排序(Shuffle Sort)或猴子排序(Monkey Sort),特点是将待排序的元素随机排列。

主要思路:

  1. 输入待排序的元素组成的列表或数组。
  2. 对于列表中的每个元素,生成一个随机数或随机权重,并将元素与其对应的随机数关联。
  3. 根据随机数对元素进行排序。这通常可以使用标准的排序算法,如快速排序或归并排序,但比较的标准是元素的随机数而不是元素本身的值。
  4. 完成排序后,元素就会以随机的顺序排列。

限制:

  1. 随机性:由于随机数的引入,每次随机排序的结果都会不同,即使相同的输入数据也会产生不同的输出。
  2. 不稳定性:随机排序通常不保证排序的稳定性,因为它完全依赖于随机数。
  3. 低效性:随机排序的时间复杂度通常较高,取决于用于排序的具体算法。这远不如许多其他排序算法,因此不适合大规模数据集的排序任务。

2.2 代码实现

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class RandomSort {
    public static void main(String[] args) throws InterruptedException {
        // 这个数建议不要太大,不然要随机好久
        int count = 5;
        Random random = new Random();
        // 限制主线程不要退出
        CountDownLatch countDownLatch = new CountDownLatch(1);
        int[] nums = new int[count];
        for (int i = 0; i < count; i++) {
            nums[i] = random.nextInt(10);
        }
        int[] sortedNums = Arrays.copyOf(nums, nums.length);
        // 这里偷懒直接用sort方法,然后在下面直接判断
        Arrays.sort(sortedNums);
        long startTime = System.currentTimeMillis();
        // 这里开一百万个虚拟线程随机
        for (int i = 0; i < 1000000; i++) {
            Thread.startVirtualThread(() -> {
                while (countDownLatch.getCount() > 0){
                    int[] tmp = new int[count];
                    for (int j = 0; j < count; j++) {
                        tmp[j] = random.nextInt(10);
                    }
                    // 可以打印中间过程的输出
                    // System.out.println(Arrays.toString(tmp));
                    if (Arrays.equals(sortedNums, tmp)){
                        System.out.println("排序完毕:未排序数据" + Arrays.toString(nums));
                        System.out.println("排序完毕:已排序数据" + Arrays.toString(tmp));
                        countDownLatch.countDown();
                        break;
                    }
                }
            });
        }
        countDownLatch.await();
        // 计算总时间
        System.out.println(System.currentTimeMillis() - startTime);
    }
}

3. 总结

这两个排序算法可以说是排序算法界的卧龙凤雏,建议大家还是不要在生产环境使用。