简介:面向需要对接华为对象存储服务的Java开发者,这份华为OBS SDK 3.20.3离线包提供从接口查阅到示例参考的完整材料,适合初中级开发者熟悉SDK能力并用于项目集成。包内共766个文件,以449个HTML接口文档和286个Java示例与源码为主,辅以10个XML配置、9个JAR依赖及少量说明文档,压缩包约7.31MB,整体偏向API查阅与代码参考。已有1853人学习下载,在OBS Java接入场景中具有一定参考价值。读者可据此了解ObsClient等核心类的调用方式,查看上传、下载、列举桶、删除对象等示例,并借助Javadoc、源码、依赖配置与日志说明排查集成问题;对需要离线查阅接口、对照示例实现和定位异常处理思路的开发者较为实用。
1. 从一次内网构建机的启动失败说起
对象存储的 Java 接入,几乎每个后台项目都会碰上。那一次是给一个内网构建环境加上传能力,构建机不通外网,只拿到一个 esdk-obs-java-3.20.3.zip。我解压,把 esdk-obs-java-3.20.3.jar 拷进 WEB-INF/lib,本地 main 方法跑通,提交到测试环境却起不来:NoClassDefFoundError: okhttp3/OkHttpClient。报错里没有出现 esdk-obs-java 相关字样,我却在一个多小时里反复翻查 SDK 自己的 jar,结果没查出问题。反直觉的地方在这儿——3.20.3 的主 jar 只装 SDK 自己的类,真正发请求的 HTTP 层、解析 XML 的那一层,全在 lib 目录那堆第三方包里。这篇就把这套环境从 JDK 对齐、jar 清点、安装路径,到客户端参数、安装失败现场、上传下载闭环走一遍,适合要在离线或半离线环境接 OBS Java SDK 的人。
2. 环境盘点:装 esdk-obs-java 3.20.3 之前先对齐 JDK 和依赖
动手写第一行代码之前,我更愿意花二十分钟把环境列清楚。这一步省下来,后面就要用一整天去查 NoClassDefFoundError,运维的吃苦耐劳不该花在这种地方。
2.1 JDK 版本怎么选:8 是下限,11 是常态,17 要留个心眼
esdk-obs-java 3.x 这条线的主 jar 编译目标是 Java 8,也就是说 class 文件版本 52.0,跑在 JDK 8、11、17 上都没问题。真正会让人出错的不是 SDK,是构建机上的 JDK 和你本地 IDE 里配置的那个 JDK 不是同一个。这个情况在离线环境里尤其常见:机器上装了三个 JDK,PATH 指一个,JAVA_HOME 指另一个,Maven 的 toolchain 又指第三个。
我一般先跑四条命令,把输出结果相互比对:
# 运行时版本 java -version 2>&1 # 编译器版本,必须和上面同源 javac -version # 构建工具实际用的 JDK mvn -v # 环境变量指向,和 mvn -v 里的 Java home 对一下 echo "$JAVA_HOME"判断标准很直接:java -version和javac -version报的大版本号必须一致,mvn -v里那行 Java version 必须和$JAVA_HOME对得上。如果对不上,先改JAVA_HOME,再确认PATH里$JAVA_HOME/bin排在系统自带 JDK 前面。在 JDK 11 和 17 上,OBS SDK 本体不需要额外的--add-opens参数,它不是模块化改造过的包;容易出问题的是配套的老版本 httpclient 和一些框架的反射告警,那些是宿主工程的问题,不是 SDK 的。
顺带记一下 class 文件版本对照,排查 “本地能编译、服务器跑不了” 时一眼就能定位:52.0 是 JDK 8,55.0 是 JDK 11,61.0 是 JDK 17。用javap -verbose Xxx.class | grep major查,比猜快。
2.2 解压第一件事是清点 jar,不是写代码
zip 包解出来的目录结构,和我第一次见到时想的不一样。主 jar 在根目录,第三方依赖在 lib 下,可能还有示例代码目录和一份版本说明。先看清单:
unzip -l esdk-obs-java-3.20.3.zip # 解到指定目录,不要在当前目录里铺开 unzip -q esdk-obs-java-3.20.3.zip -d esdk-obs # 数一下 lib 下有几个 jar,这个数字后面做校验要用 ls -1 esdk-obs/lib/*.jar | wc -lunzip -l用来在解压前确认包内层级,避免解出来一堆散文件;-d指定子目录,是为了后面能整目录打包、整目录回滚;最后那条wc -l得到的数字,是我做部署核对用的基准值——上线时目标机器上的 jar 数量比这个少,就说明拷贝漏了。
依赖清单大致是这样,具体文件名以包里实际为准:
| 文件 | 作用 | 缺了会怎样 |
|---|---|---|
| esdk-obs-java-3.20.3.jar | SDK 主体,ObsClient 与各类 Request/Result | 编译期就报 cannot find symbol |
| okhttp-*.jar | HTTP 客户端,3.x 的请求基本都走它 | 启动即 NoClassDefFoundError: okhttp3/OkHttpClient |
| okio-*.jar | okhttp 的 IO 底层 | 报错顺序在 okhttp 之后,NoClassDefFoundError: okio/Buffer |
| jdom2-*.jar | 解析 XML 响应 | 简单 PUT 能跑通,列举桶、分片上传直接炸 |
| httpclient / httpcore | 部分接口走 Apache HttpClient | 只有特定接口报错,最难查的一类 |
| slf4j-api | 日志门面 | 启动一行 Warning,功能正常 |
| 日志绑定(log4j / slf4j-log4j12 等) | 把日志真正落盘 | 有 Warning,没有日志输出 |
表里最值得记的是 jdom2 那一行。它造成的故障有个特点——你测最简单的上传下载都成功,一调列桶接口就挂了,很容易误判成权限问题。
2.3 冲突预判:工程里已经有的 okhttp 和 slf4j 怎么办
新起的项目最省事,照包里带的版本用就行。麻烦的是老工程,里面早就有一份 okhttp,多半是你用的某个网络框架带进来的。两套 okhttp 同时出现在 classpath 上,运行期加载哪个取决于顺序,表现就是 “在 A 机器上是好的,在 B 机器上签名报错”。
先用 dependency:tree 把现有版本查出来,再决定留谁:
# 只看关心的这几个坐标,输出短,好读 mvn -q dependency:tree \ -Dincludes=com.squareup.okhttp3:okhttp,com.squareup.okio:okio,org.jdom:jdom2,org.slf4j:slf4j-api输出里+-和\-表示层级,omitted for conflict with x.x.x就是被 Maven 调解掉的那个版本。我的取舍原则是:工程里已有的 okhttp 版本高于 SDK 带的,保留高的,一般向后兼容;低于 SDK 带的,抬到 SDK 的水平,别为了迁就旧框架把 SDK 压下去。slf4j 只保留一个绑定,多绑定在启动时就会打印 StaticLoggerBinder 的警告,这个后面第 5 章还会具体说。
提示:清点完 jar,把
ls -1 esdk-obs/lib的输出存一份到部署文档里。下次换机器、换版本,直接 diff 这两份清单,比回忆 “上次是不是少拷了什么”可靠得多。
3. 三条安装路径:把 zip 变成工程认得出的依赖
zip 包本身不是依赖,它只是一堆 jar 的集合。要让它变成构建工具认的东西,无非三条路:手工拷 lib、装进本地仓库、推到内网私服。三条路我都走过,选哪条取决于你的项目是 war 包还是 Maven 工程,以及有没有第二个人跟你一起开发。
3.1 路径一:WEB-INF/lib 手工放置,老项目最直接
传统 war 包项目、单机部署、就一个人维护,手工拷最省事,五分钟搞定,还不用碰 Maven 配置。
# 主 jar 和 lib 下所有依赖一次性拷进去,注意通配符别漏了主 jar cp esdk-obs/esdk-obs-java-3.20.3.jar \ esdk-obs/lib/*.jar \ "$TOMCAT_HOME/webapps/app/WEB-INF/lib/" # 核对:目标目录里的 jar 数应该 = 主 jar 1 个 + lib 下数量 ls -1 "$TOMCAT_HOME/webapps/app/WEB-INF/lib/"*.jar | wc -l这里有三个细节。第一,别把整个解压目录塞进 WEB-INF/lib,那个目录里还有示例代码和文档,容器加载时虽然不报错,但打出来的 war 包会白白大出几兆,每次构建上传都慢。第二,jar 文件名不要改,有人喜欢把版本号去掉图清爽,结果半年后完全看不出线上跑的是哪个版本,出了问题连回滚都没法回。第三,容器自己的 lib 目录下可能已经有一个同名不同版本的 jar,这种双份 classpath 的情况,看启动日志看不出来,得用jar tf确认 jar 里到底有没有那个类。
这种装法的短板也很明确:依赖关系写在人脑里,换个人接手,或者要打第二个 war 包,就得再数一遍 jar。
3.2 路径二:install:install-file,Maven 工程的标准做法
Maven 工程就别手工拷了。离线环境拿不到远程仓库,可以把 jar 装进本机仓库,坐标自己定:
# 装主 jar,groupId 用团队约定的前缀,别和官方坐标混着用 mvn install:install-file \ -Dfile=esdk-obs/esdk-obs-java-3.20.3.jar \ -DgroupId=com.example.obs \ -DartifactId=esdk-obs-java \ -Dversion=3.20.3 \ -Dpackaging=jar # 第三方依赖同样要装,坐标照抄各自官方的,别自己发明 mvn install:install-file -Dfile=esdk-obs/lib/okhttp-*.jar \ -DgroupId=com.squareup.okhttp3 -DartifactId=okhttp -Dversion=<包内版本> -Dpackaging=jar mvn install:install-file -Dfile=esdk-obs/lib/jdom2-*.jar \ -DgroupId=org.jdom -DartifactId=jdom2 -Dversion=<包内版本> -Dpackaging=jar参数逐个说:-Dfile是 jar 的物理路径,支持相对路径但别用~;-DgroupId/-DartifactId/-Dversion三者合起来是这个 jar 在本机仓库里的唯一地址,装完能在~/.m2/repository/com/example/obs/esdk-obs-java/3.20.3/下看到;-Dpackaging=jar不写会默认按 jar 处理,但写上更清楚。<包内版本>这种占位符要换成ls esdk-obs/lib看到的真实版本号,别照抄我这里的写法。
这里有个很多人第一次会踩的坑:install:install-file 只装 jar 本身,不生成依赖关系。也就是说装完之后,你的 pom 里只写一行 esdk-obs-java,编译能过,运行到发网络请求那一步就 NoClassDefFoundError。解决办法就两个,要么把 okhttp、okio、jdom2 这些在 pom 里显式声明一遍,要么用-DpomFile=提供一个手写好的 pom,把依赖关系带进去。我一般选前者,虽然 pom 长几行,但谁都能一眼看出这个 SDK 到底依赖了什么。
pom 里的写法:
<dependency> <groupId>com.example.obs</groupId> <artifactId>esdk-obs-java</artifactId> <version>3.20.3</version> </dependency> <!-- 手工 install 的包不会带传递依赖,这几个必须自己补 --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version><!-- 与 lib 目录里的版本一致 --></version> </dependency>注意:
com.example.obs这类自定 groupId 一旦定下来就别改。以后如果切到官方仓库坐标,改 pom 是一行的事,但已经发出去的 jar 里 import 路径不变,很容易出现同一个类被两个坐标引入两份,那种冲突排查起来非常费劲。
3.3 路径三:deploy 到内网私服,团队协作才值得做
只要有第二个人跟你一起开发,前两条路都会开始难受:每个人的本机仓库都要装一遍,版本还可能不一致。这时候把 jar 推到内网私服,一次搞定。
# 推到私服的 releases 仓库,快照版本要换 snapshots 地址 mvn deploy:deploy-file \ -Dfile=esdk-obs/esdk-obs-java-3.20.3.jar \ -DgroupId=com.example.obs \ -DartifactId=esdk-obs-java \ -Dversion=3.20.3 \ -Dpackaging=jar \ -DrepositoryId=nexus-releases \ -Durl=http://nexus.internal/repository/maven-releases/-DrepositoryId要和 settings.xml 里<server><id>完全一致,这个是认证用的钥匙名,写错了就是 401。-Durl指向仓库地址,末尾斜杠别丢。settings.xml 里对应的片段是这样:
<servers> <server> <id>nexus-releases</id> <username>deployer</username> <password><!-- 放CI变量或本地加密,别明文提交 --></password> </server> </servers>第三方依赖也要一起推上去,顺序是先推 okhttp、okio、jdom2 这些底层包,再推 esdk-obs-java,否则别人拉下来还是缺依赖。
3.4 三条路径怎么选
| 维度 | 手工拷 lib | 本地 install | 内网私服 deploy |
|---|---|---|---|
| 上手成本 | 五分钟 | 半小时 | 半天,要协调仓库权限 |
| 适合项目 | war 包、单机、一人维护 | Maven 工程、离线开发 | 团队多人、多项目复用 |
| 依赖关系 | 靠文档和人脑 | 要自己补 pom | 一次配好,全团队受益 |
| 升级 3.20.x 小版本 | 手工替换 jar | 重跑 install | deploy 新版本号即可 |
| 出问题回滚 | 从备份目录拷回 | 改 version | 改 version |
我的习惯是:先用手工拷 lib 的方式在测试机跑通,确认这个 SDK 在当前 JDK 和框架下没问题,再花时间做本地 install 或者推私服。反过来先搞仓库、结果发现是 JDK 版本不对,前面的工夫白费。
4. 客户端初始化:new ObsClient 之前要填对的参数
依赖装好只是让代码能编译。真正决定这套东西在生产上能不能扛住流量的,是 ObsClient 初始化那几行。我见过太多项目把 AK/SK 写死在常量类里,超时全用默认值,然后在大文件上传时集体超时。
4.1 AK/SK 与 endpoint:写死是最贵的一课
// 从环境变量或配置中心取,别写在代码里 String ak = System.getenv("OBS_AK"); String sk = System.getenv("OBS_SK"); // endpoint 只到域名或 IP:端口,不带桶名,带协议头 String endpoint = "https://obs.internal.example.com:443"; ObsClient obsClient = new ObsClient(ak, sk, endpoint);endpoint 是出错率最高的一个参数,三个细节:一是不要带桶名,桶名是请求时单独传的,拼进 endpoint 会得到 “域名解析失败” 这种完全指错方向的报错;二是协议头要显式写,写obs.internal.example.com和写https://obs.internal.example.com是两回事,前者会走默认协议,端口也可能不是你想要的;三是端口显式写,尤其是内网做了端口映射的环境,443 映射到 8443 的情况很常见。
AK/SK 就别写代码里了。不是说教,是血泪经验:写死在代码里,一旦要轮换密钥,就得改代码、打包、走一遍发布流程;放在环境变量或配置中心,运维那边改完重启应用就生效。用System.getenv拿不到的时候记得判断 null,早失败比拿着空字符串去签名、然后收到一个 403 好查得多。
4.2 ObsConfiguration:连接数、超时、重试这三个必须动
用三参数构造函数,所有配置都是默认值。默认值能用,但不一定适合你的场景。我一般显式写一遍:
ObsConfiguration config = new ObsConfiguration(); config.setEndPoint(endpoint); // 并发不高的小应用,别开太大,连接池闲着也是占资源 config.setMaxConnections(100); // 建连超时,内网 10 秒足够,公网可以放到 15 config.setConnectionTimeout(10_000); // 读超时,大文件上传必须放大,否则传到一半就断 config.setSocketTimeout(120_000); // 重试次数,PUT 是幂等的可以重试,追加类接口要慎重 config.setMaxErrorRetry(3); ObsClient obsClient = new ObsClient(ak, sk, config);| 参数 | 我一般取值 | 什么时候调 |
|---|---|---|
| maxConnections | 50~100 | 高并发上传服务可到 200,但要看对端限流 |
| connectionTimeout | 10000 ms | 跨机房或公网链路放到 15000 |
| socketTimeout | 60000~120000 ms | 有几十兆以上文件时一定放大 |
| maxErrorRetry | 3 | 网络抖动多可到 5,注意接口幂等性 |
这几个值的单位都是毫秒,setSocketTimeout(60)写成本意 60 秒、实际 60 毫秒,是我在 code review 里见过不止一次的笔误。默认值具体是多少不用背,需要的时候打印一下 config 对象,或者直接看包里附带的源码,比记博客上的数字靠谱。
还有一个容易忽略的点:ObsClient 是线程安全的,一个应用一个实例,放在 Spring 容器里做单例。我见过在方法里每次请求 new 一个的写法,压测的时候连接池根本建不起来,全在反复握手,QPS 上不去还以为是服务端限流。
4.3 日志与资源关闭:slf4j 绑定和 close
SDK 内部用 slf4j 打日志,包名是com.obs.services。日志级别调到 DEBUG 能看到完整的请求 URL 和响应头,排查签名问题全靠它,但生产上别开,一个上传请求能刷几十行。
<!-- logback 里单独把这个包压到 INFO,排查时临时改 DEBUG --> <logger name="com.obs.services" level="INFO"/>如果你的工程用 log4j2,别让 SDK 自带的 log4j 1.x 绑定生效,把它从依赖里排掉,加上log4j-slf4j-impl之类的桥接包,全工程只留一个 slf4j 绑定。多个绑定共存时,启动日志会打印一行 “Class path contains multiple SLF4J bindings”,很多人扫一眼就过去了,等到要查线上问题发现日志缺一半才想起来。
资源关闭用 try-with-resources,3.x 的 ObsClient 实现了 Closeable:
// 短期任务这样写,长驻服务交给容器销毁时 close try (ObsClient client = new ObsClient(ak, sk, config)) { // 业务代码 }长驻服务里不用每次 close,但要在应用停止的钩子里关一次,否则连接池里的连接会挂在那儿等超时。
5. 避坑与排查:装完起不来的五类现场
下面这几条,是我在不同项目里真实碰到过的,按“现象 → 原因 → 解决”记,遇到类似的直接对号入座。
5.1 启动就 NoClassDefFoundError: okhttp3/OkHttpClient
现象:编译完全正常,本地 main 方法也跑通了,一部署到测试环境启动就抛 NoClassDefFoundError,堆栈指向 ObsClient 的构造函数。
原因:只把 esdk-obs-java-3.20.3.jar 拷进了 lib 目录,lib 下那堆第三方 jar 没拷。main 方法能跑通是因为 IDE 里配的 classpath 包含了整个解压目录,部署时那个目录根本没进去。
解决:回到第 2 章那条ls -1 esdk-obs/lib/*.jar | wc -l,和目标机器的 jar 数量对一遍。差几个就补几个,别只补报错里提到的那一个——报错是逐个暴露的,补完 okhttp 还会缺 okio,补完 okio 还会缺 jdom2。
5.2 403 SignatureDoesNotMatch 或 RequestTimeTooSkewed
现象:请求返回 403,错误码是 SignatureDoesNotMatch,或者 RequestTimeTooSkewed。
原因:前者八成是 SK 不对,多一个空格、少一个字符、复制时带上了引号,都会导致签名对不上;后者是客户端和服务器的时间差超过允许范围,虚拟机休眠恢复后系统时间漂了很常见。
解决:先把 AK/SK 打印长度出来看,正常是一串固定长度的字符,长度不对基本就是复制粘贴的问题。时间问题用date -u看 UTC 时间,和标准时间比一下,差超过几分钟就校时:
# 看当前时间和时区 date -u # 校时,chrony 或 ntpd 按机器上装的那个来 chronyc makestep 2>/dev/null || ntpdate -u pool.ntp.org顺手把应用启动时的时间也打一条日志,容器里时区没设对的情况很常见,TZ=Asia/Shanghai记得配上。
5.3 PKIX path building failed 或 SSLHandshakeException
现象:本地连测试环境正常,上了生产就 SSLHandshakeException,或者 PKIX path building failed: unable to find valid certification path。
原因:生产用的是内网自签证书,JDK 自带的 cacerts 里没有这条证书链。本地能通是因为你之前在 IDE 里随手加过信任,或者压根连的是 http。
解决:把服务端证书链导入 JDK 的信任库,别在代码里写 trustAll 那种东西,那是给自己埋雷。
# 从服务端导出证书链 openssl s_client -connect obs.internal.example.com:443 -showcerts </dev/null \ | openssl x509 -outform PEM > obs-chain.pem # 导入当前 JDK 的信任库,changeit 是默认口令,改过就换成实际口令 keytool -importcert -alias obs-internal -file obs-chain.pem \ -keystore "$JAVA_HOME/lib/security/cacerts" -storepass changeit导完记得重启应用。JDK 升级时 cacerts 会被覆盖,这条导入要写进部署脚本,别指望它一直在。
5.4 日志里一堆 SLF4J 绑定警告,或者干脆没日志
现象:启动日志出现 “Class path contains multiple SLF4J bindings”,或者反过来,一条业务日志都没有,只有 log4j:WARN No appenders could be found。
原因:前一种情况是工程里有 logback、SDK 带的 log4j 1.x 绑定都在,slf4j 挑了一个你不想用的;后一种情况是只有 slf4j-api 没有绑定,或者绑定有但配置文件不在 classpath 根目录。
解决:先定一个日志实现,全工程只留一个绑定。用 logback 就把 SDK 里的 log4j 绑定的坐标(或 jar)排除掉;用 log4j2 就加桥接包。配置文件位置也要确认,logback.xml 在 resources 根目录,Spring Boot 项目用 logback-spring.xml。
5.5 大文件上传 OOM 或传到一半 socketTimeout
现象:几十兆的文件上传,要么应用 OOM,要么传到中途报 SocketTimeoutException。
原因:把整个文件读成 byte[] 再上传,文件多大就占多大堆内存;同时 socketTimeout 用的是默认值,大文件传输时间超过阈值就断。
解决:能用流就别用 byte[],超大文件用分片上传;socketTimeout 按最大文件大小和带宽估一个值,宁大勿小。分片大小也不是越小越好,片太小请求次数多,反而更容易被限流。
6. 验证跑通:一次上传下载闭环加我的核对清单
装没装对,别靠“编译通过”判断,编译通过只证明 classpath 上有主 jar。我固定的做法是写一个二十来行的 main 方法,把「上传 → 下载 → 内容比对 → 列举 → 删除」一整圈跑完,任何一步失败都说明环境有问题。
// 端到端自检:任何一步抛异常,都不用往下看业务代码了 try (ObsClient client = new ObsClient(ak, sk, config)) { String bucket = "demo-bucket"; String key = "selftest/hello.txt"; byte[] body = "obs selftest".getBytes(StandardCharsets.UTF_8); // 1. 上传 client.putObject(bucket, key, new ByteArrayInputStream(body)); // 2. 下载并比对长度和内容,长度对不上多半是分片或编码问题 ObsObject obj = client.getObject(bucket, key); byte[] back = obj.getObjectContent().readAllBytes(); if (!Arrays.equals(body, back)) { throw new IllegalStateException("内容不一致,先查传输编码"); } // 3. 列举,能列出来说明 XML 解析这条链路是通的 client.listObjects(bucket, "selftest/"); // 4. 清理,避免自检残留文件把桶塞满 client.deleteObject(bucket, key); }这段代码里,第 2 步的内容比对是最有价值的,它同时验证了网络、签名、编码三件事;第 3 步专治“只拷了主 jar 没拷 jdom2”,很多环境上传能成、列举就炸,跑一次就能暴露。
| 检查项 | 期望结果 | 不对时先看哪 |
|---|---|---|
| 应用启动 | 无 NoClassDefFoundError | WEB-INF/lib 的 jar 数与第 2 章基准值 |
| 上传小文件 | 返回 200,ETag 有值 | AK/SK 是否含多余空格 |
| 下载比对 | 字节完全一致 | 传输编码、Content-Length |
| 列举 bucket | 能返回列表 | jdom2 依赖是否在 |
| 上传 50M 文件 | 不超时、不 OOM | socketTimeout、是否用了流式上传 |
| 应用停止 | 无连接泄漏告警 | 是否在销毁钩子里 close |
从那以后,我每次接一个新的对象存储 SDK,不管文档写得多清楚,都强制走一遍这套闭环自检,再把它写成脚本挂在 CI 的冒烟用例里。装环境这件事,能一次跑通是运气,能每次跑通才是本事。希望这套流程能帮你少走几趟我当年走过的弯路。
本文还有配套的精品资源,点击获取