若依框架-添加OSS

若依框架-添加OSS

前言

若依框架是一款开源且十分优秀的国产开源软件,功能十分强大而且免费开源可商用

RuoYi 若依官方网站 |后台管理系统|权限管理系统|快速开发框架|企业管理系统|开源框架|微服务框架|前后端分离框架|开源后台系统|RuoYi|RuoYi-Vue|RuoYi-Cloud|RuoYi框架|RuoYi开源|RuoYi视频|若依视频|RuoYi开发文档|若依开发文档|Java开源框架|Java|SpringBoot|SrpingBoot2.0|SrpingCloud|Alibaba|MyBatis|Shiro|OAuth2.0|Thymeleaf|BootStrap|Vue|Element-UI||www.ruoyi.vip (twom.top)

RuoYi: 基于SpringBoot的权限管理系统 易读易懂、界面简洁美观。 核心技术采用Spring、MyBatis、Shiro没有任何其它重度依赖。直接运行即可用 (gitee.com)

因为RuoYi里面的文件是存储在本地的,这云原生时代,业务需求是将文件资源存储在OSS对象存储服务器的,所以就依照这若依的源码更改了一下,希望对你有用!

环境

OSS:docker 部署的minio,并提前将例子中使用的bucket设为了public minio/minio:RELEASE.2022-10-15T19-57-03Z

若依:前后端分离单体版本,Vue2+Element UI + 后端单体版

在线快速搭建: Fastbuild Factory 快速项目搭建平台

分析

在若依框架中使用文件上传下载的地方主要有:用户头像的上传admin模块中common包下的CommonController,后者是框架给开发者预留的资源上传、下载的接口,我并没有在前端找到对应的使用的地方。

由用户头像上传得到文件上传功能实现的位置

用户头像上传的后端地址:


/**
     * 头像上传
     */
@Log(title = "用户头像", businessType = BusinessType.UPDATE)
@PostMapping("/avatar")
public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
{
    if (!file.isEmpty())
    {
        // 获取当前访问的用户
        LoginUser loginUser = getLoginUser();
        
        // 获取我们在application.yml配置的地址
        String baseDir = CloudstudyConfig.getProfile();
        // 需要修改的地方 关键
        String avatar = FileUploadUtils.upload(baseDir, file, MimeTypeUtils.IMAGE_EXTENSION);
        // 将上传成功的头像路径 更新进数据库
        if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
        {
            AjaxResult ajax = AjaxResult.success();
            ajax.put("imgUrl", avatar);
            // 更新redis服务器中 缓存的用户头像
            loginUser.getUser().setAvatar(avatar);
            tokenService.setLoginUser(loginUser);
            // 返回给前端通用 响应对象
            return ajax;
        }
    }
    return AjaxResult.error("上传图片异常,请联系管理员");
}
  • 所以FileUploadUtils.upload(),是若依上传用户头像的关键,且admin模块中common包下的CommonController中关于文件的通用上传也是调用的FileUploadUtils.upload()
@PostMapping("/upload")
@ApiOperation("通用上传请求(单个)")
public AjaxResult uploadFile(@RequestPart MultipartFile file) throws Exception
{
    try
    {
        // 上传文件路径
        String filePath = CloudstudyConfig.getUploadPath();
        // 上传并返回新文件名称
        String fileName = FileUploadUtils.upload(filePath, file);
        AjaxResult ajax = AjaxResult.success();
        ajax.put("url", fileName);
        ajax.put("fileName", fileName);
        ajax.put("newFileName", FileUtils.getName(fileName));
        ajax.put("originalFilename", file.getOriginalFilename());
        return ajax;
    }
    catch (Exception e)
    {
        return AjaxResult.error(e.getMessage());
    }
}

FileUploadUtils.upload()

/**
     * 文件上传
     * TODO 文件上传逻辑
     * @param baseDir 相对应用的基目录
     * @param file 上传的文件
     * @param allowedExtension 上传文件类型
     * @return 返回上传成功的文件名
     * @throws FileSizeLimitExceededException 如果超出最大大小
     * @throws FileNameLengthLimitExceededException 文件名太长
     * @throws IOException 比如读写文件出错时
     * @throws InvalidExtensionException 文件校验异常
     */
public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
    throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
