Java学者 发表于 2020-12-7 09:19

小说精品屋-plus数据服务层的设计和实现

本帖最后由 Java学者 于 2020-12-7 09:24 编辑

## 前言
小说精品屋中集成了比较多的数据服务。比如缓存相关的Redis和Ehcache,文件相关的本地、Aliyun OSS和FastDfs,搜索相关的ElasticSearch和Mysql,这些数据服务均可在配置文件中通过一行代码进行切换底层实现,下面以文件服务为例来说说数据服务层的具体设计与实现。

## 文件服务模块的设计与实现

1. 新建文件服务接口,定义存储图片的抽象方法。

```
package com.java2nb.novel.service;


/**
* @author 11797
*/
public interface FileService {

    /**
   * 将爬取的网络图片转存为自己的存储介质(本地、OSS、fastDfs)
   * @param picSrc 爬取的网络图片路径
   * @param picSavePath 保存路径
   * @return 新图片地址
   * */
    String transFile(String picSrc, String picSavePath);

}
```
2. 新建本地文件服务实现类,实现文件服务接口,保存文件到本地,并通过@ConditionalOnProperty注解来控制当配置属性pic.save.storage=local时,该实现类会实例化被Spring容器管理。

```
package com.java2nb.novel.service.impl;

import com.java2nb.novel.core.utils.Constants;
import com.java2nb.novel.core.utils.FileUtil;
import com.java2nb.novel.service.FileService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

/**
* @author 11797
*/
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "pic.save", name = "storage", havingValue = "local")
public class LocalFileServiceImpl implements FileService {

    @Override
    public String transFile(String picSrc, String picSavePath){

      return FileUtil.network2Local(picSrc, picSavePath, Constants.LOCAL_PIC_PREFIX);
    }
}
```
3. 新建Aliyun OSS文件服务实现类,实现文件服务接口,保存文件到Aliyun OSS,并通过@ConditionalOnProperty注解来控制当配置属性pic.save.storage=OSS时,该实现类会实例化被Spring容器管理。

```
package com.java2nb.novel.service.impl;

import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.CannedAccessControlList;
import com.aliyun.oss.model.CreateBucketRequest;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import com.java2nb.novel.core.config.OssProperties;
import com.java2nb.novel.core.utils.Constants;
import com.java2nb.novel.core.utils.FileUtil;
import com.java2nb.novel.service.FileService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

import java.io.File;

/**
* @author 11797
*/
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "pic.save", name = "storage", havingValue = "OSS")
@Slf4j
public class OssFileServiceImpl implements FileService {

    private final OssProperties ossProperties;

    @Override
    public String transFile(String picSrc, String picSavePath) {

      File file;
      String filePath = FileUtil.network2Local(picSrc, picSavePath, Constants.LOCAL_PIC_PREFIX);
      if (filePath.contains(Constants.LOCAL_PIC_PREFIX)) {
            file = new File(picSavePath+filePath);
      } else {
            //默认图片不存储
            return filePath;
      }

      filePath = filePath.replaceFirst(picSavePath,"");

      filePath = filePath.startsWith("/") ? filePath.replaceFirst("/","") : filePath;


      OSSClient ossClient = new OSSClient(ossProperties.getEndpoint(), ossProperties.getKeyId(), ossProperties.getKeySecret());
      try {
            //容器不存在,就创建
            if (!ossClient.doesBucketExist(ossProperties.getBucketName())) {
                ossClient.createBucket(ossProperties.getBucketName());
                CreateBucketRequest createBucketRequest = new CreateBucketRequest(ossProperties.getBucketName());
                createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead);
                ossClient.createBucket(createBucketRequest);
            }
            //上传文件
            PutObjectResult result = ossClient.putObject(new PutObjectRequest(ossProperties.getBucketName(), filePath, file));
            //设置权限 这里是公开读
            ossClient.setBucketAcl(ossProperties.getBucketName(), CannedAccessControlList.PublicRead);

            if(result != null) {
                return ossProperties.getWebUrl() + "/" + filePath;
            }
      } catch (Exception e) {
            log.error(e.getMessage(), e);
      } finally {
            //关闭
            ossClient.shutdown();
            file.delete();
      }

      return "/images/default.gif";
    }


}
```
4. 新建FastDfs文件服务实现类,实现文件服务接口,保存文件到FastDfs,并通过@ConditionalOnProperty注解来控制当配置属性pic.save.storage=fastDfs时,该实现类会实例化被Spring容器管理。

