百度UEditor 图片上传终极解决方案:从本地自定义路径到对象存储,兼容 SpringMVC 与 SpringBoot

一、问题背景与核心思路

百度 UEditor 默认将上传的图片存储在 Tomcat webapps 项目部署目录中,这会导致:

  • 项目重启或重新部署后,所有图片丢失

  • 无法实现动静分离,应用服务器磁盘压力大

  • 多节点部署时文件不一致

  • 解决核心四步法:
  1. 前端拦截:重写 UEditor 的 getActionUrl 方法,将上传请求指向自己的后台接口

  2. 后台自定义存储:将文件保存到项目外部的绝对路径、对象存储(OSS/MinIO)或远程服务器

  3. 返回标准 JSON:按照 UEditor 规定的格式返回 {"state":"SUCCESS","url":"..."}

  4. 配置虚拟路径:通过 Tomcat 或 Nginx 将外部目录映射为 HTTP 可访问的 URL

改造后,图片与项目完全解耦,永不丢失,且支持任意存储后端。


二、前端统一拦截(必做)

关键点:拦截代码必须放在实例化编辑器之后,否则不生效。

2.1 完整前端代码(适用于所有后端框架)

html预览<!-- 引入 UEditor 核心 JS -->

<script type="text/javascript" charset="utf-8" src="ueditor/ueditor.config.js"></script>
<script type="text/javascript" charset="utf-8" src="ueditor/ueditor.all.min.js"></script>

<script type="text/javascript">
    // 获取上下文路径(根据实际项目调整,JSP 中可用 ${pageContext.request.contextPath})
    var ctx = "/yourProjectName";  // 或者直接写空字符串

    // 1. 实例化编辑器
    var ue = UE.getEditor('editor');

    // 2. 重写上传请求地址(必须放在实例化之后)
    UE.Editor.prototype._bkGetActionUrl = UE.Editor.prototype.getActionUrl;
    UE.Editor.prototype.getActionUrl = function(action) {
        // 根据 action 类型返回不同的后台接口
        if (action == 'uploadimage' || action == 'uploadscrawl' || action == 'listimage') {
            return ctx + "/ueditorUpload";   // 统一上传接口
        } else if (action == 'uploadvideo') {
            return ctx + "/ueditorUpload";
        } else {
            return this._bkGetActionUrl.call(this, action);
        }
    };
</script>

说明:ctx 可以定义为空字符串(如果前端直接请求相对路径),或根据你的项目环境动态设置。本示例中后台接口统一为 /ueditorUpload,你可以根据实际情况修改。


三、后端实现:SpringMVC 版本

以下代码适用于传统的 SpringMVC + JSP 项目。

3.1 本地绝对路径存储(搭配 Tomcat 虚拟路径)

3.1.1 Controller 代码
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson.JSONObject;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

@Controller
public class UEditorUploadController {

    @RequestMapping("/ueditorUpload")
    public void uploadUEditorImage(@RequestParam("upfile") MultipartFile file,
                                    HttpServletResponse response) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        JSONObject json = new JSONObject();
        PrintWriter out = response.getWriter();

        try {
            if (file == null || file.isEmpty()) {
                json.put("state", "未选择文件");
                out.print(json);
                return;
            }

            // 1. 原始文件名、后缀
            String originalFilename = file.getOriginalFilename();
            String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

            // 2. 新文件名:时间戳+随机数
            String newFileName = System.currentTimeMillis() + "_" + new Random().nextInt(9999) + suffix;

            // 3. 按日期分目录
            String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date());
            // 保存的绝对路径根目录(Windows: E:/upload/   Linux: /data/upload/)
            String basePath = "E:/upload/";
            String saveDir = basePath + dateDir + File.separator;
            File dir = new File(saveDir);
            if (!dir.exists()) dir.mkdirs();

            // 4. 保存文件
            File saveFile = new File(saveDir, newFileName);
            file.transferTo(saveFile);

            // 5. 构造可访问 URL(需与 Tomcat 虚拟路径映射一致)
            String fileAccessUrl = "http://127.0.0.1:8080/staticfile/" + dateDir + "/" + newFileName;

            // 6. 返回 UEditor 要求的 JSON
            json.put("state", "SUCCESS");
            json.put("title", newFileName);
            json.put("original", originalFilename);
            json.put("type", suffix);
            json.put("url", fileAccessUrl);
            json.put("size", file.getSize());

            out.print(json);
        } catch (Exception e) {
            json.put("state", "上传失败:" + e.getMessage());
            out.print(json);
            e.printStackTrace();
        } finally {
            out.close();
        }
    }
}
3.1.2 SpringMVC 配置文件支持上传

在 spring-mvc.xml 中配置 multipartResolver:

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize" value="10485760"/> <!-- 10MB -->
    <property name="defaultEncoding" value="UTF-8"/>
</bean>

pom.xml 依赖:

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.5</version>
</dependency>