InvalidExtensionException
{
    // 是否为null
    int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
    // 文件名是否超出了指定长度
    if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
    {
        throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
    }
    // 文件大小校验
    assertAllowed(file, allowedExtension);

    // 编码文件名
    String fileName = extractFilename(file);

    // 返回绝对路径的文件
    String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
    // 将文件复制到服务器本地文件系统
    // TODO 待替换为oss服务器的内容
    file.transferTo(Paths.get(absPath));
    // 返回文件路径
    return getPathFileName(baseDir, fileName);
}

替换自己的OSS

新建oss模块并添加到common模块,

<dependencies>
   
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-context-supportartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-webartifactId>
    dependency>

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-validationartifactId>
    dependency>

    
    <dependency>
        <groupId>org.yamlgroupId>
        <artifactId>snakeyamlartifactId>
    dependency>

    <dependency>
        <groupId>io.miniogroupId>
        <artifactId>minioartifactId>
        <exclusions>
            <exclusion>
                <groupId>com.squareup.okhttp3groupId>
                <artifactId>okhttpartifactId>
            exclusion>
        exclusions>
    dependency>
    <dependency>
        <groupId>com.squareup.okhttp3groupId>
        <artifactId>okhttpartifactId>
    dependency>

    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
    dependency>

    <dependency>
        <groupId>javax.servletgroupId>
        <artifactId>javax.servlet-apiartifactId>
    dependency>
dependencies>

将我们自己的OSS模块添加进common模块。

创建配置minio连接信息的配置类,记得在admin模块中为自己的配置类添加信息,这里就不展示了

@Data
@Component
// 读取 admin模块中application.yml配置的信息
@ConfigurationProperties(prefix = "oss.minio")
public class MinioConfig {
    // 连接地址+端口号
    private String url;
    // 用户名
    private String access;
    // 密码
    private String secret;
    // 桶名
    private String bucket;
}

定义一个简单的连接池

@Data
@Component
@ConfigurationProperties(prefix = "oss.pool")
public class MinioPool {
    // 线程安全队列
    private ConcurrentLinkedQueue<MinioClient> minioPool = new ConcurrentLinkedQueue<>();

    @Autowired
    private MinioConfig minioConfig;

    // 核心客户端数量
    private Integer coreSize = 10;
    // 队列最大的
    private Integer maxSize = 20;

    // 连接池的大小
    private int poolSize = 0;

    /**
     * 返回一个minio客户端
     * @return 配置好的客户端对象
     */
    public synchronized MinioClient getMinioClient() {
        // 判断队列是否为null
        if (poolSize == 0){
            // 为空,初始化连接池
            for (int i =0 ;i < coreSize;i++){
                MinioClient client = MinioClient.builder()
                        .httpClient(new OkHttpClient())
                        .endpoint(minioConfig.getUrl())
                        .credentials(minioConfig.getAccess(),minioConfig.getSecret())
                        .build();
                minioPool.add(client);
                // 对应的连接池数量+1
                poolSize++;
            }
        }
        // 返回连接的客户端
        MinioClient client = null;
        // 连接池不为null
        if (!minioPool.isEmpty()){
            // 连接队列不为null ,直接返回
            return minioPool.poll();
        }
        // 连接池队列为null了
        if(poolSize >= coreSize && poolSize <= maxSize){
            // 在队列所能容纳的范围内
            client = MinioClient.builder()
                    .httpClient(new OkHttpClient())
                    .endpoint(minioConfig.getUrl())
                    .credentials(minioConfig.getAccess(),minioConfig.getSecret())
                    .build();
            poolSize++;
        }else {
            // 如果已经超过了队列所能容纳的最多的客户端,抛出异常
            throw new RuntimeException("系统需要的minio客户端数量超过了连接池的最大容量,请跳转连接池的数量");
        }
        return client;
    }

    /**
     * 回收客户端
     * @param client 客户端
     */
    public synchronized void recycleMinioClient(MinioClient client) {
        // 回收的为核心客户端
        if (0 < poolSize && poolSize <= coreSize){
            // 回收至队列中
            minioPool.add(client);
        }
        // 其余情况不需要回收至队列,对应的标记数量-1即可
        poolSize--;
    }
}

声明一个OSS接口,规范若依中的操作

OSSCient

class interface OSSClient{
    /**
     * 上传文件到oss
     * @param file   要上传的文件
     * @param filename 文件名 common模块对文件进行文件名编码
     * @param prefix 要上传的前缀
     * @return 资源的访问路径
     */
    public String uploadFile(MultipartFile file,String filename, String prefix);
    
