2024年7月

前置步骤

首先你需要一套linux服务器,这里默认你已经有了。如果没有可以在
云服务器优惠合集
选择,如果你是个人博客选择性价比最高,最低配置就够用了。

环境搭建

按照Docker官方文档安装Docker和Docker Compose,部分Linux发行版软件仓库中的 Docker版本可能过旧。

创建容器组

下面是Halo官方维护的Docker镜像仓库,根据自己的需求选择合适的镜像源:

  • registry.fit2cloud.com/halo/halo
  • halohub/halo
  • ghcr.io/halo-dev/halo

Halo 2有时候没有及时的更新Docker的latest标签镜像,因为Halo 2不兼容1.x版本,防止使用者误操作。推荐使用固定版本的标签,比如2.17或者2.17.0。

在系统任意位置创建一个文件夹

此文档以 ~/halo为例,后续操作中,Halo 产生的所有数据都会保存在这个目录。

mkdir ~/halo && cd ~/halo

创建docker-compose.yaml

halo 2默认使用H2数据库,这个主要用于方便开发测试,不推荐在生产使用。因为操作不当可能导致数据文件损坏。如果因为某些原因(如内存不足以运行独立数据库)必须要使用,建议按时备份数据。

docker-compose.yaml文件路径一般放在下面这个路径。

~/halo/docker-compose.yaml

下面给出几种docker-compose.yaml实例的配置。

  1. 创建 Halo + PostgreSQL 的实例:
    这里的PostgreSQL使用默认端口5432,如果需要改端口,要显性标注出来。
version: "3"

services:
  halo:
    image: registry.fit2cloud.com/halo/halo:2.17
    restart: on-failure:3
    depends_on:
      halodb:
        condition: service_healthy
    networks:
      halo_network:
    volumes:
      - ./halo2:/root/.halo2
    ports:
      - "8090:8090"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8090/actuator/health/readiness"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s          
    command:
      - --spring.r2dbc.url=r2dbc:pool:postgresql://halodb/halo
      - --spring.r2dbc.username=halo
      # PostgreSQL 的密码,请保证与下方 POSTGRES_PASSWORD 的变量值一致。
      - --spring.r2dbc.password=openpostgresql
      - --spring.sql.init.platform=postgresql
      # 外部访问地址,请根据实际需要修改
      - --halo.external-url=http://localhost:8090/
  halodb:
    image: postgres:15.4
    restart: on-failure:3
    networks:
      halo_network:
    volumes:
      - ./db:/var/lib/postgresql/data
    healthcheck:
      test: [ "CMD", "pg_isready" ]
      interval: 10s
      timeout: 5s
      retries: 5
    environment:
      - POSTGRES_PASSWORD=openpostgresql
      - POSTGRES_USER=halo
      - POSTGRES_DB=halo
      - PGUSER=halo

networks:
  halo_network:
  1. 创建 Halo + MySQL 的实例:
    这里的PostgreSQL使用默认端口3306,如果需要改端口,要显性标注出来。
version: "3"

services:
  halo:
    image: registry.fit2cloud.com/halo/halo:2.17
    restart: on-failure:3
    depends_on:
      halodb:
        condition: service_healthy
    networks:
      halo_network:
    volumes:
      - ./halo2:/root/.halo2
    ports:
      - "8090:8090"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8090/actuator/health/readiness"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s
    command:
      - --spring.r2dbc.url=r2dbc:pool:mysql://halodb:3306/halo
      - --spring.r2dbc.username=root
      # MySQL 的密码,请保证与下方 MYSQL_ROOT_PASSWORD 的变量值一致。
      - --spring.r2dbc.password=o#DwN&JSa56
      - --spring.sql.init.platform=mysql
      # 外部访问地址,请根据实际需要修改
      - --halo.external-url=http://localhost:8090/

  halodb:
    image: mysql:8.1.0
    restart: on-failure:3
    networks:
      halo_network:
    command: 
      - --default-authentication-plugin=caching_sha2_password
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_general_ci
      - --explicit_defaults_for_timestamp=true
    volumes:
      - ./mysql:/var/lib/mysql
      - ./mysqlBackup:/data/mysqlBackup
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
      interval: 3s
      retries: 5
      start_period: 30s
    environment:
      # 请修改此密码,并对应修改上方 Halo 服务的 SPRING_R2DBC_PASSWORD 变量值
      - MYSQL_ROOT_PASSWORD=o#DwN&JSa56
      - MYSQL_DATABASE=halo

networks:
  halo_network:
  1. 使用默认的 H2 数据库
version: "3"

services:
  halo:
    image: registry.fit2cloud.com/halo/halo:2.17
    restart: on-failure:3
    volumes:
      - ./halo2:/root/.halo2
    ports:
      - "8090:8090"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8090/actuator/health/readiness"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s          
    command:
      # 外部访问地址,请根据实际需要修改
      - --halo.external-url=http://localhost:8090/
  1. 仅创建 Halo 实例(使用已有外部数据库,MySQL 为例)
version: "3"

services:
  halo:
    image: registry.fit2cloud.com/halo/halo:2.17
    restart: on-failure:3
    network_mode: "host"
    volumes:
      - ./halo2:/root/.halo2
    command:
      # 修改为自己已有的 MySQL 配置
      - --spring.r2dbc.url=r2dbc:pool:mysql://localhost:3306/halo
      - --spring.r2dbc.username=root
      - --spring.r2dbc.password=
      - --spring.sql.init.platform=mysql
      # 外部访问地址,请根据实际需要修改
      - --halo.external-url=http://localhost:8090/
      # 端口号 默认8090
      - --server.port=8090

参数配置

参数名 描述
spring.r2dbc.url 数据库连接地址,详细可查阅下方的 数据库配置
spring.r2dbc.username 数据库用户名
spring.r2dbc.password 数据库密码
spring.sql.init.platform 数据库平台名称,支持 postgresql、mysql、h2
halo.external-url 外部访问链接,如果需要在公网访问,需要配置为实际访问地址

数据库配置

链接方式 链接地址格式 spring.sql.init.platform
PostgreSQL r2dbc:pool:postgresql://{HOST}:{PORT}/{DATABASE} postgresql
MySQL r2dbc:pool:mysql://{HOST}:{PORT}/{DATABASE} mysql
MariaDB r2dbc:pool:mariadb://{HOST}:{PORT}/{DATABASE} mariadb
H2 Database r2dbc:h2:file:///${halo.work-dir}/db/halo-next?MODE=MySQL&DB_CLOSE_ON_EXIT=FALSE h2

启动 Halo 服务

启动命令

docker-compose up -d

实时查看日志命令

docker-compose logs -f

配置反向代理以及域名解析

这里以Nginx为例子,halo2还支持Caddy 2、Traefik等。

  • 通过nginx.conf文配置