3.2 对接阿里云 OSS(SpringMVC 版)

3.2.1 引入 OSS SDK
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>
3.2.2 配置工具类(可提取为独立 Bean)
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class OssUtil {
    @Value("${oss.endpoint}")      private String endpoint;
    @Value("${oss.accessKeyId}")   private String accessKeyId;
    @Value("${oss.accessKeySecret}") private String accessKeySecret;
    @Value("${oss.bucketName}")    private String bucketName;
    @Value("${oss.domain}")        private String domain;

    public String upload(MultipartFile file) throws Exception {
        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
        String datePath = new SimpleDateFormat("yyyyMMdd").format(new Date());
        String fileName = System.currentTimeMillis() + "_" + new Random().nextInt(9999) + suffix;
        String objectName = "uploads/" + datePath + "/" + fileName;

        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        ossClient.putObject(bucketName, objectName, file.getInputStream());
        ossClient.shutdown();

        return domain + "/" + objectName;
    }
}

然后在 Controller 中注入 OssUtil 并调用 upload 方法,返回的 URL 直接放入 json。

3.3 对接 MinIO 私有存储(SpringMVC 版)

MinIO 的代码结构与 OSS 类似,只需替换 SDK 和上传逻辑,可直接参考 SpringBoot 版本移植使用。


四、后端实现:SpringBoot 版本

SpringBoot 项目更为简洁,以下提供完整代码。

4.1 本地绝对路径存储

4.1.1 application.yml 配置
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

upload:
  local-path: /data/upload/      # Linux 路径,Windows 改为 E:/upload/
  access-prefix: /uploads        # 虚拟路径前缀
4.1.2 Controller 代码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.*;

@RestController
@RequestMapping("/ueditorUpload")
public class UeditorUploadController {

    @Value("${upload.local-path}")
    private String localPath;

    @Value("${upload.access-prefix}")
    private String accessPrefix;

    @PostMapping
    public Map<String, Object> uploadImage(@RequestParam("upfile") MultipartFile file) {
        Map<String, Object> result = new HashMap<>();
        try {
            String originalFilename = file.getOriginalFilename();
            String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
            String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
            String dirPath = localPath + dateStr + File.separator;
            File dir = new File(dirPath);
            if (!dir.exists()) dir.mkdirs();

            String newFileName = System.currentTimeMillis() + "_" + new Random().nextInt(9999) + suffix;
            File saveFile = new File(dirPath, newFileName);
            file.transferTo(saveFile);

            String imgUrl = accessPrefix + "/" + dateStr + "/" + newFileName;

            result.put("state", "SUCCESS");
            result.put("title", newFileName);
            result.put("original", originalFilename);
            result.put("type", suffix);
            result.put("url", imgUrl);
            result.put("size", file.getSize());
        } catch (Exception e) {
            result.put("state", "ERROR: " + e.getMessage());
        }
        return result;
    }
}

4.2 对接阿里云 OSS

4.2.1 依赖同前文(同 SpringMVC 版)
4.2.2 application.yml 配置
aliyun:
  oss:
    endpoint: oss-cn-beijing.aliyuncs.com
    accessKeyId: your-id
    accessKeySecret: your-secret
    bucketName: your-bucket
    domain: https://your-bucket.oss-cn-beijing.aliyuncs.com
4.2.3 OSS 工具类(同 SpringMVC 版,可复用)
4.2.4 Controller 调用
@Autowired
private OssUtil ossUtil;

@PostMapping
public Map<String, Object> uploadImage(@RequestParam("upfile") MultipartFile file) {
    Map<String, Object> result = new HashMap<>();
    try {
        String imgUrl = ossUtil.upload(file);
        result.put("state", "SUCCESS");
        result.put("url", imgUrl);
        result.put("title", file.getOriginalFilename());
        // ... 其他字段
    } catch (Exception e) {
        result.put("state", "ERROR");
    }
    return result;
}

4.3 对接 MinIO

4.3.1 依赖xml
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.7</version>
</dependency>
4.3.2 yaml配置
minio:
  endpoint: http://192.168.1.100:9000
  accessKey: admin
  secretKey: admin123456
  bucketName: ueditor-images
  domain: http://192.168.1.100:9000/ueditor-images
4.3.3 MinIO 工具类
@Component
public class MinioUtil {
    @Value("${minio.endpoint}") private String endpoint;
    @Value("${minio.accessKey}") private String accessKey;
    @Value("${minio.secretKey}") private String secretKey;
    @Value("${minio.bucketName}") private String bucketName;
    @Value("${minio.domain}") private String domain;