    /**
     * 下载文件
     *
     * @param prefix   前缀
     * @param filename 要下载文件名
     * @param response 输出流对象
     */
    public void download(String prefix, String filename, HttpServletResponse response) throws Exception;
    /**
     * 通用下载
     * @param resource 资源路径
     * @param response 响应体
     */
    public void download(String resource,HttpServletResponse response);
    /**
     * 删除指定文件
     *
     * @param prefix   文件前缀路径
     * @param filename 文件名
     * @return true:删除成功;false:删除失败
     */
    public boolean deleteFile(String prefix, String filename);
}

这里只使用了minio OSS一种,其余厂商的OSS产品实现该接口应该也是可以完成响应的功能的。

定义minioOss的实现客户端实现

@Component
public class OSSClientByMinio {
    @Autowired
    private MinioConfig minioConfig;
    @Autowired
    private MinioPool minioPool;
    /**
     * 上传文件到oss
     *
     * @param file   要上传的文件
     * @param filename 文件名
     * @param prefix 要上传的前缀
     * @return 桶加+filepath
     */
    public String uploadFile(MultipartFile file,String filename, String prefix) {
        // 文件编码
        StringBuilder randomName = new StringBuilder();
        // 创建minio客户端
        MinioClient client = minioPool.getMinioClient();
        try {
            randomName.append(minioConfig.getUrl());
            randomName.append("/");
            randomName.append(minioConfig.getBucket());
            randomName.append("/");
            randomName.append(prefix);
            randomName.append("/");
            // 获取文件stream流
            InputStream stream = file.getInputStream();
            // 文件类型
            String contentType = file.getContentType();
            // 上传对象
            client.putObject(
                    PutObjectArgs
                            .builder()
                            .stream(stream, file.getSize(), -1)
                            .contentType(contentType)
                            .bucket(minioConfig.getBucket())
                            .object(prefix+"/"+filename)
                            .build());
            randomName.append(filename);
            // 推送成功
            return randomName.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }finally {
            // 回收连接对象
            minioPool.recycleMinioClient(client);
        }
    }


    /**
     * 下载文件
     *
     * @param prefix   前缀
     * @param filename 要下载文件名
     * @param response 输出流对象
     */
    public void download(String prefix, String filename, HttpServletResponse response) throws Exception {
        // 创建minio客户端
        download(prefix+"/"+filename,response);
    }