```
package com.java2nb.novel.service.impl;

import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.java2nb.novel.core.utils.Constants;
import com.java2nb.novel.core.utils.FileUtil;
import com.java2nb.novel.service.FileService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.FileInputStream;

/**
* @author 11797
*/
@Service
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty(prefix = "pic.save", name = "storage", havingValue = "fastDfs")
public class FastDfsFileServiceImpl implements FileService {

    private final FastFileStorageClient storageClient;

    @Value("${fdfs.webUrl}")
    private String webUrl;

    @Override
    public String transFile(String picSrc, String picSavePath) {

      File file;
      String filePath = FileUtil.network2Local(picSrc, picSavePath, Constants.LOCAL_PIC_PREFIX);
      if (filePath.contains(Constants.LOCAL_PIC_PREFIX)) {
            file = new File(picSavePath + filePath);
      } else {
            //默认图片不存储
            return filePath;
      }

      try {
            FileInputStream inputStream = new FileInputStream(file);
            StorePath storePath = storageClient.uploadFile(inputStream, file.length(),
                  FilenameUtils.getExtension(file.getName()), null);
            //这里额外加上LOCAL_PIC_PREFIX路径,表明该图片是个人资源,而不是爬虫爬取的网络资源,不需要再次进行转换,
            // 实际访问时,再通过nginx的rewite指令来重写路径,去掉LOCAL_PIC_PREFIX
            return webUrl+Constants.LOCAL_PIC_PREFIX+storePath.getFullPath();
      } catch (Exception e) {
            log.error(e.getMessage(), e);
      } finally {
            //删除
            file.delete();
      }

      return "/images/default.gif";
    }
}
```
5. 新建配置pic.save.storage,用来控制真正被Spring容器管理的实现类。

```
pic:
save:
    type: 2 #图片保存方式, 1不保存,使用爬取的网络图片 ,2保存在自己的存储介质
    storage: local #存储介质,local:本地,OSS:阿里云对象存储,fastDfs:分布式文件系统
    path: /var/pic#图片保存路径
```

6. 在需要使用文件服务的类中注入文件服务接口FileService,具体实现类只有在运行期才知道,由配置属性pic.save.storage来指定。

```
    @Autowire
    private FileService fileService;

    @Override
    public void updateBookPicToLocal(String picUrl, Long bookId) {

      picUrl = fileService.transFile(picUrl, picSavePath);

      bookMapper.update(update(book)
                .set(BookDynamicSqlSupport.picUrl)
                .equalTo(picUrl)
                .set(updateTime)
                .equalTo(new Date())
                .where(id, isEqualTo(bookId))
                .build()
                .render(RenderingStrategies.MYBATIS3));

    }
```
## 依赖倒置原则
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。数据服务层的设计正是遵循了依赖倒置原则,实现了模块间的解耦,切换底层数据存储只需要修改一个配置项即可。

## 项目仓库地址
https://github.com/201206030/novel-plus

Java学者 发表于 2020-12-7 09:40

agi学习者 发表于 2020-12-7 09:35
厉害了,word楼主学者,确实要做一个小说app,所涉及的数据是海量的,爬虫去爬取来的数据需要存储,而且尽 ...

嗯嗯,后面有时间,教程也会慢慢完善的

Java学者 发表于 2020-12-13 12:58

shao981109 发表于 2020-12-13 12:22
老哥,这边和你讨论个问题,你那个book的分页问题,你那逻辑生成的book_id有95%以上是偶数,也就是落在奇数 ...

用murmur3的hash算法的话,那得再单独开发个ID生成器的服务,又增加了系统的复杂度,还得考虑单点等问题,不然ID生成器挂了,整个系统就没法用了,很没有必要,而且也没听说有人用这个来做分布式ID的生成算法,雪花算法是目前分布式ID生成算法用的比较多的,经过业界验证过的,偶数多,你可以修改分表的策略呀,为什么会想到修改ID的生成算法呢?分表策略是可以直接在外部配置文件中修改,我这个只是用的其中一种算法而已,每个人都可以根据自己的需求定制自己的分库分表策略

agi学习者 发表于 2020-12-7 09:35

厉害了,word楼主学者,确实要做一个小说app,所涉及的数据是海量的,爬虫去爬取来的数据需要存储,而且尽量不要漏掉,有更新的部分要实现增量存储,对数据服务层要求很高,前端用户阅读部分,与用户的交互也要流畅,必然需要引入缓存集群。
不知楼主大大可否在百忙之中录制一部讲解小说精品屋-plus这个项目开发过程的视频教程?就好像那些培训机构那样,从UML讲起,把整个软件前后端的设计思路、技术选型、数据结构、部署运维、持续集成/持续交付……等过程给您的粉丝们讲讲,我非常想知道项目实现的思路以及代码为什么这么写的原因。

agi学习者 发表于 2020-12-7 09:43

Java学者 发表于 2020-12-7 09:40
嗯嗯,后面有时间,教程也会慢慢完善的

太感谢了~就希望看您搭建环境,讲一下架构设计思路,然后复现一下代码,当然最重要的就是爬虫模块,是咱们项目的核心,是生产力的源泉~

大张呀 发表于 2020-12-7 09:44

感觉好厉害,外行人看热闹。

lzy333 发表于 2020-12-7 09:44

挺厉害的样子,支持支持支持

wlaeni 发表于 2020-12-7 09:50

新手!看着很厉害,就是看不懂

easthq 发表于 2020-12-7 09:56

哇,谢谢楼主的分析分享

清淡如风 发表于 2020-12-7 10:06

厉害了,支持!

qihang5518 发表于 2020-12-7 10:07

谢谢分享学习经验
页: [1] 2 3 4
查看完整版本: 小说精品屋-plus数据服务层的设计和实现