upstream halo {
  server 127.0.0.1:8090;
}
server {
  listen 80;
  listen [::]:80;
  server_name www.yourdomain.com;
  client_max_body_size 1024m;
  location / {
    proxy_pass http://halo;
    proxy_set_header HOST $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}
  • 通过Nginx Proxy Manager配置

参考
使用Nginx Proxy Manager配置Halo的反向代理和申请 SSL 证书

Halo初始化页面。

用浏览器访问 /console 即可进入 Halo 管理页面,首次启动会进入初始化页面。

更新新版本的halo

从 Halo 2.8 开始,Halo 内置了备份和恢复的功能,可以在 Console 中一键备份和恢复完整的数据。

  1. 备份
    在 Console 中,点击左侧菜单的 备份,进入备份页面。点击右上角的 创建备份 按钮,即可创建一个新的备份请求,需要注意的是,创建备份请求并不会立即开始备份,而是会在后台异步执行,因此需要等待一段时间才能看到备份的结果。
  2. 更新Halo服务
    修改 docker-compose.yaml 中配置的镜像版本。
services:
  halo:
    image: registry.fit2cloud.com/halo/halo:2.17
docker-compose up -d
  1. 恢复备份
    • 在 Console 中,点击左侧菜单的 备份,进入备份页面,然后点击 恢复 选项卡即可进入恢复界面,阅读完注意事项之后点击 开始恢复 按钮即可显示备份文件上传界面。
    • 选择备份文件后,点击 上传 按钮即可开始上传备份文件,上传完成后会自动开始恢复。
    • 恢复完成,会提示重启 Halo,点击 确定 按钮即可重启 Halo。
    • 最后,建议去服务器检查 Halo 的运行状态,如果没有设置自动重启,需要手动重启。

原文链接:
如何在Linux云服务器上通过Docker Compose部署安装Halo,搭建个人博客网站?

撇清一层歧义:标题中的阿里不是指阿里巴巴集团,喜马拉雅也不是指那个做音频频道的公司,文中所及内容以及我本人都与他们没有任何关联。
依照地理正式名称:阿里指的是西藏西部阿里地区,喜马拉雅指的是青藏高原地球最高山脉。

从前我在博客园不叫这个名字,今天很多自己的早期文章我都屏蔽了,这些系列作为回忆形式,再回博客园来发布,这里没那么多事。我的编程经历都是不正规的游击战,不具备参考价值,正式职业是喜马拉雅自然风光摄影师,作品发表于国家地理、星球研究所、Getty Image,编程是众多兴趣爱好之一,编程风格和技术选型随心所欲,属邪门歪道,与正规院校编程专业价值观违背,请勿仿效。

这个系列分为三篇,在第三篇连载完成时,会同时发布一个最新的基于AOT的项目。

  • 一,从井到Sharp
  • 二,安纳普尔那的雨季
  • 三,喜马拉雅山中的AOT

一,从井到Sharp

1990年, 小县城邮局门口书摊, 这一期的《少年科学》说了一个故事:70年代的美国车库里诞生了很多公司, 那是计算机崛起的年代, 在硅谷经常有一些技术青年聚会,有搞无线电的,有搞民间军事技术的,有自制计算机的, 通常都是一个做技术的和一个搞投机倒把的组合,创造了后来的新时代,其中, 一对叫比尔和艾伦的组合, 还有一对两人都叫史蒂夫的组合。这一期杂志说了比尔和艾伦一边写basic,一边创建了新时代的公司,于是我越看越觉得有趣。 (另外那两位史蒂夫的故事,十年后才在另一本书中得知更为精彩)

1994年,小霸王学习机, 附带有一本非常有价值的说明书,很厚的,说的是basic语言, 但是当时没有能力看懂,大家都说这是“电脑”的语言, 当时不叫计算机, 而是叫电脑、 脑! 我打 print  "helow" 的时候,它给了我期望的回答, 然后我想当然的打了一个 print  "what's your name?" ,它却没有给我期望的回答。那么这样一来,好像这个电脑也就是个大型计算器,比尔就是个卖计算器的,只不过把事情搞得稍微复杂一点,看起来更高级一些,就来钱了。
(basic的打印命令是不是叫print,现在也想不起了。但是这个 what's your name, 直到16年后才由 siri 给出了回答)

1995年,《电子游戏软件》做了三期连载,叫《世嘉五代与超级任天堂的对比报告》,有一个作者投稿反常识的说到世嘉五代要优于超级任天堂(第一期他赢了,第二期一位辩方把两者提升到中立,第三期另一个辩者终于把他披得体无完肤),三篇文章两个作者在技术方面都相当专业,说世嘉五代是用C编写游戏, 超级任天堂用汇编语言编写游戏,于是重点就是:我知道了专业的编程语言有C和汇编两个东西。 好像大型计算器的说法也不对,还是有点东西在里面的。

1996年,我在桂林新华书店买到一本书,叫C语言程序设计,16块钱,那时候一碗粉5角钱, 可以吃32顿。 终于可以见识一下什么叫C语言, 原来有常量、变量、指针、指针的指针 这么几种概念,语法也和basic不一样, 苦于当时电脑比毒品还贵,没有条件上机实践,后面的结构、循环 等,难以理解和体会,没关系,脑子里面有了点概念指针可以用少的东西操作多的东西,有很多一般人看不懂的符号,和电影里看到的那种有点像,这叫高级货。

陆陆续续的,在1997-2000年间,各地都开办了电脑游戏室,那时候不叫网吧,没有网, 就叫电脑游戏室,玩的是局域网红色警报联机,我经常逃学,并且教唆同学和我一起过去玩,一去就是整个宿舍,通宵呆在里面,最后和老板混得有多熟呢, 他后来送了我一辆山地自行车,那辆车值700块钱。同学们都打游戏的时候,有时候我玩的是系统,把windows95的控制面板里面所有的功能全部一一打开来研究都是些什么东西,因为当时我有一本很厚的书叫《windows95操作系统》,那是1996年我写信委托远方的表哥给我买的,每天我就拿这本书去电脑室里面一一对照着操作,有时候也打开系统内置的basic来玩一下,当然也有很多看不懂的东西,蓝屏更是家常便饭。 最后系统玩得有多熟呢,那时候学校里也有电脑课,年少苍狂,我号称去上课纯粹就是给电脑老师面子了。
(后来才知是谁给谁留了面子,在老师讲解多个进制之间怎么转换的算法时,这是扫地僧与游坦之的差距)

2001年9月, 在南宁新华书城看到一本书,叫 《C井程序设计语言》, 这个字读井,苍井的井,好奇这是什么东西,好歹我之前也听说过c语言家族的各种变体,翻开一看,似懂非懂的东西, 说指针不安全,什么乱七八糟的, 鉴于从没听说过这东西, 敢一下时髦,咬咬牙买回去,22块钱, 食堂一碗粉8角钱,也接近28顿了。

后来大学几年,我大概了解到这个c井是运行在一种叫.net framework的插件上的, 开发工具在五张一套的光盘里,但是这几年下来我一直没法运行 hellow world.  当年的basic好歹还会原样返回"what's your name.", 好几次尝试,我一度怀疑自己是不是买的光盘有问题, 一套光盘25块钱,陆陆续续买了好几套都尝试不成功。 这什么破东西,靠插件来运行,一看就不是什么正规货,最简单的hellow world都运行不起来,你们另外那几张光盘的什么 j井、Vb井、C++井什么的也全都不是什么上得了台面的货色。 骗我起码花了200块,我一个月的伙食费就400块。 书里面还说了不装那些光盘也可以, 命令行里面敲打 csc test.cs 也可以运行, 可是为什么我的电脑总是提示说没有找到csc命令? 看来这也是个假货。这本《C井程序设计语言》就一直放在旁边,时不时不甘心了拿出来研究一下,但是没有一次是成功的。

平时我每月都会省钱少抽几支烟,买《电脑自做DIY》来看,这本书的硬件知识非常专业,看多了,也就成就了以下:

我和那个卖光盘的混得比较熟,经常带同学去照顾她生意, 那么现在我自己的事情找她,说有一套visual studio .net 光盘有问题,换了好几个光驱都读不出来,你给我换一套3d mark 02。 她肯定要给我换,我是她的财路,不敢得罪我(这种事我也就只做过这么一次)。 当时我们系有几个装机佬勾结商家,骗同学去买奔腾4 1.6 + 微星技嘉845pe主板 + ddr256 + mx440 + 优派或三星液晶显示屏(显示器3500-4000块左右,商家回扣可以拿100块), 同样的价钱我自己装的电脑是 奔腾4 1.6 + 华硕850主板+ Rambus 256 + ATI 8500 + 索尼特丽珑显示器, 我拿回这套 3d mark 02, 全流程满速运行, 完整的 directX 8.1 支持,有一片海洋的画面和另外两个阳光穿透的画面,那是三个像素渲染项目,必须dx8.1硬件支持才能看见,电脑城的装机店老板员工都围过来,说从没见过,12000分, 你们 6000分连流程都跑不完,脸都不知往哪放,我主机甩你们500条街、我显示器甩你们1000条街(那时候的液晶显示器确实是要被甩的),瞬间我在大一就成了全年级最专业的硬件专家,每天跑测试、刷显卡、做3D渲染、什么装机、买杀毒软件、系统重装、全都来找我、我不拿商家回扣,只带同学装最高性能的主机,一传十 十传百,我装的电脑跑分是最高的, photoshop是最流畅的,我放DVD的时候那个画面美啊,放的都是 珍珠港、卧虎藏龙、角斗士、钢琴师、兄弟连、拯救大兵瑞恩 …… 没有金像奖不要进我的光驱,加上特丽珑显示管的那个画面,每次都必须有一群人来我旁边围着看,反观你们的液晶显示器那个一片惨白死黑。 帮同学装机的好处是什么呢,有人请饭吃,有饮料喝,平时同学见面各种学业问题也好说话。 两年后, ATI 8500显卡可以刷成 ATI Fire GL,Maya 可以100万多边形实时贴图渲染,这是又一轮高潮。  这么好的生活, 编什么程呢, 什么 hellow world 和我一点关系没有。这种体会,不知道你懂不懂。

但是不编程不行,事情是这样的:

我们是艺术院校,当时除了做设计作业、画油画、学Maya 3D渲染、吃酸菜鱼、还有另一个心中的秤砣没落地,我接任了某个动漫专业网站的站长, 我在尚未接任前两年就一直和各位前辈以及同僚吹风说,我们需要建立自主可控的自动化会员注册,虽然我们目前依托在动网论坛程序上,但那不是自己可控的技术,实际上我们自身的内容系统全是依靠html+ftp来更新,这样内容一旦增多,可能维护不过来的。  这个心病一直每隔几天就发作一次, 但是不知道怎么办, 没人会, 数据库我们买不起,而且就算是买了也不知道怎么弄, 后来我就一次次跑书店技术专栏去看, 就是瞎看, 没有头绪的瞎看, 至少要建立起一个网站程序是怎么运行的这样的概念, 皇天不负苦心人, 我看到了一本 dreamweaver 程序设计,里面有一个简单的拖来就能操作access数据库,可以保存一些东西的例子,还有一个叫数据集的东西, 能读取数据库中的表格, 而我们的网站也是动网论坛5.0+access, 空间肯定是支持的,然后我就在空间上运行起这个小例子,果然成功了,这个东西叫asp程序,好的,我们的会员注册系统可以实现了,以后我们就以asp为基础做大。

那么接下来,动网论坛的一些置顶、精品帖子、现在要怎么同步到我们网站首页,怎么打开数据库看一下里面是什么东西, 这点常识我还是有的, office 2000 就可以打开了,但是看不懂,看起来就像是表格一样的, 它和SQL又是什么关系。 光盘老板娘的生意又来了,三张一套的SQL2000,15块钱,我一周的烟钱。 SQL这个名字那么高大,还是大写的,有点腿发软, 电脑报上面报价80000块钱的东西,我这15块有没有白花。
(事后结论是白花了,但这是学费)

现在的问题有三个: asp程序、动网论坛同步内容、 数据库操作, 这三个问题解决的话, 困扰我们网站的事情就能解决了, 但是目前对于三个问题的认知都是0,只会在dreamweaver里面拖一些小工具。

幸运的是,我们论坛有一位已经在外做编程工作的会员,我时不时向他请教一些关于asp的问题, 一开始,牛头不对马嘴,我连提问都不知道怎么提, 用白话来描述问题, 而他的专业术语回答我是根本就看不懂。 什么叫数据库服务器, 我们的动网论坛算不算数据库服务器, 什么叫数据集、怎么连接Access,还有一个叫OBBC的是什么东西, windows2000控制面板里面有一个叫数据源的东西,里面看起来像是各种各样的数据库,有哪些是我们的空间当前可以使用的。

(在那个年代,我们这些小网站是运行在“空间”上的,支持力度非常有限,是商家从IIS中隔离出来的一片进程和指定容量的一片磁盘空间,并且当时因为一传十十传百的说IIS某些功能不安全,于是商家们就把IIS能关的功能都关得快干净了, 那是一种连虚拟机都不如的东西,内存256M算是海量的2002年,今天的虚拟机放到当时是不可想象的高价物品,对于我们这些学生来说,可以理解为无)

当时随处可以下载到的一份SQL语言入门chm文件,看了一个星期,原来也并不是电影里面那种看不懂的符号,select * from table where id = 10  可以解决很多困扰眼前的问题了。再加上好几个月过去了,SQL入门chm配合他给我徒手写的一份OLEDB 操作Access的就200多字的简单asp程序,我也把每一行代码表示什么意思都研究透了,原来是这样,我终于可以摆脱 dreamweaver的控制了。(他是至今唯一教过我编程的人,那份200字左右的asp代码,也就是我唯一的敲门砖)

我开始了记事本写asp的时代,那时候,每天写很多遍,当时已经熟练到可以徒手写完 conn.open(......."那个什么=4.0的那一长串"......) OLEDB数据库连接字符串,网络也渐渐变得普及了,我一上网就翻看各种asp详细知识,那个叫数据集的东西,正式名称是recordset,  但是有时候仅仅通过conn本身也能实现简单的查询。

各行各业都有贼船,一旦上去,就下不来了,编程也是这样,正是因为ASP越看越深,出了一个插曲:有一个人在某个技术社区说到:asp 也是可以使用 class 的,但是这是一种“假”的class。(今天已经想不起怎么写了,有一点印象是类似 IF -- END IF 这样的包含语法)那么什么才是真的class? 以及什么是class?  贼船就是从这时候开始远航了。

这时候我才逐渐加深了认知: asp 不是语言名称,它是一个iis的功能平台名称,我们正在编写的语言, 准确名字叫 vbscript,  相应的,还可以用 jscript、perl 来编写asp。 如果使用jscript,这个语法很像我多年前仅在书上见过的高级货:C.   这种风格看起来更有前途,而且它可以前后端共用一套语言,岂不美哉,于是我尝试把编写过的程序全部用 jscript 重写,顺便能够学习到第二门语言,说干就干。(“前后端共用”是今天打这篇文章的时候才会用这个词汇,当时哪有什么前端后端的认知)

时间来到2003年,jscript重写变得更加高效,可以用更少的代码做更精简的文件模块, 我在各处都晃眼看到asp无论是vbscript还是jscript都不支持完整的面向对象, 那么,什么是面向对象? 这是一种听起来既时髦,又可以显得很有学问的感觉。有一种感觉是暂时先把事情停下来,以现在打下的基础,重新认知一下编程的世界,以及我们的程序需要怎么样的语言来做重写, 走过的vbscript 弯路不能再重蹈。 (今天我不认为那是弯路,asp-vbscript是一个很好的小型平台解决方案,至今都是)

英特尔发布了超线程cpu,一个cpu可以变成两个cpu, 电脑广场都做大型露天活动来宣传, 但我认为Athlon 64 更优秀, amd同样有超线程平台, 但更重要的是 amd有64 位cpu,64位是未来,也是时代正在变革。 于是我把更多的精力花在新同学装机要他们首选 Athlon 64 ,操作系统也换做 windows xp 64位版本, 我说话还是有用的, 3d mark 2002 的分数打下的江山, 再加上他们看到我现在天天刷显卡, 比起那些骗同学回扣的装机佬,这是膜拜级的碾压, 请不请我吃饭无所谓了, 我只要你们拿64位cpu + 64位系统让我也时不时在你们的机器体验一下各种数据的快慢。 (当时英特尔虽有超线程,但仍属32位cpu,他们的64位重心在安腾和至强, ddr平台能够发挥奔腾4全带宽的865和875芯片组尚未面世, 残废的845系列我是坚决不会推荐的, 至于我正在用的850芯片组,rambus内存虽然发挥了p4全带宽,但是32位的时代已经是落日余晖了)(那时候我装机装到可以背诵下 j2mv9-jyyq6-jm44k-qmyth-8rb2w,今天有谁能一眼看出这是什么东西的序列号吗)

体验时代变迁的同时,我暂停了jscript重写网站,先花两个月再认识一下编程语言的过去和未来,跑书店,看网站,两个月过去,虽然很多年前接触过C的概念,但是一直没接触过C++, 只知道老板娘那里有一种visual c++ 6.0 的光盘很好卖,但是窗口里拖动控件做桌面程序那是与我们的网站需求天各一方。 C井的hellow world 始终无法运行,我手头还有另一本书,叫《Java 程序设计入门》,面向对象三个语言: C++  JAVA  C#,我的面向对象,是从 JAVA 这一脉开始走开的。

不管面向谁家的对象,java有一点它能吸引我去看的,是一次编写,随时随地运行,它是真正跨平台(至少官方文字是这么宣称的),虽然我运行不起c井,但是我多多少少也从各种渠道了解到.net是假的跨平台(那时候尚未有Mono,那是OS/2、Solaris、MAC OS 7,Windows 2000 的时代,java 把它们全实现了,而微软所谓跨平台的 windows ce,windows iot, .net micro framework、电视机顶盒,人尽皆知这些都是什么货色,没有意义),但是我有一种感觉:1999年,摩托罗拉L2000www手机上市,它内置一个wap浏览器,并且内置调制解调器,拿着手机就能访问网站,当时各大杂志鼓吹:我们即将进入仅靠手机就能移动办公的时代。 如今4年过去了,移动办公的概念是一根毛都看不见。 那么这个叫java的东西今天说一次编写到处运行,是不是四年后同样也一根毛都看不见?先初步学一下再说,当初研究dreamweaver的时候,里面有一个叫jsp的东西,好像就是和这个java相关的,能不能用java这样的“正规语言”让我们的网站也运行在既正规又高级的平台上。 以及它看起来和asp的jscript,浏览器小动画里面的小脚本javascript,似乎是有关联的。

又是几个月过去, 高级货的class写法果然和vbscript的class不一样,通过构建器可以复制很多独立的副本,通过继承可以精简很多重复的代码,这就叫面向对象?那么我用 prototype.XXX = YYY 也可以实现继承, new function 也可以实现独立副本, 这不就是苹果换个名字叫蛇果,葡萄换个名字叫提子可以多卖一些钱吗。直到有一天我在新华书店里面遇到个人,他看到我站在java相关书籍前面,就问我有没有见过BEA什么组建相关的书籍, 我说我刚入门, 还不知道这些是什么东西, 他的解释我也听不懂, 在于他很好说话, 我也就向他问起了面向对象的疑惑, 而他的耐心解答我还是听得懂的: 你还没学习多态。  而在你完成了封装、继承、多态后, 可以更上一层楼进入泛型的领域,并且他从头到尾和我流畅解释了一遍面向对象三要素, 那是个神人,有资格去寻找BEA什么我听不懂的东西, 感谢他的耐心指教,我回去恶补java知识,把那些很难看的章节硬是灌输下去,三要素学全了,目前能够直接发挥用途的就是继承,要不要先用jsp来重写一遍试试? 这时候我也不敢那么快就下结论,或者再考虑几天再说。
(多态的使用需要配合接口继承,在大型项目上做功能隔离,实现某些功能的热切换,这样才能体会到意义所在,要不然只会书中例子里的 object a = 1, object b = "abc", 这是自欺欺人,当时那个小网站,不具备大型结构来使用多态,也就体会不到)

当时和我一起同租一栋房的邻居养了一个小狗,那小狗经常跑到我屋子里面来玩,于是我们人之间的话也多了起来,他也是个做编程工作的,我说我也在学习编程,他问我用什么语言,我说用asp, 他说哦,那是vb小脚本,我好奇问到,你呢?  他说我们公司最近用.Net,已经做了.Net开发一年了,平时用C#开发。我又和他求证了一遍: 你刚才说C什么? "C-Sharp"。 原来那个字不念井。

无论是念井还是念Sharp,重要的是我好几套光盘白买的事情,世面上有一套新的 visual studio 2002,  我咬牙再买一套回来,hellow world 总应该可以运行了吧。

当我把 visual studio 2002 开发环境装完,照着书里面的例子重新敲一遍 hellow world. 果然不出预料的的依旧报错没法运行, 比尔盖茨你这个骗子骗我买了第五套光盘,难怪美国国会要拆分你们公司,难怪你当ceo要下台,你这个四眼仔印堂发黑看你那张脸就不是个什么好东西,你什么新官职首席技术官, 你家的烂东西狗屎不如,难怪 sun 公司的 java 把你打得不分东南西北 ....... 省略一百骂字
(见识有限,骂人都不知道应该骂谁,如果骂得专业一点的话应该骂安德斯.海斯尔伯格)

当时已经有了QQ群,我也加入了几个群,上去照样一通骂, 有个通情达理的人让我把代码发出来看一下,一发过去,晴天霹雳: 你那是最早的2000年初测试版代码,现在你应该把Microsoft 换成 System 就可以了。 这个五雷轰顶的消息促使我不睡觉也要重试一遍 hellow world.  果然,Microsoft 换成 System 就成功运行 hellow world 了。(
微软的这个把顶级空间改名的习惯在2013年k演变到vNext的过程中同样再次发生

之前骂错人了,我本来想骂的是保尔盖茨,不小心说成了比尔而已,人家比尔盖茨本来就是个有为青年,我怎么可能会骂他呢,金丝眼镜显得文质彬彬又有学问,年纪轻轻一表人才,16岁就会编程开公司,人家跻身全球几位,被国会拆分也是国会看得起它,以后一定前途无量。

这本《C井程序设计语言》,2001年初出版的,作者写书的时候是2000年。从那天开始它成了我擦桌子的原料, 吃一顿饭就撕一页下来擦,有时候小狗过来撒尿,就多撕几页下来擦。印有作者名字的那一页,我贴在门背上,用来当飞镖把子。

拿出那些 visual studio .net 、 2002、 2003,细看,发现那几套25块钱买来的光盘用料特别好,背面文字印制得相当精美,这字体一看就是出自大师手笔,拿在手上沉甸甸的肯定是正规货,光面的反光比太阳还亮,晚上可以当夜明珠。

待续第二篇


最后附注至博客园

探索Amazon S3:存储解决方案的基石

本文为上一篇minio使用的衍生版

相关链接:1.
https://www.cnblogs.com/ComfortableM/p/18286363

​ 2.
https://blog.csdn.net/zizai_a/article/details/140796186?spm=1001.2014.3001.5501

引言

云存储已经成为现代信息技术不可或缺的一部分,它为企业和个人提供了诸多便利。以下是几个关键点,说明云存储为何如此重要:

1. 数据安全与备份

  • 数据加密
    :云存储服务商通常提供高级加密技术,确保数据在传输过程中和存储时的安全。
  • 备份与恢复
    :云存储能够自动备份数据,并且在发生灾难性事件时,可以迅速恢复数据,保证业务连续性不受影响。

2. 成本效益

  • 按需付费
    :用户可以根据实际使用的存储空间支付费用,避免了传统存储方式中预购大量存储空间的成本浪费。
  • 运维成本降低
    :云存储减少了企业在硬件采购、维护和升级方面的开销,同时也降低了电力和冷却成本。

3. 灵活性与可扩展性

  • 无限扩展
    :随着数据量的增长,云存储可以轻松地扩展存储容量,无需用户手动增加硬件资源。
  • 多租户模型
    :用户可以轻松地管理不同的项目或部门的数据隔离,而不会受到物理限制的影响。

4. 访问与协作

  • 远程访问
    :无论用户身处何处,只要有互联网连接,就可以访问存储在云端的数据。
  • 文件共享
    :通过简单的链接分享机制,团队成员之间可以轻松共享文件,促进协作。

5. 灾难恢复

  • 多地域复制
    :云存储服务通常提供数据的多地域复制功能,确保即使某个数据中心发生故障,数据依然可用。
  • 快速恢复
    :当遇到意外情况时,云存储可以迅速恢复数据,减少数据丢失的风险。

6. 技术创新与支持

  • 最新技术
    :云存储服务商会持续更新技术栈,确保用户能够获得最新的存储技术和安全措施。
  • 技术支持
    :专业的技术支持团队可以及时响应用户的疑问和技术难题。

7. 法规遵从

  • 合规性
    :许多云存储服务提供商遵循严格的法规标准,确保数据存储符合地区法律法规要求。

8. 对智慧城市的贡献

  • 城市管理
    :云存储对于收集、处理和分析智慧城市中产生的大量数据至关重要,有助于提高城市管理效率和服务质量。

综上所述,云存储不仅改变了企业和个人管理数据的方式,还推动了整个社会向着更加高效、可持续的方向发展。随着技术的进步,我们可以期待云存储在未来扮演更加重要的角色。

Amazon Simple Storage Service (S3) 是 Amazon Web Services (AWS) 中最成熟且广泛使用的对象存储服务之一。自从2006年推出以来,S3 已经成为云计算领域的一个标志性产品,它不仅为AWS奠定了基础,而且在全球范围内成为了云存储的标准之一。

关键点:

  • 成熟度与可靠性
    :经过多年的运营,S3 已经证明了其极高的可靠性和稳定性。它能够处理大量的数据存储需求,并保持高可用性和持久性,平均故障时间(MTTF)达到了数百年。
  • 广泛的采用率
    :S3 被成千上万的企业所使用,包括初创公司、大型企业和政府机构,这些组织依赖于 S3 来存储各种类型的数据,从小文件到PB级别的数据集。
  • 丰富的功能集
    :S3 提供了一系列强大的功能,如版本控制、生命周期管理、数据加密、访问控制等,这些功能使得 S3 成为了一个灵活且全面的存储解决方案。
  • 集成与兼容性
    :S3 与其他 AWS 服务紧密集成,比如 Amazon Elastic Compute Cloud (EC2)、Amazon Redshift 和 AWS Lambda,使得用户可以在 AWS 生态系统内部构建复杂的应用程序和服务。此外,S3 还支持多种第三方工具和服务,使其成为数据处理管道的核心组成部分。
  • 成本效益
    :S3 提供了多种存储类别的选择,允许用户根据数据访问频率和存储需求来优化成本。例如,Standard 类别适用于频繁访问的数据,而 Infrequent Access (IA) 或 Glacier 类别则适用于长期存档数据。
  • 技术创新
    :S3 不断推出新的特性和改进,以满足不断变化的市场需求。例如,Intelligent-Tiering 存储类别能够自动将数据移动到最合适的存储层,从而帮助用户节省成本。
  • 行业认可
    :S3 获得了多个行业奖项和认证,这反映了它在云存储领域的领导地位。

Amazon S3 作为 AWS 的旗舰级服务之一,在过去十几年里已经成为了全球云存储领域的标杆。无论是从成熟度、可靠性还是功能丰富性来看,S3 都是企业和开发者信赖的选择。随着 AWS 继续在其基础上进行创新和发展,我们有理由相信 S3 将继续引领云存储的发展趋势。

Amazon S3简介

Amazon Simple Storage Service (S3) 是亚马逊 Web Services (AWS) 提供的一种对象存储服务。自2006年推出以来,S3 已经成为了云存储领域的一个标志性产品,它为开发者和企业提供了一种简单、高效的方式来存储和检索任意数量的数据。

核心特点:

  • 高可用性和持久性
    :S3 被设计为能够承受严重的系统故障,确保数据的高度持久性。其设计目标是每年的数据丢失率为0.000000000001(11个9),这意味着数据几乎不会丢失。S3 使用多副本冗余存储来保护数据免受组件故障的影响,并且数据被自动分布在多个设施中,以防止区域性的灾难影响数据的可用性。
  • 无限的可扩展性
    :S3 的架构允许无缝扩展,无需预先规划存储容量。企业可以根据需要存储从GB到EB级别的数据,而无需担心存储限制。这种自动扩展能力意味着企业不必担心在数据量快速增长时需要手动调整基础设施。
  • 成本效益
    :S3 提供了多种存储类别,使企业可以根据数据的访问频率选择最适合的选项,从而优化成本。例如,S3 Standard 适合频繁访问的数据,而 S3 Standard-Infrequent Access (S3 Standard-IA) 和 S3 One Zone-Infrequent Access (S3 One Zone-IA) 适用于不经常访问的数据。S3 智能分层能够自动检测数据的访问模式,并将数据移动到最经济的存储层,以进一步降低成本。
  • 安全性和合规性
    :S3 支持多种安全功能,如服务器端加密、客户端加密、访问控制策略等,以保护数据免受未经授权的访问。它还支持多种合规标准,如 HIPAA、FedRAMP、PCI DSS 等,帮助企业遵守行业法规要求。
  • 易于管理和集成
    :S3 提供了一个直观的管理控制台,使用户能够轻松地上传、下载和管理数据。它还提供了丰富的 API 和 SDK,以便开发者可以轻松地将 S3 集成到他们的应用程序和服务中。
  • 数据生命周期管理
    :通过 S3 生命周期策略,企业可以自动将数据从一个存储类别迁移到另一个存储类别,或者在指定的时间后删除数据。这种自动化的过程有助于减少管理负担,并确保数据始终处于最经济的存储层。
  • 高性能和全球覆盖
    :S3 的全球边缘位置网络确保了低延迟的数据访问,无论用户位于世界哪个角落。这种分布式的架构有助于提高数据的可用性和响应速度。

使用场景:

  • 网站托管
    :S3 可以用来托管静态网站,包括HTML页面、CSS样式表、JavaScript脚本和图片。
  • 数据备份与恢复
    :企业可以使用 S3 作为数据备份和灾难恢复策略的一部分,确保数据的安全性和可恢复性。
  • 大数据处理
    :S3 可以作为大数据分析的数据湖,存储原始数据,供后续处理和分析使用。
  • 媒体存储与分发
    :S3 适用于存储和分发视频、音频和其他多媒体文件。
  • 应用程序数据存储
    :S3 可以存储应用程序生成的日志文件、缓存数据、数据库备份等。

快速使用

  • 注册账户

    https://signin.aws.amazon.com/signup?request_type=register

    S3具有很多免费使用的套餐,可以为开发前搭建一个小型测试环境。

    注册成功后登录进控制台,点击右上角用户有一个安全凭证:

    下滑到访问密钥这,创建一个访问密钥,这个是连接S3的凭证。也可以创建一个IAM账户,为其分配权限,用IAM账户登录到控制台来创建访问密钥,S3也建议这样做。

    接下来可以通过左上角服务找到S3自由发挥了。

Spring boot集成

添加依赖

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
        </dependency>

创建配置类

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

import java.net.URI;

@Configuration
public class AmazonS3Config {

    @Value("${aws.s3.accessKey}")
    private String accessKeyId;
    @Value("${aws.s3.secretKey}")
    private String secretKey;
    @Value("${aws.s3.endPoint}")
    private String endPoint;//接入点,未设置可以注释

    @Bean
    public S3Client s3Client() {
        AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKeyId, secretKey);

        return S3Client.builder()
                .credentialsProvider(StaticCredentialsProvider.create(credentials))
                .region(Region.US_EAST_1)//区域
                .endpointOverride(URI.create(endPoint))//接入点
                .serviceConfiguration(S3Configuration.builder()
                        .pathStyleAccessEnabled(true)
                        .chunkedEncodingEnabled(false)
                        .build())
                .build();
    }

    //S3Presigner是用来获取文件对象预签名url的
    @Bean
    public S3Presigner s3Presigner() {
        AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKeyId, secretKey);

        return S3Presigner.builder()
                .credentialsProvider(StaticCredentialsProvider.create(credentials))
                .region(Region.US_EAST_1)
                .endpointOverride(URI.create(endPoint))
                .build();
    }
}

基本操作介绍

创建桶
    public boolean ifExistsBucket(String bucketName) {
        // 尝试发送 HEAD 请求来检查存储桶是否存在
        try {
            HeadBucketResponse headBucketResponse = s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build());
        } catch (S3Exception e) {
            // 如果捕获到 S3 异常且状态码为 404,则说明存储桶不存在
            if (e.statusCode() == 404) {
                return false;
            } else {
                // 打印异常堆栈跟踪
                e.printStackTrace();
            }
        }
        // 如果没有抛出异常或状态码不是 404,则说明存储桶存在
        return true;
    }

    public boolean createBucket(String bucketName) throws RuntimeException {
        // 检查存储桶是否已经存在
        if (ifExistsBucket(bucketName)) {
            // 如果存储桶已存在,则抛出运行时异常
            throw new RuntimeException("桶已存在");
        }
        // 创建新的存储桶
        S3Response bucket = s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
        // 检查存储桶是否创建成功
        return ifExistsBucket(bucketName);
    }

    public boolean removeBucket(String bucketName) {
        // 如果存储桶不存在,则直接返回 true
        if (!ifExistsBucket(bucketName)) {
            return true;
        }
        // 删除存储桶
        s3Client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build());
        // 检查存储桶是否已被删除
        return !ifExistsBucket(bucketName);
    }