    /**
     * 通用下载
     * @param resource 资源路径
     * @param response 响应体
     */
    public void download(String resource,HttpServletResponse response){
        MinioClient client = minioPool.getMinioClient();
        GetObjectResponse object = null;
        BufferedOutputStream outputStream = null;
        // 获取输出流
        try {
            outputStream = new BufferedOutputStream(response.getOutputStream());
            object = client.getObject(GetObjectArgs.builder()
                    .object(resource)
                    .bucket(minioConfig.getBucket())
                    .build());
            // 设置输出的文件名和文件类型
            response.setContentType("application/x-msdownload;");
            response.setHeader("Content-disposition", "attachment;filename=" + resource.substring(resource.lastIndexOf("/")));
            byte[] bytes = new byte[2048];
            int len;
            while ((len =object.read(bytes,0,bytes.length)) != -1) {
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }finally {
            if(object != null){
                try {
                    object.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if(outputStream!= null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            // 回收客户端
            minioPool.recycleMinioClient(client);
        }


    }

    /**
     * 删除指定文件
     *
     * @param prefix   文件前缀路径
     * @param filename 文件名
     * @return true:删除成功;false:删除失败
     */
    public boolean deleteFile(String prefix, String filename) {
        MinioClient client = minioPool.getMinioClient();
        try {
            client.removeObject(RemoveObjectArgs.builder()
                            .bucket(minioConfig.getBucket())
                            .object(prefix+"/"+filename)
                    .build());
            return true;
        } catch (Exception e) {
            return false;
        }finally {
            // 回收
            minioPool.recycleMinioClient(client);
        }
    }
}

定义oss服务的服务业务接口

/**
 * OSS服务端的接口定义
 */
public interface IOSSService {
    // 通用上传文件
    String uploadFile(MultipartFile file,String filename,String prefix);
    // 基本下载
    void downloadFile(String filename, HttpServletResponse response);
    // 通用下载
    void generalDownload(String resource,HttpServletResponse response);
    boolean deleteFile(String filename);
}

实现:

@Component
public class OSSServiceImpl implements IOSSService {
    @Autowired
    private OSSClientByMinio ossClientByMinio;

    /**
     * 上传用户对象
     * @param file 要上传的对象
     * @return 资源的访问路径
     */
    @Override
    public String uploadFile(MultipartFile file,String filename,String prefix) {
        return ossClientByMinio.uploadFile(file,filename, prefix);
    }

    /**
     * 通用下载实现
     * @param filename 文件名
     */
    @Override
    public void downloadFile(String filename, HttpServletResponse response) {
        try {
            ossClientByMinio.download("common",filename,response);
        } catch (Exception e) {
            throw new RuntimeException("文件不存在");
        }
    }

    /**
     * 通用资源下载
     * @param resource 带有文件前缀的资源路径
     * @param response 响应
     */
    @Override
    public void generalDownload(String resource, HttpServletResponse response) {
        ossClientByMinio.download(resource,response);
    }

    @Override
    public boolean deleteFile(String filename) {
        return ossClientByMinio.deleteFile("common",filename);
    }
}

替换若依的资源上传下载功能

common模块的文件上传

/**
     * 文件上传
     *
     * @param baseDir 相对应用的基目录
     * @param file 上传的文件
     * @param allowedExtension 上传文件类型
     * @return 返回上传成功的文件名
     * @throws FileSizeLimitExceededException 如果超出最大大小
     * @throws FileNameLengthLimitExceededException 文件名太长
     * @throws IOException 比如读写文件出错时
     * @throws InvalidExtensionException 文件校验异常
     */
public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
    throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
InvalidExtensionException
{
    // 判断是否存在文件名
    int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
    // 判断文件名的长度是否超过了指定的长度 100
    if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
    {
        throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
    }

    // 校验文件大小
    assertAllowed(file, allowedExtension);
    // 文件编码
    String fileName = FileUploadUtils.extractFilename(file);
    // 获取ioc容器的minio客户端   这里为替换的内容
    // 因为方法是 static类型的,所以获取bean的方式有写不同,但若依已经封装好了对应的方法了
    IOSSService iossService = SpringUtils.getBean(IOSSService.class);
    // 调用自己实现的oss资源上传
    return iossService.uploadFile(file,fileName,baseDir);
}

由于若依的文件名的编码前缀过多,在minio不便后面的下载操作,所以我改了一下FileUploadUtils.extractFilename

 /**
     * 编码文件名
     */
public static final String extractFilename(MultipartFile file)
{
    // 将原来的 {YYYY}/{MM}/{DD}/{filename}_{时间}.{文件后缀},改为了{filename}_{时间}.{文件后缀}格式
    return StringUtils.format("{}_{}.{}",
                              FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file));
}

为了好管理,将用户头像统一放在avatar文件夹下,通过资源放在common文件夹下,所以这里要修改一下调用upload()是传入的baseDir的实参,原来传入的是我们在yml中配置的本地目录,现在在/system/user/profile/avatar中传入的是avatar


========[修改之前]===========
LoginUser loginUser = getLoginUser();
            String baseDir = CloudstudyConfig.getProfile();
            String avatar = FileUploadUtils.upload(baseDir, file, MimeTypeUtils.IMAGE_EXTENSION);

===== [修改之后] =======
LoginUser loginUser = getLoginUser();
            String avatar = FileUploadUtils.upload("avatar", file, MimeTypeUtils.IMAGE_EXTENSION);

admin模块中也是如此,将之前的baseDir,替换为"common"

@RestController
@RequestMapping("/common")
@Api("文件上传下载管理")
@Anonymous
public class CommonController
{
    private static final Logger log = LoggerFactory.getLogger(CommonController.class);


    private static final String FILE_DELIMETER = ",";


    @Autowired
    private IOSSService iossService;
    /**
     * 通用下载请求
     * 
     * @param fileName 文件名称
     * @param delete 是否删除
     */
    @GetMapping("/download")
    @ApiOperation("通用下载请求")
    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
    {
        try
        {
            if (!FileUtils.checkAllowDownload(fileName))
            {
                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
            }
            // 下载
            iossService.downloadFile(fileName,response);
            // 是否删除
            if (delete){
                iossService.deleteFile(fileName);
            }
//            String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
//            String filePath = CloudstudyConfig.getDownloadPath() + fileName;

//            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
//            FileUtils.setAttachmentResponseHeader(response, realFileName);
//            FileUtils.writeBytes(filePath, response.getOutputStream());
//            if (delete)
//            {
                FileUtils.deleteFile(filePath);
//            }
        }
        catch (Exception e)
        {
            log.error("下载文件失败", e);
        }
    }

    /**
     * 通用上传请求(单个)
     */
    @PostMapping("/upload")
    @Anonymous
    @ApiOperation("通用上传请求(单个)")
    public AjaxResult uploadFile(@RequestPart MultipartFile file) throws Exception
    {
        try
        {
            // 上传文件路径
//            String filePath = CloudstudyConfig.getUploadPath();
            // 上传并返回新文件名称
            String fileName = FileUploadUtils.upload("common", file);
            AjaxResult ajax = AjaxResult.success();
            ajax.put("url", fileName);
            ajax.put("fileName", fileName);
            ajax.put("newFileName", FileUtils.getName(fileName));
            ajax.put("originalFilename", file.getOriginalFilename());
            return ajax;
        }
        catch (Exception e)
        {
            return AjaxResult.error(e.getMessage());
        }
    }

    /**
     * 通用上传请求(多个)
     */
    @PostMapping("/uploads")
    @ApiOperation("通用上传请求(多个)")
    public AjaxResult uploadFiles(@RequestPart List<MultipartFile> files) throws Exception
    {
        try
        {
            // 上传文件路径
            List<String> urls = new ArrayList<String>();
            List<String> fileNames = new ArrayList<String>();
            List<String> newFileNames = new ArrayList<String>();
            List<String> originalFilenames = new ArrayList<String>();
            for (MultipartFile file : files)
            {
                // 上传并返回新文件名称
                String fileName = FileUploadUtils.upload("common", file);
                urls.add(fileName);
                fileNames.add(fileName);
                newFileNames.add(FileUtils.getName(fileName));
                originalFilenames.add(file.getOriginalFilename());
            }
            AjaxResult ajax = AjaxResult.success();
            ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
            ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
            ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
            ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
            return ajax;
        }
        catch (Exception e)
        {
            return AjaxResult.error(e.getMessage());
        }
    }

    /**
     * 本地资源通用下载
     */
    @GetMapping("/download/resource")
    @ApiOperation("本地资源通用下载")
    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
            throws Exception
    {
        try
        {
            // 检查资源路径是否可以下载
            if (!FileUtils.checkAllowDownload(resource))
            {
                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
            }
            iossService.generalDownload(resource,response);
//            // 本地资源路径
//            String localPath = CloudstudyConfig.getProfile();
//            // 数据库资源地址
//            String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
//            // 下载名称
//            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
//            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
//            FileUtils.setAttachmentResponseHeader(response, downloadName);
//            FileUtils.writeBytes(downloadPath, response.getOutputStream());
        }
        catch (Exception e)
        {
            log.error("下载文件失败", e);
        }
    }
}

最后一步–修改前端

因为我们已经使用了自己的OSS服务器,但前端的请求地址仍以/dev-api/**的方式获得用户的头像,所以这里要修改一下前端的请求地址的拼接,

位置:/src/views/system/user/userAvatar.vue

 // 上传图片
uploadImg() {
    this.$refs.cropper.getCropBlob(data => {
    let formData = new FormData();
    formData.append("avatarfile", data);
        uploadAvatar(formData).then(response => {
            this.open = false;
			// 这里之前是获取 .env中后端的请求地址 + response.imgUrl,现在要改为 response.imgUrl
            this.options.img = response.imgUrl;
            store.commit('SET_AVATAR', this.options.img);
            this.$modal.msgSuccess("修改成功");
            this.visible = false;
        });
    });
},

上传之后回显搞定了,还有未上传时,用户头像的请求

位置:/src/api/store/module/user.js

SET_AVATAR: (state, avatar) => {
    state.avatar = avatar
},

文章写的不好,还请谅解!

oss替换之后,还有就是日志服务器了,这个我还没做,但其业务逻辑在:

system模块下的service.impl.SysLogininforServiceImpl

你可能感兴趣的:(若依,mybatis,spring,boot,java)