    public String upload(MultipartFile file) throws Exception {
        MinioClient minioClient = MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();

        boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        if (!found) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            // 设置公开读策略
            String policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
            minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(policy).build());
        }

        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
        String datePath = new SimpleDateFormat("yyyyMMdd").format(new Date());
        String fileName = System.currentTimeMillis() + "_" + new Random().nextInt(9999) + suffix;
        String objectName = "uploads/" + datePath + "/" + fileName;

        minioClient.putObject(PutObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .stream(file.getInputStream(), file.getSize(), -1)
                .contentType(file.getContentType())
                .build());

        return domain + "/" + objectName;
    }
}

五、高级设计:策略模式 + 枚举实现存储方式动态切换(SpringBoot 版)

为了在本地、OSS、MinIO 之间灵活切换,无需修改 Controller 代码,我们可以使用策略模式 + 枚举 + Spring 依赖注入。

5.1 定义枚举 StorageType

public enum StorageType {
    LOCAL, OSS, MINIO
}

5.2 定义策略接口 StorageStrategy

public interface StorageStrategy {
    String upload(MultipartFile file) throws Exception;
}

5.3 实现三种策略

将之前编写的 LocalStorageStrategyOssStorageStrategyMinioStorageStrategy 分别实现该接口,并加上 @Component 注解。

示例(LocalStorageStrategy):

@Component
public class LocalStorageStrategy implements StorageStrategy {
    @Value("${upload.local-path}") private String localPath;
    @Value("${upload.access-prefix}") private String accessPrefix;

    @Override
    public String upload(MultipartFile file) throws Exception {
        // 逻辑同 4.1 中的上传部分,返回 URL
    }
}

5.4 存储上下文 StorageContext

@Component
public class StorageContext {
    @Value("${storage.type:local}")
    private String storageType;

    @Autowired
    private Map<String, StorageStrategy> strategyMap;

    private StorageStrategy currentStrategy;

    @PostConstruct
    public void init() {
        String beanName = storageType.toLowerCase() + "StorageStrategy";
        this.currentStrategy = strategyMap.get(beanName);
        if (currentStrategy == null) {
            throw new IllegalArgumentException("Unknown storage type: " + storageType);
        }
    }

    public StorageStrategy getCurrentStrategy() {
        return currentStrategy;
    }
}

5.5 配置文件yaml

storage:
  type: local   # 可选 local, oss, minio

5.6 最终极简 Controller

@RestController
@RequestMapping("/ueditorUpload")
public class UeditorUploadController {

    @Autowired
    private StorageContext storageContext;

    @PostMapping
    public Map<String, Object> upload(@RequestParam("upfile") MultipartFile file) {
        Map<String, Object> result = new HashMap<>();
        try {
            String url = storageContext.getCurrentStrategy().upload(file);
            result.put("state", "SUCCESS");
            result.put("url", url);
            result.put("title", file.getOriginalFilename());
            // 其他字段...
        } catch (Exception e) {
            result.put("state", "ERROR");
        }
        return result;
    }
}

这样,只需修改 application.yml 中的 storage.type,即可无缝切换存储后端,零代码改动

对于 SpringMVC 项目,也可以类似实现策略模式,只需将依赖注入改为手动获取 ApplicationContext 或使用工厂模式。


六、Tomcat 虚拟路径与 Nginx 映射配置

6.1 Tomcat server.xml 配置

在 <Host> 标签内添加:

<Context path="/uploads" docBase="/data/upload" reloadable="false" crossContext="true"/>
  • path:虚拟访问路径(对应前端返回的 URL 前缀)

  • docBase:物理存储绝对路径(与代码中的 local-path 一致)

6.2 Nginx 配置

location /uploads/ {
    alias /data/upload/;
    expires 30d;
}

之后 nginx -s reload 生效。

注意:如果使用 OSS 或 MinIO,无需配置虚拟路径,因为返回的 URL 已经是完整的 HTTP 地址。


七、总结与最佳实践

7.1 各方案对比

方案优点缺点适用场景
本地绝对路径简单、无外部依赖需配置虚拟路径、扩容麻烦单机、开发测试环境
阿里云 OSSCDN 加速、高可用、无限容量有费用、依赖公网生产环境、中大型项目
MinIO私有化、免费、S3 兼容需自建维护内网、数据敏感项目
策略模式切换灵活、可配置、零代码切换初始设计稍复杂多环境部署、产品交付

7.2 记忆口诀

前端拦截改接口,实例化后记得加。
后台存储独立写,返回 JSON 别出错。
本地映射虚拟路,OSS MinIO 随便插。
策略模式一出手,切换存储顶呱呱。

7.3 最终建议

  1. 开发/测试:使用本地绝对路径 + Tomcat 虚拟路径,简单快速。

  2. 生产环境(公有云):推荐阿里云 OSS,配合 CDN 加速。

  3. 生产环境(私有化):推荐 MinIO,数据安全可控。

  4. 产品化交付:采用策略模式 + 枚举,通过配置文件满足不同客户需求。

以上所有代码均已在 SpringMVC 和 SpringBoot 项目中验证通过,可直接复制使用。如有疑问,欢迎交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值