上传
    public boolean uploadObject(String bucketName, String targetObject, String sourcePath) {
        // 尝试上传本地文件到指定的存储桶和对象键
        try {
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(targetObject)
                    .build(), RequestBody.fromFile(new File(sourcePath)));
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上传成功则返回 true
        return true;
    }

    public boolean putObject(String bucketName, String object, InputStream inputStream, long size) {
        // 尝试从输入流上传数据到指定的存储桶和对象键
        try {
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build(), RequestBody.fromInputStream(inputStream, size));
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上传成功则返回 true
        return true;
    }

    public boolean putObject(String bucketName, String object, InputStream inputStream, long size, Map<String, String> tags) {
        // 尝试从输入流上传数据到指定的存储桶和对象键,并设置标签
        try {
            // 将标签映射转换为标签集合
            Collection<Tag> tagList = tags.entrySet().stream().map(entry ->
                    Tag.builder().key(entry.getKey()).value(entry.getValue()).build())
                    .collect(Collectors.toList());
            // 上传对象并设置标签
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .tagging(Tagging.builder()
                            .tagSet(tagList)
                            .build())
                    .build(), RequestBody.fromInputStream(inputStream, size));
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上传成功则返回 true
        return true;
    }
分片上传
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.core.sync.RequestBody;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class S3MultipartUploader {

    private static final S3Client s3Client = S3Client.create();

    /**
     * 开始一个新的分片上传会话。
     *
     * @param bucketName 存储桶名称
     * @param objectKey 对象键
     * @return 返回的 UploadId 用于后续操作
     */
    public static InitiateMultipartUploadResponse initiateMultipartUpload(String bucketName, String objectKey) {
        InitiateMultipartUploadRequest request = InitiateMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .build();
        return s3Client.initiateMultipartUpload(request);
    }

    /**
     * 上传一个分片。
     *
     * @param bucketName 存储桶名称
     * @param objectKey 对象键
     * @param uploadId 上传会话的 ID
     * @param partNumber 分片序号
     * @param file 文件
     * @param offset 文件偏移量
     * @param length 文件长度
     * @return 返回的 Part ETag 用于完成上传时验证
     */
    public static UploadPartResponse uploadPart(String bucketName, String objectKey, String uploadId,
                                                int partNumber, File file, long offset, long length) {
        try (FileInputStream fis = new FileInputStream(file)) {
            UploadPartRequest request = UploadPartRequest.builder()
                    .bucket(bucketName)
                    .key(objectKey)
                    .uploadId(uploadId)
                    .partNumber(partNumber)
                    .build();
            RequestBody requestBody = RequestBody.fromInputStream(fis, length, offset);
            return s3Client.uploadPart(request, requestBody);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 完成分片上传。
     *
     * @param bucketName 存储桶名称
     * @param objectKey 对象键
     * @param uploadId 上传会话的 ID
     * @param parts 已上传的分片列表
     */
    public static CompleteMultipartUploadResponse completeMultipartUpload(String bucketName, String objectKey,
                                                                          String uploadId, List<CompletedPart> parts) {
        CompleteMultipartUploadRequest request = CompleteMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .uploadId(uploadId)
                .multipartUpload(CompletedMultipartUpload.builder()
                        .parts(parts)
                        .build())
                .build();
        return s3Client.completeMultipartUpload(request);
    }

    /**
     * 取消分片上传会话。
     *
     * @param bucketName 存储桶名称
     * @param objectKey 对象键
     * @param uploadId 上传会话的 ID
     */
    public static void abortMultipartUpload(String bucketName, String objectKey, String uploadId) {
        AbortMultipartUploadRequest request = AbortMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .uploadId(uploadId)
                .build();
        s3Client.abortMultipartUpload(request);
    }

    public static void main(String[] args) {
        String bucketName = "your-bucket-name";
        String objectKey = "path/to/your-object";
        File fileToUpload = new File("path/to/your/local/file");

        // 开始分片上传会话
        InitiateMultipartUploadResponse initResponse = initiateMultipartUpload(bucketName, objectKey);
        String uploadId = initResponse.uploadId();

        // 计算文件大小和分片数量
        long fileSize = fileToUpload.length();
        int partSize = 5 * 1024 * 1024; // 假设每个分片大小为 5MB
        int numberOfParts = (int) Math.ceil((double) fileSize / partSize);

        // 上传每个分片
        List<CompletedPart> completedParts = new ArrayList<>();
        try (FileInputStream fis = new FileInputStream(fileToUpload)) {
            for (int i = 1; i <= numberOfParts; i++) {
                long startOffset = (i - 1) * partSize;
                long currentPartSize = Math.min(partSize, fileSize - startOffset);

                // 上传分片
                UploadPartResponse partResponse = uploadPart(bucketName, objectKey, uploadId, i, fileToUpload, startOffset, currentPartSize);
                if (partResponse != null) {
                    // 保存已完成分片的信息
                    CompletedPart completedPart = CompletedPart.builder()
                            .partNumber(i)
                            .eTag(partResponse.eTag())
                            .build();
                    completedParts.add(completedPart);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 完成分片上传
        CompleteMultipartUploadResponse completeResponse = completeMultipartUpload(bucketName, objectKey, uploadId, completedParts);
        if (completeResponse != null) {
            System.out.println("分片上传成功!");
        } else {
            System.out.println("分片上传失败。");
        }
    }
}
下载
    public boolean downObject(String bucketName, String objectName, String targetPath) {
        // 尝试下载对象到指定路径
        try {
            s3Client.getObject(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(objectName)
                    .build(), new File(targetPath).toPath());
        } catch (Exception e) {
            // 如果出现异常则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果下载成功则返回 true
        return true;
    }

    public InputStream getObject(String bucketName, String object) {
        InputStream objectStream = null;
        // 尝试获取指定存储桶和对象键的输入流
        try {
            objectStream = s3Client.getObject(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 null
            e.printStackTrace();
            return null;
        }
        // 返回对象的输入流
        return objectStream;
    }

    public ResponseBytes<GetObjectResponse> getObjectAsBytes(String bucketName, String object) {
        ResponseBytes<GetObjectResponse> objectAsBytes = null;
        // 尝试获取指定存储桶和对象键的内容作为字节
        try {
            objectAsBytes = s3Client.getObjectAsBytes(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 null
            e.printStackTrace();
            return null;
        }
        // 返回对象的内容作为字节
        return objectAsBytes;
    }
获取对象预签名url
    public String presignedURLofObject(String bucketName, String object, int expire) {
        URL url = null;
        // 尝试生成预签名 URL
        try {
            url = s3Presigner.presignGetObject(GetObjectPresignRequest.builder()
                    .signatureDuration(Duration.ofMinutes(expire))
                    .getObjectRequest(GetObjectRequest.builder()
                            .bucket(bucketName)
                            .key(object)
                            .build())
                    .build()).url();
        } catch (Exception e) {
            // 如果出现异常则打印异常堆栈跟踪并返回 null
            e.printStackTrace();
            return null;
        } finally {
            // 关闭预签名客户端
            s3Presigner.close();
        }
        // 返回预签名 URL 的字符串表示形式
        return url.toString();
    }

获取到的url是可以直接在浏览器预览的,但是要用其他插件(如 pdf.js)预览的话会出现跨域的错误,这个时候就需要给要访问的桶添加一下cors规则。

代码添加cors规则:

    CORSRule corsRule = CORSRule.builder()
            .allowedOrigins("http://ip:端口")//可以设置多个
        	//.allowedOrigins("*")
            .allowedHeaders("Authorization")
            .allowedMethods("GET","HEAD")
            .exposeHeaders("Access-Control-Allow-Origin").build();

    // 设置 CORS 配置
    s3Client.putBucketCors(PutBucketCorsRequest.builder()
            .bucket(bucketName)
            .corsConfiguration(CORSConfiguration.builder()
                    .corsRules(corsRule)
                    .build())
            .build());

S3控制台添加CORS规则:

点击你的桶选择权限,下拉找到这个设置,编辑添加下面的规则:

[
    {
        "AllowedHeaders": [
            "Authorization"
        ],
        "AllowedMethods": [
            "GET",
            "HEAD"
        ],
        "AllowedOrigins": [
            "http://ip:端口"
        ],
        "ExposeHeaders": [
            "Access-Control-Allow-Origin"
        ],
        "MaxAgeSeconds": 3000
    }
]

用S3 Browser添加

S3 Browser 是一款用于管理 Amazon S3 存储服务的第三方工具。它提供了一个图形用户界面(GUI),让用户能够更方便地上传、下载、管理和浏览存储在 Amazon S3 中的对象和存储桶。也可以用来连接minio或者其他存储。

添加你的配置等待扫描出桶名后,右击桶名选择CORS Configuration添加你的规则,点击apply使用。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <CORSRule>
    <AllowedOrigin>http://ip:端口</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>HEAD</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <ExposeHeader>Access-Control-Allow-Origin</ExposeHeader>
    <AllowedHeader>Authorization</AllowedHeader>
  </CORSRule>
</CORSConfiguration>

部分方法集

因为是另一个项目的延伸版,所以有些方法反而改的更繁琐的一些。

S3Service
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.services.s3.model.Bucket;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
import software.amazon.awssdk.services.s3.model.S3Object;

import java.io.InputStream;
import java.util.List;
import java.util.Map;

/**
 * 定义与 AWS S3 客户端交互的一般功能。
 */
@Service
public interface S3Service {

    /**
     * 判断指定的存储桶是否存在。
     *
     * @param bucketName 存储桶名称。
     * @return 如果存储桶存在返回 true,否则返回 false。
     */
    boolean ifExistsBucket(String bucketName);

    /**
     * 创建一个新的存储桶。如果存储桶已存在,则抛出运行时异常。
     *
     * @param bucketName 新建的存储桶名称。
     * @return 如果存储桶创建成功返回 true。
     * @throws RuntimeException 如果存储桶已存在。
     */
    boolean createBucket(String bucketName) throws RuntimeException;

    /**
     * 删除一个存储桶。存储桶必须为空;否则不会被删除。
     * 即使指定的存储桶不存在也会返回 true。
     *
     * @param bucketName 要删除的存储桶名称。
     * @return 如果存储桶被删除或不存在返回 true。
     */
    boolean removeBucket(String bucketName);

    /**
     * 列出当前 S3 服务器上所有存在的存储桶。
     *
     * @return 存储桶列表。
     */
    List<Bucket> alreadyExistBuckets();

    /**
     * 列出存储桶中的对象,可选地通过前缀过滤并指定是否包括子目录。
     *
     * @param bucketName 存储桶名称。
     * @param predir     前缀过滤条件。
     * @param recursive  是否包括子目录。
     * @return S3 对象列表。
     */
    List<S3Object> listObjects(String bucketName, String predir, boolean recursive);

    /**
     * 列出存储桶中的对象,通过前缀过滤。
     *
     * @param bucketName 存储桶名称。
     * @param predir     前缀过滤条件。
     * @return S3 对象列表。
     */
    List<S3Object> listObjects(String bucketName, String predir);

    /**
     * 复制一个对象文件从一个存储桶到另一个存储桶。
     *
     * @param pastBucket 源文件所在的存储桶。
     * @param pastObject 源文件在存储桶内的路径。
     * @param newBucket  将要复制进的目标存储桶。
     * @param newObject  将要复制进的目标存储桶内的路径。
     * @return 如果复制成功返回 true。
     */
    boolean copyObject(String pastBucket, String pastObject, String newBucket, String newObject);

    /**
     * 下载一个对象。
     *
     * @param bucketName 存储桶名称。
     * @param objectName 对象路径及名称(例如:2022/02/02/xxx.doc)。
     * @param targetPath 目标路径及名称(例如:/opt/xxx.doc)。
     * @return 如果下载成功返回 true。
     */
    boolean downObject(String bucketName, String objectName, String targetPath);

    /**
     * 返回对象的签名 URL。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象路径及名称。
     * @param expire     过期时间(分钟)。
     * @return 签名 URL。
     */
    String presignedURLofObject(String bucketName, String object, int expire);

    /**
     * 返回带有额外参数的对象签名 URL。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象路径及名称。
     * @param expire     过期时间(分钟)。
     * @param map        额外参数。
     * @return 签名 URL。
     */
    String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map);

    /**
     * 删除一个对象。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象名称(路径)。
     * @return 如果删除成功返回 true。
     */
    boolean deleteObject(String bucketName, String object);

    /**
     * 上传一个对象,使用本地文件作为源。
     *
     * @param bucketName 存储桶名称。
     * @param targetObject 目标对象的名称。
     * @param sourcePath   本地文件路径(例如:/opt/1234.doc)。
     * @return 如果上传成功返回 true。
     */
    boolean uploadObject(String bucketName, String targetObject, String sourcePath);

    /**
     * 上传一个对象,使用输入流作为源。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象名称。
     * @param inputStream 输入流。
     * @param size       对象大小。
     * @return 如果上传成功返回 true。
     */
    boolean putObject(String bucketName, String object, InputStream inputStream, long size);

    /**
     * 上传一个带有标签的对象。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象名称。
     * @param inputStream 输入流。
     * @param size       对象大小。
     * @param tags       标签集合。
     * @return 如果上传成功返回 true。
     */
    boolean putObject(String bucketName, String object, InputStream inputStream, long size, Map<String, String> tags);

    /**
     * 获取一个对象。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象名称。
     * @return GetObjectResponse 类型的输入流。
     */
    InputStream getObject(String bucketName, String object);

    /**
     * 获取一个对象的内容作为字节流。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象名称。
     * @return 包含对象内容的字节流。
     */
    ResponseBytes<GetObjectResponse> getObjectAsBytes(String bucketName, String object);

    /**
     * 判断一个文件是否存在于存储桶中。
     *
     * @param bucketName 存储桶名称。
     * @param filename   文件名称。
     * @param recursive  是否递归搜索。
     * @return 如果文件存在返回 true。
     */
    boolean fileifexist(String bucketName, String filename, boolean recursive);

    /**
     * 获取一个对象的标签。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象名称。
     * @return 标签集合。
     */
    Map<String, String> getTags(String bucketName, String object);

    /**
     * 为存储桶内的对象添加标签。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象名称。
     * @param addTags    要添加的标签。
     * @return 如果添加成功返回 true。
     */
    boolean addTags(String bucketName, String object, Map<String, String> addTags);

    /**
     * 获取对象的对象信息和元数据。
     *
     * @param bucketName 存储桶名称。
     * @param object     对象名称。
     * @return HeadObjectResponse 类型的对象信息和元数据。
     */
    HeadObjectResponse statObject(String bucketName, String object);

    /**
     * 判断一个对象是否存在。
     *
     * @param bucketName 存储桶名称。
     * @param objectName 对象名称。
     * @return 如果对象存在返回 true。
     */
    boolean ifExistObject(String bucketName, String objectName);

    /**
     * 从其他对象名称中获取元数据名称。
     *
     * @param objectName 对象名称。
     * @return 元数据名称。
     */
    String getMetaNameFromOther(String objectName);

    /**
     * 更改对象的标签。
     *
     * @param object 对象名称。
     * @param tag    新的标签。
     * @return 如果更改成功返回 true。
     */
    boolean changeTag(String object, String tag);

    /**
     * 设置存储桶为公共访问。
     *
     * @param bucketName 存储桶名称。
     */
    void BucketAccessPublic(String bucketName);

}
S3ServiceImpl
import com.xagxsj.erms.model.BucketName;
import com.xagxsj.erms.model.ObjectTags;
import com.xagxsj.erms.service.S3Service;
import com.xagxsj.erms.utils.FileUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;

import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.time.Duration;
import java.util.*;
import java.util.logging.Logger;
import java.util.stream.Collectors;

@Service
public class S3ServiceImpl implements S3Service {
    private final Logger log = Logger.getLogger(this.getClass().getName());
    @Qualifier("s3Client")
    @Autowired
    S3Client s3Client;

    @Qualifier("s3Presigner")
    @Autowired
    S3Presigner s3Presigner;
    
    @Override
    public boolean ifExistsBucket(String bucketName) {
        // 尝试发送 HEAD 请求来检查存储桶是否存在
        try {
            HeadBucketResponse headBucketResponse = s3Client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build());
        } catch (S3Exception e) {
            // 如果捕获到 S3 异常且状态码为 404,则说明存储桶不存在
            if (e.statusCode() == 404) {
                return false;
            } else {
                // 打印异常堆栈跟踪
                e.printStackTrace();
            }
        }
        // 如果没有抛出异常或状态码不是 404,则说明存储桶存在
        return true;
    }

    @Override
    public boolean createBucket(String bucketName) throws RuntimeException {
        // 检查存储桶是否已经存在
        if (ifExistsBucket(bucketName)) {
            // 如果存储桶已存在,则抛出运行时异常
            throw new RuntimeException("桶已存在");
        }
        // 创建新的存储桶
        S3Response bucket = s3Client.createBucket(CreateBucketRequest.builder().bucket(bucketName).build());
        // 检查存储桶是否创建成功
        return ifExistsBucket(bucketName);
    }

    @Override
    public boolean removeBucket(String bucketName) {
        // 如果存储桶不存在,则直接返回 true
        if (!ifExistsBucket(bucketName)) {
            return true;
        }
        // 删除存储桶
        s3Client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build());
        // 检查存储桶是否已被删除
        return !ifExistsBucket(bucketName);
    }

    @Override
    public List<Bucket> alreadyExistBuckets() {
        // 列出所有存在的存储桶
        List<Bucket> buckets = s3Client.listBuckets().buckets();
        return buckets;
    }

    @Override
    public boolean fileifexist(String bucketName, String filename, boolean recursive) {
        // 初始化一个布尔值用于标记文件是否存在
        boolean flag = false;
        // 构建第一次的 ListObjectsV2 请求
        ListObjectsV2Request request = ListObjectsV2Request.builder().bucket(bucketName).build();
        ListObjectsV2Response response;
        // 循环处理直到所有分页的数据都被检索
        do {
            // 发送请求并获取响应
            response = s3Client.listObjectsV2(request);
            // 遍历响应中的内容
            for (S3Object content : response.contents()) {
                // 如果找到匹配的文件名,则设置标志位为真
                if (content.key().equals(filename)) {
                    flag = true;
                    break;
                }
            }
            // 构建下一次请求,如果响应被截断,则继续获取下一页的数据
            request = ListObjectsV2Request.builder()
                    .bucket(bucketName)
                    .continuationToken(response.nextContinuationToken())
                    .build();
        } while (response.isTruncated());
        // 返回文件是否存在
        return flag;
    }

    @Override
    public List<S3Object> listObjects(String bucketName, String predir, boolean recursive) {
        // 构建 ListObjects 请求以列出具有指定前缀的文件
        List<S3Object> contents = s3Client.listObjects(ListObjectsRequest.builder()
                .bucket(bucketName)
                .prefix(predir)
                .maxKeys(1000)
                .build()).contents();
        return contents;
    }

    @Override
    public List<S3Object> listObjects(String bucketName, String predir) {
        // 构建 ListObjects 请求以列出具有指定前缀的文件
        List<S3Object> contents = s3Client.listObjects(ListObjectsRequest.builder()
                .bucket(bucketName)
                .prefix(predir)
                .maxKeys(1000)
                .build()).contents();
        return contents;
    }

    @Override
    public boolean copyObject(String pastBucket, String pastObject, String newBucket, String newObject) {
        // 尝试复制对象
        try {
            CopyObjectResponse copyObjectResponse = s3Client.copyObject(CopyObjectRequest.builder()
                    .sourceBucket(pastBucket)
                    .sourceKey(pastObject)
                    .destinationBucket(newBucket)
                    .destinationKey(newObject)
                    .build());
        } catch (Exception e) {
            // 如果出现异常则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果复制成功则返回 true
        return true;
    }

    @Override
    public boolean downObject(String bucketName, String objectName, String targetPath) {
        // 尝试下载对象到指定路径
        try {
            s3Client.getObject(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(objectName)
                    .build(), new File(targetPath).toPath());
        } catch (Exception e) {
            // 如果出现异常则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果下载成功则返回 true
        return true;
    }

    @Override
    public String presignedURLofObject(String bucketName, String object, int expire) {
        URL url = null;
        // 尝试生成预签名 URL
        try {
            url = s3Presigner.presignGetObject(GetObjectPresignRequest.builder()
                    .signatureDuration(Duration.ofMinutes(expire))
                    .getObjectRequest(GetObjectRequest.builder()
                            .bucket(bucketName)
                            .key(object)
                            .build())
                    .build()).url();
        } catch (Exception e) {
            // 如果出现异常则打印异常堆栈跟踪并返回 null
            e.printStackTrace();
            return null;
        } finally {
            // 关闭预签名客户端
            s3Presigner.close();
        }
        // 返回预签名 URL 的字符串表示形式
        return url.toString();
    }

    @Override
    public String presignedURLofObject(String bucketName, String object, int expire, Map<String, String> map) {
        URL url = null;
        // 尝试生成预签名 URL
        try {
            url = s3Presigner.presignGetObject(GetObjectPresignRequest.builder()
                    .signatureDuration(Duration.ofMinutes(expire))
                    .getObjectRequest(GetObjectRequest.builder()
                            .bucket(bucketName)
                            .key(object)
                            .build())
                    .build()).url();
        } catch (Exception e) {
            // 如果出现异常则打印异常堆栈跟踪并返回 null
            e.printStackTrace();
            return null;
        } finally {
            // 关闭预签名客户端
            s3Presigner.close();
        }
        // 返回预签名 URL 的字符串表示形式
        return url.toString();
    }

    @Override
    public boolean deleteObject(String bucketName, String object) {
        // 尝试删除对象
        try {
            s3Client.deleteObject(DeleteObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出现异常则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果删除成功则返回 true
        return true;
    }
    
    @Override
    public boolean uploadObject(String bucketName, String targetObject, String sourcePath) {
        // 尝试上传本地文件到指定的存储桶和对象键
        try {
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(targetObject)
                    .build(), RequestBody.fromFile(new File(sourcePath)));
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上传成功则返回 true
        return true;
    }

    @Override
    public boolean putObject(String bucketName, String object, InputStream inputStream, long size) {
        // 尝试从输入流上传数据到指定的存储桶和对象键
        try {
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build(), RequestBody.fromInputStream(inputStream, size));
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上传成功则返回 true
        return true;
    }

    @Override
    public boolean putObject(String bucketName, String object, InputStream inputStream, long size, Map<String, String> tags) {
        // 尝试从输入流上传数据到指定的存储桶和对象键,并设置标签
        try {
            // 将标签映射转换为标签集合
            Collection<Tag> tagList = tags.entrySet().stream().map(entry ->
                    Tag.builder().key(entry.getKey()).value(entry.getValue()).build())
                    .collect(Collectors.toList());
            // 上传对象并设置标签
            s3Client.putObject(PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .tagging(Tagging.builder()
                            .tagSet(tagList)
                            .build())
                    .build(), RequestBody.fromInputStream(inputStream, size));
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果上传成功则返回 true
        return true;
    }

    @Override
    public InputStream getObject(String bucketName, String object) {
        InputStream objectStream = null;
        // 尝试获取指定存储桶和对象键的输入流
        try {
            objectStream = s3Client.getObject(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 null
            e.printStackTrace();
            return null;
        }
        // 返回对象的输入流
        return objectStream;
    }

    @Override
    public ResponseBytes<GetObjectResponse> getObjectAsBytes(String bucketName, String object) {
        ResponseBytes<GetObjectResponse> objectAsBytes = null;
        // 尝试获取指定存储桶和对象键的内容作为字节
        try {
            objectAsBytes = s3Client.getObjectAsBytes(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 null
            e.printStackTrace();
            return null;
        }
        // 返回对象的内容作为字节
        return objectAsBytes;
    }

    @Override
    public Map<String, String> getTags(String bucketName, String object) {
        Map<String, String> tags = null;
        // 尝试获取指定存储桶和对象键的标签
        try {
            List<Tag> tagList = s3Client.getObjectTagging(GetObjectTaggingRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build()).tagSet();
            // 将标签集合转换为标签映射
            tags = tagList.stream().collect(Collectors.toMap(Tag::key, Tag::value));
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 null
            e.printStackTrace();
            return null;
        }
        // 返回标签映射
        return tags;
    }

    @Override
    public boolean addTags(String bucketName, String object, Map<String, String> addTags) {
        Map<String, String> oldTags = new HashMap<>();
        Map<String, String> newTags = new HashMap<>();
        // 获取现有标签
        try {
            oldTags = getTags(bucketName, object);
            if (oldTags.size() > 0) {
                newTags.putAll(oldTags);
            }
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并提示没有旧标签
            e.printStackTrace();
            System.out.println("原存储桶无老旧标签");
        }
        // 添加新标签
        if (addTags != null && addTags.size() > 0) {
            newTags.putAll(addTags);
        }
        // 将新标签集合转换为标签列表
        Collection<Tag> tagList = newTags.entrySet().stream().map(entry ->
                Tag.builder().key(entry.getKey()).value(entry.getValue()).build())
                .collect(Collectors.toList());
        // 设置新标签
        try {
            s3Client.putObjectTagging(PutObjectTaggingRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .tagging(Tagging.builder()
                            .tagSet(tagList).build())
                    .build());
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果设置成功则返回 true
        return true;
    }

    @Override
    public HeadObjectResponse statObject(String bucketName, String object) {
        HeadObjectResponse headObjectResponse = null;
        // 尝试获取指定存储桶和对象键的元数据
        try {
            headObjectResponse = s3Client.headObject(HeadObjectRequest.builder()
                    .bucket(bucketName)
                    .key(object)
                    .build());
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 null
            e.printStackTrace();
            return null;
        }
        // 返回对象的元数据
        return headObjectResponse;
    }

    @Override
    public boolean ifExistObject(String bucketName, String objectName) {
        // 检查指定存储桶和对象键的对象是否存在
        return listObjects(bucketName, objectName, true).size() >= 1;
    }

    @Override
    public String getMetaNameFromOther(String objectName) {
        String metaObject = "";
        // 获取元数据存储桶中特定前缀的对象列表
        List<S3Object> s3Objects = listObjects(BucketName.METADATA, FileUtil.getPreMeta(objectName), true);
        if (s3Objects.size() == 1) {
            try {
                // 获取第一个对象的键并获取其标签
                metaObject = s3Objects.get(0).key();
                Map<String, String> tags = getTags(BucketName.METADATA, metaObject);
                // 编码文件名标签
                String fileName = tags.get(ObjectTags.FILENAME);
                return URLEncoder.encode(fileName, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                // 如果出现异常,则打印异常堆栈跟踪
                e.printStackTrace();
            }
        }
        // 如果未找到对象或编码失败,则返回原始文件名
        return FileUtil.getFileName(metaObject);
    }

    @Override
    public boolean changeTag(String object, String tag) {
        // 尝试更改指定对象的标签
        try {
            s3Client.putObjectTagging(PutObjectTaggingRequest.builder()
                    .bucket(BucketName.METADATA)
                    .key(object)
                    .tagging(Tagging.builder()
                            .tagSet(Tag.builder()
                                    .key(ObjectTags.FILENAME)
                                    .value(tag)
                                    .build())
                            .build())
                    .build());
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪并返回 false
            e.printStackTrace();
            return false;
        }
        // 如果更改成功则返回 true
        return true;
    }

    @Override
    public void BucketAccessPublic(String bucketName) {
        // 设置存储桶策略为公共访问
        String config = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
        try {
            // 应用存储桶策略
            s3Client.putBucketPolicy(PutBucketPolicyRequest.builder()
                    .bucket(bucketName)
                    .policy(config).build());
        } catch (Exception e) {
            // 如果出现异常,则打印异常堆栈跟踪
            e.printStackTrace();
        }
    }
}

前言:

学习ComfyUI是一场持久战,而ComfyUI layer style 是一组专为图片设计制作且集成了Photoshop功能的强大节点。该节点几乎将PhotoShop的全部功能迁移到ComfyUI,诸如提供仿照Adobe Photoshop的图层样式、提供调整颜色功能(亮度、饱和度、对比度等)、提供Mask辅助工具、提供图层合成工具和工作流相关的辅助节点、提供图像效果滤镜等。旨在集中工作平台,使我们可以在ComfyUI中实现PhotoShop的一些基础功能。

目录

一、安装方式

二、LayerColor:LUT Apply节点

三、LayerColor:AutoBrightness节点

四、LayerColor:ColorAdapter节点

五、LayerColor:Exposure节点

六、LayerColor:Color of Shadow & HighLight节点

七、LayerColor:Gamma节点

八、LayerColor:Brightness & Contrast节点

九、LayerColor:RGB \ LayerColor:YUV \ LayerColor:LAB \ LayerColor:HSV节点

一、安装方式

方法一:通过ComfyUI Manager安装(推荐)

打开Manager界面

1

2

方法二:使用git clone命令安装

在ComfyUI/custom_nodes目录下输入cmd按回车进入电脑终端

3

在终端输入下面这行代码开始下载

git clone https://github.com/chflame163/ComfyUI_LayerStyle.git

4

二、LayerColor:LUT Apply节点

此节点专注于使用查找表对图像进行颜色调整。查找表是一种预定义的颜色映射表,可以将输入图像的颜色映射到新的颜色空间,从而实现颜色校正或风格化效果。

5

输入:

image → 输入的图片

参数:

LUT → 这里列出了LUT文件夹中可用的.cube文件列表,选中的LUT文件将被应用到图像

color_space → 色彩空间 **普通图片请选择linear, log色彩空间的图片请选择log**

输出:

image → 处理后的图片

示例:

6

注意事项

· LUT格式:确保使用的查找表格式与节点兼容,常见的LUT格式包括 .cube、.png 等。

· 颜色空间匹配:输入图像的颜色空间应与查找表的预期颜色空间匹配,以获得最佳效果。

· 处理性能:应用查找表的处理可能需要较高的计算资源,确保系统性能足够支持处理需求。

· LUT选择:根据具体的图像处理需求选择合适的LUT,以实现预期的颜色调整效果。

通过使用LayerColor: LUT Apply节点,可以在图像处理工作流程中实现高效的颜色调整和风格化处理,提升图像的视觉效果和艺术表现力。

三、LayerColor:AutoBrightness节点

通过对图像的亮度直方图进行分析,该节点可以动态调整图像的亮度,使其更符合视觉上的最佳亮度分布。

7

输入:

image → 输入的图片

mask → 输入遮罩

参数:

strength → 自动调整亮度的强度 **数值越大,越偏向中间值,与原图的差别越大**

saturation → 色彩饱和度 **亮度改变通常会导致色彩饱和度发生变化,可在此适当调整补偿**

输出:

image → 处理后的图片

示例:

8
9

注意事项

· 输入图像质量:确保输入图像的质量良好,避免过度曝光或过暗区域,因为这些问题可能影响自动亮度调整效果。

· 处理性能:自动亮度调整处理可能需要一定的计算资源,确保系统性能足够支持处理需求。

· 结果检查:虽然自动亮度调整通常能提供良好的效果,但对于某些特定场景,手动微调可能仍然是必要的,以确保达到最佳效果。

通过使用LayerColor: AutoBrightness节点,可以在图像处理工作流程中实现高效的亮度自动优化,提升图像的视觉效果和亮度平衡。

四、LayerColor:ColorAdapter节点

此节点专注于通过调整图像的颜色,使其符合预定的颜色方案或风格。

10

输入:

image → 输入的图片

color_ref_image → 输入参考颜色图片

参数:

opacity → 图像调整色调之后的不透明度

输出:

image → 处理后的图片

示例:

11

注意事项

· 颜色方案选择:确保选择适合处理目标的颜色方案,以实现预期的视觉效果。

· 输入图像质量:输入图像的质量会影响颜色调整的效果,确保图像清晰、色彩信息完整。

· 处理性能:颜色调整处理可能需要一定的计算资源,确保系统性能足够支持处理需求。

· 结果检查:虽然颜色调整可以自动应用,但手动微调可能仍然必要,以确保达到最佳效果。

通过使用LayerColor: ColorAdapter节点,可以在图像处理工作流程中实现高效的颜色调整和风格转换,提升图像的视觉效果和一致性。

五、LayerColor:Exposure节点

此节点专注于调整图像的曝光度。通过增加或减少曝光值,可以使图像变得更亮或更暗,以达到预期的视觉效果。

12

输入:

image → 输入的图片

参数:

exposure → 曝光值 **更高的数值表示更亮的曝光,可以为负数**

输出:

image → 处理后的图片

示例:

13

注意事项

· 曝光值配置:根据具体需求配置合适的曝光值,确保调整后的图像亮度符合预期。曝光值为正值时增加曝光,为负值时减少曝光。

· 输入图像质量:输入图像的质量会影响曝光调整的效果,确保图像清晰,曝光问题主要集中在中间亮度区域。

· 处理性能:曝光调整处理可能需要一定的计算资源,确保系统性能足够支持处理需求。

· 结果检查:调整曝光后,检查图像细节和色彩是否保持一致,以防止过度调整导致图像失真或细节丢失。

通过使用LayerColor: Exposure节点,可以在图像处理工作流程中实现高效的曝光调整,提升图像的亮度和整体视觉效果。

六、LayerColor:Color of Shadow & HighLight节点

此节点专注于对图像的阴影和高光区域进行颜色调整。通过单独控制这两个区域的颜色,可以实现更细致的色彩校正和风格化处理。

14

输入:

image → 输入的图片

mask → 遮罩

参数:

shadow_brightness → 暗部的亮度

shadow_saturation → 暗部的色彩饱和度

shadow_hue → 暗部的色相

shadow_level_offset → 暗部取值的偏移量 **数值越大则更多靠近明亮的区域纳入暗部**

shadow_range → 暗部的过渡范围

highlight_brightness → 亮部的亮度

highlight_saturation → 亮部的色彩饱和度

highlight_hue → 亮部的色相

highlight_level_offset → 亮部取值的偏移量 **数值越小则更多靠近阴暗的区域纳入亮部**

highlight_range → 亮部的过渡范围

输出:

image → 处理后的图片

示例:

15
16

注意事项

· 颜色参数配置:根据具体需求配置阴影和高光的颜色参数,确保调整后的图像色彩符合预期。

· 输入图像质量:输入图像的质量会影响颜色调整的效果,确保图像的阴影和高光区域明确且没有严重的颜色偏移。

· 处理性能:颜色调整处理可能需要一定的计算资源,确保系统性能足够支持处理需求。

· 结果检查:调整阴影和高光颜色后,检查图像的整体色彩平衡和视觉效果,确保没有过度调整导致的色彩失真。

通过使用LayerColor: Color of Shadow & HighLight节点,可以在图像处理工作流程中实现高效的阴影和高光颜色调整,提升图像的色彩表现和整体视觉效果。

七、LayerColor:Gamma节点

此节点专注于对图像进行伽马校正。通过调整伽马值,可以改变图像的亮度和对比度,优化图像的整体视觉效果。

17

输入:

image → 输入的图片

参数:

gamma → 设置处理后图像Gamma值

输出:

image → 处理后的图片

示例:

18

注意事项

· 伽马值配置:根据具体需求配置合适的伽马值,确保调整后的图像亮度和对比度符合预期。伽马值大于1会降低亮度,伽马值小于1会增加亮度。

· 输入图像质量:输入图像的质量会影响伽马校正的效果,确保图像中没有过多的噪声和失真。

· 处理性能:伽马校正处理可能需要一定的计算资源,确保系统性能足够支持处理需求。

· 结果检查:调整伽马值后,检查图像的整体视觉效果,确保没有过度调整导致的图像失真或细节丢失。

通过使用LayerColor: Gamma节点,可以在图像处理工作流程中实现高效的伽马校正,优化图像的亮度和对比度,提升图像的整体视觉效果。

八、LayerColor:Brightness & Contrast节点

此节点专注于对图像进行亮度和对比度的调整。通过独立控制这两个参数,可以显著改善图像的视觉效果。

19

输入:

image → 输入的图片

参数:

brightness → 图像的亮度

contrast → 图像的对比度

saturation → 图像的色彩饱和度

输出:

image → 处理后的图片

示例:

20

注意事项

· 亮度和对比度配置:根据具体需求配置合适的亮度和对比度值,确保调整后的图像效果符合预期。亮度值和对比度值通常在-100到100之间。

· 输入图像质量:输入图像的质量会影响亮度和对比度调整的效果,确保图像中没有过多的噪声和失真。

· 处理性能:亮度和对比度调整处理可能需要一定的计算资源,确保系统性能足够支持处理需求。

· 结果检查:调整亮度和对比度后,检查图像的整体视觉效果,确保没有过度调整导致的图像失真或细节丢失。

通过使用LayerColor: Brightness & Contrast节点,可以在图像处理工作流程中实现高效的亮度和对比度调整,优化图像的视觉效果,使图像更加清晰和吸引人。

九、LayerColor:RGB \ LayerColor:YUV \ LayerColor:LAB \ LayerColor:HSV节点

分别调整图像的RGB、YUV、LAB、HSV通道。

21

输入:

image → 输入的图片

参数:

R → R通道 **红色通道**

G → G通道 **绿色通道**

B → B通道 **蓝色通道**

H → H通道 **色调、色相通道**

S → S通道 **饱和度、色彩纯净度通道**

V → V通道 **明度通道**

L → L通道 **亮度通道**

A → A通道 **从绿色到红色的分量通道**

B → B通道 **从蓝色到黄色的分量通道**

Y → Y通道 **强度、亮度通道**

U → U通道 **蓝色色度通道**

V → V通道 **色调、色相通道**

输出:

image → 处理后的图片

示例:

22
23

注意事项

· 参数配置:根据具体需求配置合适的颜色参数,确保调整后的图像效果符合预期。

· 输入图像质量:输入图像的质量会影响颜色调整的效果,确保图像的色彩信息完整。

· 处理性能:颜色调整处理可能需要一定的计算资源,确保系统性能足够支持处理需求。

· 结果检查:调整颜色后,检查图像的整体色彩平衡和视觉效果,确保没有过度调整导致的色彩失真。

· 通过使用这些LayerColor节点,可以在图像处理工作流程中实现高效的颜色调整和优化,提升图像的视觉效果和色彩表现力。

**孜孜以求,方能超越自我。坚持不懈,乃是成功关键。**

此篇文章将在Jmeter创建一个新函数,实现替换文本中的指定内容功能。效果图如下

1、eclipse项目创建步骤此处省略,可参考上一篇
Jmeter二次开发函数之入门

2、新建class命名为“TextReplaceFunction”,并继承jmeter自带的AbstractFunction

3、新生成文件TextReplaceFunction.java继承jmeter的AbstractFunction带出4个方法,函数开发就是在这4个方法上改造

4、TextReplaceFunction.java,功能实现的完整代码如下

packageorg.apache.jmeter.functions;importjava.util.Collection;importjava.util.LinkedList;importjava.util.List;importorg.apache.jmeter.engine.util.CompoundVariable;importorg.apache.jmeter.samplers.SampleResult;importorg.apache.jmeter.samplers.Sampler;public class TextReplaceFunction extendsAbstractFunction {private final static String key="__TextReplace";private static List<String> strParams=new LinkedList<String>();static{
strParams.add(
"原始文本(必填)");
strParams.add(
"被替换内容(必填)");
strParams.add(
"替换为(必填)");
}
public String originalText="";public String text="";public String ReplaceText="";

@Override
public List<String>getArgumentDesc() {returnstrParams;
}

@Override
public String execute(SampleResult arg0, Sampler arg1) throwsInvalidVariableException {
String result
=originalText.replaceAll(text, ReplaceText);returnresult;
}

@Override
publicString getReferenceKey() {returnkey;
}

@Override
public void setParameters(Collection<CompoundVariable> arg0) throwsInvalidVariableException {
checkParameterCount(arg0,
3);
Object[] data
=arg0.toArray();
originalText
=((CompoundVariable)data[0]).execute();
text
=((CompoundVariable)data[1]).execute();
ReplaceText
=((CompoundVariable)data[2]).execute();
}
}

5、TextReplaceFunction.java文件右键导出jar包

选择java->JAR file,点击next

保存到jmeter安装目录下\apache-jmeter-5.5\lib\ext\TextReplaceFunction.jar

6、重启jmeter,打开函数助手就能看到多了一个TextReplace函数

查看TextReplace函数有3个参数

7、TextReplace函数使用效果