小说精品屋-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 agi学习者 发表于 2020-12-7 09:35
厉害了,word楼主学者,确实要做一个小说app,所涉及的数据是海量的,爬虫去爬取来的数据需要存储,而且尽 ...
嗯嗯,后面有时间,教程也会慢慢完善的 shao981109 发表于 2020-12-13 12:22
老哥,这边和你讨论个问题,你那个book的分页问题,你那逻辑生成的book_id有95%以上是偶数,也就是落在奇数 ...
用murmur3的hash算法的话,那得再单独开发个ID生成器的服务,又增加了系统的复杂度,还得考虑单点等问题,不然ID生成器挂了,整个系统就没法用了,很没有必要,而且也没听说有人用这个来做分布式ID的生成算法,雪花算法是目前分布式ID生成算法用的比较多的,经过业界验证过的,偶数多,你可以修改分表的策略呀,为什么会想到修改ID的生成算法呢?分表策略是可以直接在外部配置文件中修改,我这个只是用的其中一种算法而已,每个人都可以根据自己的需求定制自己的分库分表策略 厉害了,word楼主学者,确实要做一个小说app,所涉及的数据是海量的,爬虫去爬取来的数据需要存储,而且尽量不要漏掉,有更新的部分要实现增量存储,对数据服务层要求很高,前端用户阅读部分,与用户的交互也要流畅,必然需要引入缓存集群。
不知楼主大大可否在百忙之中录制一部讲解小说精品屋-plus这个项目开发过程的视频教程?就好像那些培训机构那样,从UML讲起,把整个软件前后端的设计思路、技术选型、数据结构、部署运维、持续集成/持续交付……等过程给您的粉丝们讲讲,我非常想知道项目实现的思路以及代码为什么这么写的原因。 Java学者 发表于 2020-12-7 09:40
嗯嗯,后面有时间,教程也会慢慢完善的
太感谢了~就希望看您搭建环境,讲一下架构设计思路,然后复现一下代码,当然最重要的就是爬虫模块,是咱们项目的核心,是生产力的源泉~ 感觉好厉害,外行人看热闹。 挺厉害的样子,支持支持支持 新手!看着很厉害,就是看不懂 哇,谢谢楼主的分析分享 厉害了,支持! 谢谢分享学习经验