
一、云服务的定义云 一堆联网的高性能电脑 可以随时调用的资源。云服务的核心是“把这些服务器的资源算力、存储、网络等拆分成服务提供给用户。”也就是说你不需要自己买机器只要通过互联网调用这些云里的资源。你看到中间的Alibaba阿里云Huawei华为云Baidu百度智能云网易云网易云服务这些都是提供云服务的公司。它们都有自己的数据中心里面放了成千上万台服务器。然后把计算力、存储空间、短信接口、AI模型等资源封装成“服务”供开发者使用。二、阿里云OSS图片存储用什么如果你要存图片比如网站头像、商品图就会用“云存储Cloud Storage”这一类服务。不同公司叫法不同阿里云 → OSSObject Storage Service腾讯云 → COS华为云 → OBSAWS → S3图片上传到云端后会生成一个网址链接https://my-bucket.oss-cn-hangzhou.aliyuncs.com/pic001.jpg别人访问这个链接就能看到图片。三、用第三方服务的通用思路3-1、什么是“第三方服务”第三方服务Third-party service 别人提供的、你可以通过网络去调用的功能或资源。比如阿里云OSS → 图片/文件存储微信API → 发消息支付宝API → 支付OpenAI API → 调用ChatGPTGoogle Maps API → 获取地图和定位这些都不是你自己开发的而是别人已经开发好、封装好的功能你只需要“调用”。3-2、通用的使用思路五步法可以记住这个口诀“申请→ 连接 → 调用 → 接收 → 封装”① 申请Apply先到第三方平台注册账号开通对应服务。比如要用阿里云OSS你得先注册阿里云账号开通OSS服务创建一个“Bucket”相当于文件夹是一个存储空间拿到 AccessKeyId 和 AccessKeySecret这些是“身份凭证”就像钥匙证明你是谁、你能做什么。当你在阿里云控制台或 RAM 控制台创建 AccessKey 时会看到两部分AccessKey ID永久可见可查询AccessKey Secret只显示一次这时系统会提示“请妥善保存 AccessKey Secret离开此页面后将无法再次查看。”你应该立即保存到安全地方例如本地加密文件、密码管理器等AccessKey 默认没有时间限制阿里云官方说明AccessKey 没有默认有效期只要你不手动禁用或删除它就一直有效。也就是说创建后可以长期使用不会自动到期不会因为“时间太久”而失效。② 连接Connect接下来要让你的程序能连上这个服务。一般有两种方式方式说明SDK官方提供的开发包比如aliyun-oss-sdk、openai等API直接用 HTTP 请求访问通常是 RESTful 接口连接时通常需要配置endpoint: https://oss-cn-hangzhou.aliyuncs.com accessKeyId: 你的AccessKeyId accessKeySecret: 你的AccessKeySecret注意因为要调用SDK接口就需要maven安装依赖了直接在阿里云的官网找依赖的gav即可。③ 调用Call连接上以后就可以用服务的接口。例如调用上传图片的接口ossClient.put_object(my-bucket, photo.jpg, file)或者HTTP接口POST https://api.openai.com/v1/chat/completions Authorization: Bearer your_api_key这些接口通常都有固定格式URL MethodGET/POST 参数 认证头具体的接口调用的参数返回示例均可从官网获取④ 接收Receive第三方服务会返回一个结果通常是JSON格式。例如{ status: success, url: https://my-bucket.oss-cn-hangzhou.aliyuncs.com/photo.jpg }你只要解析返回值就能拿到你想要的信息比如图片URL、生成文本等。⑤ 封装Wrap最后一步是把这个结果整合进自己的系统里。比如用户上传头像 → 你的系统调用OSS → 得到URL →存数据库。用户输入文字 → 你的系统调用ChatGPT API → 返回生成结果。也就是说你不是“直接用”而是把它作为你系统的一个“模块”。3-3、思维结构总结图你的程序 ↓ [SDK / API 调用层] ↓ 网络请求带认证信息 ↓ 第三方服务云 / 平台 ↓ 返回数据(JSON) ↓ 你的系统处理显示3-4、常见第三方服务类别类型举例常见用途云计算 / 存储阿里云、腾讯云、AWS部署网站、存图片AI 服务OpenAI、百度千帆、阿里PAI文本生成、语音识别支付服务支付宝、Stripe、PayPal付款、账单地图服务高德、Google Maps定位、导航通讯服务Twilio、阿里云短信发短信、推送通知无论哪种服务思路都一样注册 → 获取Key → SDK/API连接 → 调用接口 → 用返回结果。四、上传文件SDK代码示例4-1、添加依赖4-2、使用官方SDK改造为Utils工具1、定义一个配置类OssConfigurationpackage org.jeecg.config.oss; import org.jeecg.common.util.oss.OssBootUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 云存储 配置 * author: jeecg-boot */ Configuration // 只有当配置文件里存在 jeecg.oss.endpoint 时这个配置类才会生效。 ConditionalOnProperty(prefix jeecg.oss, name endpoint) public class OssConfiguration { Value(${jeecg.oss.endpoint}) private String endpoint; Value(${jeecg.oss.accessKey}) private String accessKeyId; Value(${jeecg.oss.secretKey}) private String accessKeySecret; Value(${jeecg.oss.bucketName}) private String bucketName; Value(${jeecg.oss.staticDomain:}) private String staticDomain; Bean public void initOssBootConfiguration() { OssBootUtil.setEndPoint(endpoint); OssBootUtil.setAccessKeyId(accessKeyId); OssBootUtil.setAccessKeySecret(accessKeySecret); OssBootUtil.setBucketName(bucketName); OssBootUtil.setStaticDomain(staticDomain); } }这个类的职责是从配置文件里读取阿里云 OSS 的参数然后统一注入到工具类里。也就是endpoint、accessKey、secretKey、bucketName、staticDomain这些值本来散落在代码中现在用一个配置类集中管理、解耦代码与配置。1为什么要用ConfigurationBeanSpring Boot 的机制是启动时扫描带Configuration的类调用类中Bean方法把结果注册进 Spring 容器。这意味着你在别处可以直接使用这些配置通过Autowired或者像这里一样初始化静态工具类。这行代码保证了Spring 一启动OssBootUtil 的配置就准备好了。其他类可以直接调用OssBootUtil.upload()而不需要再手动传参。2为什么加ConditionalOnPropertyConditionalOnProperty(prefix jeecg.oss, name endpoint)意思是只有当配置文件里存在jeecg.oss.endpoint时这个配置类才会生效。好处是如果某个环境没有配置 OSS就不加载这段逻辑防止项目启动时报错因为没有配置项方便本地开发或测试环境不接 OSS。2、配置application.yml这样项目切换环境时只改配置文件不动任何 Java 代码。这就是 “配置与逻辑分离”。3、编写上传文件工具类OssBootUtil.java/** * 静态工具类所有属性和方法都是 static 静态的 * 设计思路是 任何地方都能直接调用 OssBootUtil.upload(...)不需要注入 * * Description: 阿里云 oss 上传工具类(高依赖版) * Date: 2019/5/10 * author: jeecg-boot */ Slf4j public class OssBootUtil { private static String endPoint; private static String accessKeyId; private static String accessKeySecret; private static String bucketName; private static String staticDomain; public static void setEndPoint(String endPoint) { OssBootUtil.endPoint endPoint; } public static void setAccessKeyId(String accessKeyId) { OssBootUtil.accessKeyId accessKeyId; } public static void setAccessKeySecret(String accessKeySecret) { OssBootUtil.accessKeySecret accessKeySecret; } public static void setBucketName(String bucketName) { OssBootUtil.bucketName bucketName; } public static void setStaticDomain(String staticDomain) { OssBootUtil.staticDomain staticDomain; } public static String getStaticDomain() { return staticDomain; } public static String getEndPoint() { return endPoint; } public static String getAccessKeyId() { return accessKeyId; } public static String getAccessKeySecret() { return accessKeySecret; } public static String getBucketName() { return bucketName; } public static OSSClient getOssClient() { return ossClient; } /** * oss 工具客户端 */ private static OSSClient ossClient null; /** * 上传文件至阿里云 OSS * 文件上传成功,返回文件完整访问路径 * 文件上传失败,返回 null * * param file 待上传文件 * param fileDir 文件保存目录 * return oss 中的相对文件路径 */ public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception { //update-begin-author:liusq date:20210809 for: 过滤上传文件类型 SsrfFileTypeFilter.checkUploadFileType(file); //update-end-author:liusq date:20210809 for: 过滤上传文件类型 String filePath null; initOss(endPoint, accessKeyId, accessKeySecret); StringBuilder fileUrl new StringBuilder(); String newBucket bucketName; if(oConvertUtils.isNotEmpty(customBucket)){ newBucket customBucket; } try { //判断桶是否存在,不存在则创建桶 if(!ossClient.doesBucketExist(newBucket)){ ossClient.createBucket(newBucket); } // 获取文件名 String orgName file.getOriginalFilename(); if( orgName){ orgNamefile.getName(); } orgName CommonUtils.getFileName(orgName); String fileName orgName.indexOf(.)-1 ?orgName _ System.currentTimeMillis() :orgName.substring(0, orgName.lastIndexOf(.)) _ System.currentTimeMillis() orgName.substring(orgName.lastIndexOf(.)); if (!fileDir.endsWith(SymbolConstant.SINGLE_SLASH)) { fileDir fileDir.concat(SymbolConstant.SINGLE_SLASH); } //update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符防止攻击 fileDirStrAttackFilter.filter(fileDir); //update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符防止攻击 fileUrl fileUrl.append(fileDir fileName); if (oConvertUtils.isNotEmpty(staticDomain) staticDomain.toLowerCase().startsWith(CommonConstant.STR_HTTP)) { filePath staticDomain SymbolConstant.SINGLE_SLASH fileUrl; } else { filePath https:// newBucket . endPoint SymbolConstant.SINGLE_SLASH fileUrl; } PutObjectResult result ossClient.putObject(newBucket, fileUrl.toString(), file.getInputStream()); // 设置权限(公开读) // ossClient.setBucketAcl(newBucket, CannedAccessControlList.PublicRead); if (result ! null) { log.info(------OSS文件上传成功------ fileUrl); } } catch (IOException e) { log.error(e.getMessage(),e); return null; }catch (Exception e) { log.error(e.getMessage(),e); return null; } return filePath; } }4、总结关系图这个结构分层其实是Spring Boot 的“配置分离”思想的体现。五、JeecgBoot文件上传结构讲解不可以直接把配置的值写在application.yml然后在OssBootUtil.java中用Value(${jeecg.oss.endpoint})获取不也能实现Spring Boot 的“配置分离”思想吗这种方式——在OssBootUtil里直接用Value(${jeecg.oss.endpoint})注入配置——确实可以工作也确实是“配置分离”的一种形式。但为什么大型项目例如 JeecgBoot还要再包一层OssConfiguration呢 原因在于“Spring 管理范围、静态工具类限制、解耦与可维护性”。5-1、先确认你说的方法是可行的如果你把OssBootUtil写成普通的 Spring Bean即不是全静态的工具类确实可以Component public class OssBootUtil { Value(${jeecg.oss.endpoint}) private String endpoint; Value(${jeecg.oss.accessKey}) private String accessKeyId; Value(${jeecg.oss.secretKey}) private String accessKeySecret; public String upload(...) { ... } }然后在别的地方Autowired进来使用Autowired private OssBootUtil ossBootUtil;这完全符合 Spring Boot “配置分离”的思路。但是现在这个类就必须是由 Spring 管理的 Bean。5-2、问题来了原始版本里它不是 Bean而是静态工具类在 JeecgBoot 的结构中public class OssBootUtil { private static String endPoint; private static OSSClient ossClient; ... public static String upload(MultipartFile file, String fileDir, String customBucket) { ... } }特点所有属性和方法都是static 静态的设计思路是任何地方都能直接调用OssBootUtil.upload(...)不需要注入它本身不是Spring Bean所以Spring 无法对它做依赖注入Value、Autowired都不生效。 这就是为什么必须通过一个配置类来在系统启动时手动注入这些静态字段。也就是说OssConfiguration就是专门给静态工具类“喂配置”的桥梁。5-3、为什么不直接改成非静态 Bean这是一个“设计取舍”方式优点缺点静态工具类 配置类注入用法简单任何地方都能直接调用OssBootUtil.upload()兼容旧代码不符合 IoC 思想难单元测试不易扩展非静态 Bean Value 注入完全符合 Spring IoC可 mock可切换实现所有使用处都必须Autowired进来老代码需要改动JeecgBoot 是个老牌框架它保留了静态工具类的写法因为很多地方可能直接用String url OssBootUtil.upload(file, images/, null);这样就不需要依赖注入。但为了仍然能从配置文件里读到配置就引入了OssConfiguration来在 Spring 启动时初始化静态变量。5-4、总结对比设计方式是否是 BeanValue 可用吗调用方式特点你说的方式Value 直接注入 OssBootUtil✅ 是 Bean✅ 可用Autowired调用完全 Spring 化Jeecg 方式OssBootUtil 静态 OssConfiguration 初始化❌ 不是 Bean❌ 不可用直接OssBootUtil.upload()全局静态工具类简单但耦合高5-5、什么时候该用哪种方式场景推荐写法个人或新项目从零写✅ 用 Bean Value 注入现代、优雅、可测试框架级工具类、被大量静态调用✅ 保留静态工具类但用配置类初始化兼容旧代码需要动态切换配置或多云支持OSS / COS / S3✅ 用 Bean Spring Profile / Factory 模式更灵活一句话总结直接在工具类用 Value 是可以的但仅当它是一个 Spring Bean。JeecgBoot 之所以额外写了OssConfiguration是因为它要在 Spring 启动时把配置“注入到静态工具类”这样既能保持旧有的静态调用方式又能享受 Spring Boot 的配置管理。六、Bean的用法Bean是 Spring 框架里最核心的注解之一理解它就能彻底搞清楚Spring 的 Bean 是怎么创建的、怎么被管理的、怎么被注入的。6-1、Bean是干什么的一句话Bean是用来告诉 Spring“这个方法的返回值要交给我Spring 容器管理。”也就是说这个方法返回的对象会被 Spring 注册成一个Bean在容器启动时创建、放进 Spring 容器中之后你就能在项目任何地方用Autowired拿到这个 Bean。6-2、最基本的使用方式Configuration // 声明一个配置类相当于一个工厂 public class AppConfig { Bean public UserService userService() { return new UserServiceImpl(); } }这段代码会在启动时执行Spring 扫描到Configuration调用其中带Bean的方法把返回的UserServiceImpl实例放进容器你之后就可以用Autowired private UserService userService;来自动注入这个对象。类比理解Bean “告诉 Spring我要手动造一个对象请你帮我管起来。”6-3、它与Component的区别比较项BeanComponent用在方法上类上谁来创建对象你自己 newSpring 自己通过反射创建适合场景第三方类、需要自定义初始化逻辑的 Bean你自己写的普通业务类是否需要Configuration通常需要不需要 例子// 你想用第三方类不方便加Component Configuration public class WebConfig { Bean public RestTemplate restTemplate() { return new RestTemplate(); // 手动创建 } }这样 Spring 才能帮你管理RestTemplate对象否则它不会被自动注入。6-4、Bean的高级用法① 指定 Bean 名称默认 Bean 名称是方法名Bean(myRestTemplate) public RestTemplate restTemplate() { ... }之后可以通过名称注入Autowired Qualifier(myRestTemplate) private RestTemplate restTemplate;② 设置作用域ScopeBean Scope(prototype) // 每次注入都 new 一个 public UserService userService() { return new UserServiceImpl(); }常见取值singleton默认全局只有一个对象prototype每次注入都创建新实例还有 Web 环境专用的request、session等。③ 初始化与销毁方法Bean(initMethod init, destroyMethod close) public DataSource dataSource() { return new HikariDataSource(); }当 Spring 启动时调用init()关闭时调用close()。执行顺序是dataSource()→init()→ 程序运行中使用 Bean→close()【注意】调用的是hikariDataSource.init();hikariDataSource.close();④ 条件加载配合ConditionalOnProperty你可以让某个 Bean 只在配置文件中某个条件满足时才创建Bean ConditionalOnProperty(name app.cache.enabled, havingValue true) public CacheService cacheService() { return new RedisCacheService(); }如果配置文件里app.cache.enabledtrueSpring 才会加载这个 Bean。6-5、你提到的例子中的Bean回到你之前的例子Bean public void initOssBootConfiguration() { OssBootUtil.setEndPoint(endpoint); OssBootUtil.setAccessKeyId(accessKeyId); ... }这里的意思是Spring 启动时执行这个方法这个方法虽然返回void但它完成了“初始化工作”它不是要创建 Bean而是利用Bean的“初始化时机”实际上可以换成PostConstruct效果更清晰但写法略不同。所以这里的Bean用法略“取巧”利用 Spring 的加载机制在容器初始化阶段执行逻辑。七、PostConstruct讲解PostConstruct是 Spring更准确地说是 JSR-250 规范中另一种实现Bean 初始化逻辑的方式作用与Bean(initMethod...)类似但写法更简洁。7-1、PostConstruct 的作用PostConstruct用在Bean 的实例方法上表示“当 Spring 把这个 Bean 创建并注入完依赖后立刻执行这个方法。”所以它相当于替代Bean(initMethod init)或者替代实现InitializingBean.afterPropertiesSet()7-2、用法示例原来的写法是Bean(initMethod init, destroyMethod close) public DataSource dataSource() { return new HikariDataSource(); }换成PostConstruct的写法需要把初始化逻辑放在Bean 对象内部。也就是在类中定义一个方法并用PostConstruct标注Component public class MyDataSource extends HikariDataSource { PostConstruct public void init() { // 这里写初始化逻辑比如连接池准备、配置加载 System.out.println(⭐ 数据源初始化完成); } PreDestroy public void close() { // 这里写销毁逻辑比如关闭连接池 System.out.println( 数据源已关闭); } }然后在配置类中只需简单地Bean public DataSource dataSource() { return new MyDataSource(); }这样Spring 创建MyDataSource对象自动调用PostConstruct标注的方法容器关闭时自动调用PreDestroy。7-3、执行顺序结合前面对比阶段旧写法新写法Bean 创建调用dataSource()调用dataSource()初始化调用init()来自Bean(initMethodinit)调用PostConstruct方法销毁调用close()来自Bean(destroyMethodclose)调用PreDestroy方法最终执行顺序一致dataSource() → PostConstruct(init) → 程序运行中→ PreDestroy(close)7-4、两种方式对比总结比较项Bean(initMethod/destroyMethod)PostConstruct / PreDestroy写法位置在配置类外部定义在 Bean 类内部可读性逻辑集中在配置层逻辑集中在类内部适合场景第三方类源码不可改自己定义的类销毁方式destroyMethod 指定PreDestroy自动执行生命周期Bean 初始化/销毁阶段同上7-5、如果你用的是第三方类怎么办比如HikariDataSource是外部库的类你没法在里面加PostConstruct。这时仍然得用Bean(initMethodinit, destroyMethodclose)。所以自己写的类 → 用PostConstruct/PreDestroy更自然别人写的类比如数据库连接池 → 用Bean(initMethod, destroyMethod)。一句话总结PostConstruct是写在 Bean 类内部的初始化注解Spring创建 Bean 后会自动执行它对应Bean(initMethod...)的作用同理销毁阶段可用PreDestroy代替destroyMethod...。7-6、initOssBootConfiguration换成PostConstruct的写法package org.jeecg.config.oss; import jakarta.annotation.PostConstruct; import org.jeecg.common.util.oss.OssBootUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; /** * 云存储 配置 * author: jeecg-boot */ Configuration ConditionalOnProperty(prefix jeecg.oss, name endpoint) public class OssConfiguration { Value(${jeecg.oss.endpoint}) private String endpoint; Value(${jeecg.oss.accessKey}) private String accessKeyId; Value(${jeecg.oss.secretKey}) private String accessKeySecret; Value(${jeecg.oss.bucketName}) private String bucketName; Value(${jeecg.oss.staticDomain:}) private String staticDomain; /** * 使用 PostConstruct 在 Spring 完成属性注入后执行初始化逻辑 */ PostConstruct public void initOssBootConfiguration() { OssBootUtil.setEndPoint(endpoint); OssBootUtil.setAccessKeyId(accessKeyId); OssBootUtil.setAccessKeySecret(accessKeySecret); OssBootUtil.setBucketName(bucketName); OssBootUtil.setStaticDomain(staticDomain); } }执行顺序对比步骤Bean 写法PostConstruct 写法1Spring 扫描配置类Spring 扫描配置类2执行Bean方法立即执行先实例化配置类再注入属性Value3——执行PostConstruct方法属性已可用4结束结束在你的场景下两者的功能完全等价但Bean用来“产生 Bean”PostConstruct用来“初始化类中已有 Bean 或执行启动逻辑”。所以用PostConstruct更语义化也不会在 Spring 容器里产生一个“多余的 Bean”void的 Bean 方法其实也会注册一个空 Bean 定义。7-7、它在生命周期中的执行顺序完整顺序常见三件套构造方法new依赖注入给Autowired、Value这些字段赋值PostConstruct← 你关心的就是这一步可选InitializingBean.afterPropertiesSet()可选Bean(initMethod...)—— 应用运行 ——PreDestroy容器关闭时可选DisposableBean.destroy()可选Bean(destroyMethod...)小结PostConstruct比initMethod更早执行。