北京尚学堂2020java_FastDFS+Nginx

FastDFS是一个专门做文件存储的应用,不再是用IO流,而是用FastDFS作为文件系统。

第一节 项目架构的改变

如果客户端上传了一个文件到集群,集群把文件放在了A,后来请求的时候集群把请求发给了B,B中没有,简直要死了。
可以把架构变成这样:

第二节 分布式文件系统概述

2.1 分类

通用分布式文件系统:和传统的本地文件系统(ext3、NTFS等)相对应,典型代表:lustre、MooseFS
优点:标准文件系统操作方式,对开发者门槛较低
缺点:系统复杂度较高,需要支持若干标准的文件操作,如:目录结构、文件读写权限、文件锁等。复杂性更高

专用分布式文件系统
基于google File System思想,文件上传后不能修改。需要使用专有API对文件进行访问,也可称作分布式文件存储服务。典型代表:MogileFs、FastDFS、TFS。
属性之类的就是不是必要的。
优点:系统复杂性低,不需要支持若干标准的文件操作,如目录结构、文件读写权限、文件锁等,系统比较简洁。无需支持POSIX标准
缺点:采用专有API,对开发者门槛较高。

2.2 Google FS体系结构

两个角色:名字服务器(索引服务器) 存储服务器
架构特点:不支持文件修改功能,文件分块存储需要索引服务器;一个文件可以存储多份,一个文件存储的哪些存储服务器,通常采用动态分配的方式。
分块:一个很大的文件,分成三块;很多个文件在一起就很大了。

第三节 FastDFS简介

即快速的分布式文件系统,主要解决了大容量的文件存储和高并发访问的问题,文件存取的同时实现了复杂均衡。实现了软件方式的磁盘阵列(RAID),可以使用廉价的IDE(Integrated Drive Eletronics)硬盘进行存储。
FastDFS只能通过Client API访问,不支持POSIX访问方式,不是一个传统的文件系统。特别使用大中型网站使用,用来存储文件(图片文档音频视频。

第五节 FastDFS安装文档

首先安装环境:

yum install -y make cmake gcc gcc-c++

然后解压lib那个zip,移动到/usr/local,接着“./make.sh"接着“./make.sh install”

ln -s /usr/lib64/libfastcommon.so /usr/local/lib/libfastcommon.so
ln -s /usr/local/lib64/libfdfsclient.so /usr/local/lib/libfdfsclient.so

然后解压缩FastDFS,cd进去./make.sh即可。

ls /usr/bin | grep fdfs:可执行文件
ls /etc/fdfs 配置文件
ls /usr/lib 主程序代码所在位置
ls /usr/include/fastdfs 包含一些插件组所在的位置

5.2 Tracker跟踪器配置及启动

也是启动追踪器的索引服务
复制/etc/dfds下的tracker_conf然后修改一下base,自定义即可,修改好了别忘了创建自定义的文件夹。

// 启动服务
service fdfs_trackerd start

或者直接:

/etc/init.d/fdfs_trackerd start

5.3 配置storage

一般tracker和storage可以不在同一台服务器上,这里就先放在一起了。

cd /etc/fdfs
cp storage.conf.smaple storage.conf

然后再修改它,base_path指的是运行过程一些文件的存储路径,store_path_count=是存储真正文件数据的路径有几个,store_path0定义了存储文件的路径。还需要配置tracker_server的地址
tracker的端口是22122和8080,storage有23000和8888。接下来就是创建文件并启动服务:

mkdir -p /fastdfs/storage/base
mkdir -p /fastdfs/storage/store
service fdfs_storaged start

编写配置文件20_fdfs/fdfs_java/src/main/resources/fdfs_config.conf:

connect_timeout=10
network_timeout=30
charset=UTF-8
http.tracker_http_port=8080
tracker_server=192.168.2.129:22122

第六节 FastDFS增删改查

首先引入依赖20_fdfs/fdfs_java/pom.xml:

<dependencies>
    <!-- https://mvnrepository.com/artifact/cn.bestwu/fastdfs-client-java -->
    <dependency>
        <groupId>cn.bestwu</groupId>
        <artifactId>fastdfs-client-java</artifactId>
        <version>1.27</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.9</version>
    </dependency>
</dependencies>

然后利用静态代码块来那个初始化服务端客户端追踪器存储器20_fdfs/fdfs_java/src/main/java/com/bjsxt/utils/fdfs/FdfsUtil.java:

/**
 * 工具类型:提供链接创建、上传、下载和删除等操作
 */
public class FdfsUtil {
    // 通过static初始化代码块,读取配置文件,初始化客户端链接对象
    // 客户端链接对象,用于实现文件读写操作时
    private static StorageClient storageClient;
    static {
        try {
            // 读取配置文件
            // 获取文件名称
            String path = Thread.currentThread().getContextClassLoader()
                    .getResource("").getPath() +  "fdfs_config.conf";
            // 加载配置文件
            ClientGlobal.init(path);
            // 创建Tracker客户端对象
            TrackerClient trackerClient = new TrackerClient();
            // 创建tacker服务器对象
            TrackerServer trackerServer = trackerClient.getConnection();
            // 创建Storage服务器对象
            StorageServer storageServer = trackerClient.getStoreStorage(trackerServer);
            // 创建Storage客户端对象
            storageClient = new StorageClient(trackerServer, storageServer);
        }   catch (Exception e) {
            e.printStackTrace();
            // 初始代码块出错,一定要抛出错误,停止虚拟机
            throw new ExceptionInInitializerError(e);
        }
    }
}

然后编写上传函数:

/**
 * 使用StorageClient对象,实现文件上传
 */
public static String[] uploadFile(byte[] datas, String fileName, String authName) {
    try {
        // 获取文件的拓展名
        String extName = fileName.substring(fileName.lastIndexOf(".") + 1);
        // 创建文件拓展信息, 包括文件的原始名称,文件的大小,文件的上传者姓名
        NameValuePair[] metaDatas = new NameValuePair[3];
        metaDatas[0] = new NameValuePair("fileName", fileName);
        metaDatas[1] = new NameValuePair("fileSize", datas.length + "");
        metaDatas[2] = new NameValuePair("auth", authName);
        /**
         * 文件上传
         * 获取文件的拓展名
         * String[] storageClient.upload_file(byte[], String extName, NameValuePair[] metaDatas)
         * datas - 要上传的文件的内容
         * extName - 上传的文件的拓展名
         * metaDatas - 上传文件的拓展信息是什么 如:文件的原始名称、文件的容量大小、文件的上传者等等。
         */
        String[] result = storageClient.upload_file(datas, extName, metaDatas);
        // 上传成功 无异常 返回字符串数组
        // 字符串数组长度为2    0下标位置   卷名|组名   1下标位置是文件名(目录/文件)
        // fdfs为了保证上传的文件原始名称冲突内容不冲突的情况而覆盖的问题,存储文件的时候,会提供一个uuid文件名称。
        return result;
    } catch (Exception e) {
        e.printStackTrace();
        return null;    // 异常发生 返回null 代表上传失败
    }
}  

接着调用上传函数20_fdfs/fdfs_java/src/main/java/com/bjsxt/test/TestFdfs.java:

public class TestFdfs {
    public static void main(String[] args)throws Exception {
        upload();
    }
    public static void upload() throws Exception{
        // 读取文件内容
        String fileName = "1.png";
        InputStream inputStream = TestFdfs.class.getClassLoader().getResourceAsStream(fileName);
        // 创建字节数组
        byte[] datas = new byte[inputStream.available()];
        // 读取文件内容到字节数组中
        inputStream.read(datas, 0, datas.length);
        // 上传文件
        String[] result = FdfsUtil.uploadFile(datas, fileName, "老张");
        System.out.println("卷名:" + result[0]);
        System.out.println("文件名:" + result[1]);
        System.out.println("返回数组的长度:" + result.length);
    }
}

最后输出的信息:

所谓的就是设置的文件存储路径/fastdfs/storage/store/data/
接着编写下载函数20_fdfs/fdfs_java/src/main/java/com/bjsxt/utils/fdfs/FdfsUtil.java:

public static byte[] downloadFile(String groupName, String fileName, NameValuePair[] metaDatas) {
    try {
        /**
         * byte[] storageClient.download_file(String groupName, String fileName)
         * groupName    -   卷名 | 组名
         * fileName     -   文件名,是文件保存在
         */
        byte[] datas =  storageClient.download_file(groupName, fileName);
        // 要下载的文件的扩展信息
        if(metaDatas != null) {
            // 一个引用值,外部变量指向的是相同的数据
            NameValuePair[] tmp = storageClient.get_metadata(groupName, fileName);
            // 把查询到的文件拓展信息保存到传入的数组中
            for(int i = 0;  i < tmp.length; i++) {
                metaDatas[i] = tmp[i];
            }
        }
        // 返回下载的文件内容
        return datas;
    } catch (Exception e) {
        e.printStackTrace();
        return null;    //下载失败,返回Null
    }
}

接着使用该函数20_fdfs/fdfs_java/src/main/java/com/bjsxt/test/TestFdfs.java,注意文件路径需要写好盘符:

// 文件下载 下载的文件保存在D盘根目录下。文件名称是文件的原始名称
public static void download() throws Exception {
    // 用于保存文件拓展信息的数组
    String groupName = "group1";
    String fileName = "M00/00/00/wKgCgWJBMsaANUUdAAuBRYY3e1M093.png";
    NameValuePair[] metaDatas = new NameValuePair[3];
    // 下载文件
    byte[] datas =  FdfsUtil.downloadFile(groupName, fileName, metaDatas);
    System.out.println(datas);
    System.out.println(metaDatas);
    String localName = "";
    for(NameValuePair nameValuePair: metaDatas) {
        System.out.println(nameValuePair.getName() + "  -   " + nameValuePair.getValue());
        if(nameValuePair.getName().equals("fileName")) {
            localName = nameValuePair.getValue();
        }
    }
    // 把下载的文件内容保存到
    FileOutputStream outputStream = new FileOutputStream("D:\\" +  localName);
    outputStream.write(datas);
    outputStream.flush();
    outputStream.close();
}

启动服务

systemctl stop firwalld
systemctl start fdfs_trackerd
systemctl start fdfs_storaged

删除代码20_fdfs/fdfs_java/src/main/java/com/bjsxt/utils/fdfs/FdfsUtil.java:

/**
 * 文件删除,FastDFS会自动删除这个文件对应的metaData
 */
public static boolean deleteFile(String groupName, String fileName) {
    try {
        int result =  storageClient.delete_file(groupName, fileName);
        // 返回结果为0代表删除成功,其他是删除失败。
        return result == 0;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

测试删除代码20_fdfs/fdfs_java/src/main/java/com/bjsxt/test/TestFdfs.java:

public static void delete() throws Exception{
    String groupName = "group1";
    String fileName = "M00/00/00/wKgCgWJBMsaANUUdAAuBRYY3e1M093.png";
    boolean flag = FdfsUtil.deleteFile(groupName, fileName);
    System.out.println(flag ? "删除文件成功": "删除文件失败");
}

第七节 FastDFS与nginx简介

要支持在线浏览,需要Nginx,是一个高性能HTTP和反向代理服务。Nginx是Apache服务不错的替代品,是美国做虚拟主机生意老板们经常选择的软件平台之一。

7.1 正向代理与反向代理

正向代理:vpn 一个位于客户端和原始服务器(origin server),其实就是FQ软件。
为了从google取得内容,客户端向代理器发送一个请求并指定原始服务器,代理向原始服务器转交请求并将获得的内容返回给客户端。
反向代理 Reverse Proxy,类似于经理的角色,对于外部过来的网络请求,决定交给我们搭建的集群中的那台机器来解决。DNS就是反向代理服务器。
比如这里你是为了看图片,那么我就由nginx决定将请求交给哪台服务器。
正向代理假设在客户端和目标主机之间,反向代理假设在服务器端。
代理对象不同:

  • 正向代理,代理客户端,服务端不知道实际发起请求的客户端
  • 反向代理,代理服务端,客户端不知道实际提供的服务的服务端

7.3 Nginx作用

  • HTTP协议代理
    • 只要支持HTTP协议访问的内容,都可以由Nginx进行代理,Nginx只支持HTTP协议的代理,其他协议不支持。
  • 搭建虚拟主机
    • 可以监听所安装的主机的某个端口,对外支持这个端口的HTTP访问。
    • 当接收到外部HTTP请求后把本机资源返回给客户端
    • 使用Nginx的搭建虚拟主机的功能,外部请求客户端,把图片信息响应给请求。
  • 负载均衡
    • Nginx可以代理多个主机,内置负载均衡策略

第八节 Nginx安装

8.1 fastdfs-nginx组件

安装依赖:

yum install -y gcc gcc-c++ make automake autoconf libtool pcre pcre-devel zlib zlib-devel openssl openssl-devel

把插件组fastdfs-nginx-module压缩包解压并且把文件移动:

mv fastdfs-nginx-module /usr/local/fastdfs/fdfs-nginx-module

修改配置文件/usr/local/fastdfs/fdfs-nginx-module/src/config

重要的就是将CORE_INCS文件夹中的/local去掉.

8.2 安装nginx

也是解压源代码,然后配置最后编译。
最后一行表示的是插件模块的位置,这样即可把nginx和插件联系起来

./configure \
--prefix=/usr/local/nginx \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_gzip_static_module \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi \
--add-module=/usr/local/fastdfs/fdfs-nginx-module/src

只要在项目文件目录/root/nginx-1.16.1下执行:

make 
make install

然后能在/usr/local/nginx中看到一些文件,说明Nginx安装好了。
把插件移动到/etc下:

cd /usr/local/fastdfs/fdfs-nginx-module/src
cp mod_fastdfs.conf /etc/fdfs

然后修改插件配置:

将之前解压的fastdfs源码文件/conf中的两个配置文件复制到/etc/fdfs中:

创建一个软件链接:

ln -s /fastdfs/storage/store/data/ /fastdfs/storage/store/M00

还需要修改nginx的配置,/usr/local/nginx/conf/nginx.conf将身份改成root:

还需要新增一个server,监听的端口就是fastdfs storage的http端口8888:

然后启动nginx:

退出的命令是:

sbin/nginx -s quit

然后就可以通过浏览器去访问图片:
http://192.168.2.129:8888/group1/M00/00/00/wKgCgWJBMdaAcmJUAAuBRYY3e1M274.png
但很遗憾我失败了。(P543_https://www.bilibili.com/video/BV13z4y167eH?p=543)

第九节 KEditor

新建一个project,把Keditor引入依赖:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.2.5.RELEASE</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

然后把软件Keditor中的相关文件复制进resource/static/keditor:

![](https://img2022.cnblogs.com/blog/1459792/202204/1459792-20220402195134802-1343729357.png)

在前端代码中引入相关的文件20_fdfs/keditor/src/main/resources/static/index.html:

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="keditor/themes/default/default.css">
    <script src="keditor/kindeditor-all-min.js"></script>
    <script src="keditor/lang/zh_CN.js"></script>
    <script type="text/javascript">
        KindEditor.ready(function (K) {
            var params = {
                allowFileManager: true,
                uploadJson : 'upload'
            };
            var editor =  K.create('textarea[name="content"]',params);
        });
    </script>
</head>
<body>
    <form>
        <label>图文编辑:</label>
        <textarea name="content" ></textarea>
    </form>
</body>

第十节 图片管理应用

10.1 img_pojo模块

引入依赖:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.2.5.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
    </dependencies>
</dependencyManagement>

创建表:

create table img_manage(
    id bigint(20) not null auto_increment COMMENT '主键',
    url varchar(255) default '' comment '访问这个图片的HTTP地址',
    group_name varchar(64) default 'group1' comment 'FastDFS中的卷名',
    remote_file_name varchar(255) default '' comment 'FastDFS中的文件名,如:M00/00/00/abc.jpg',
    orgin_file_name varchar(255) default '' comment '上传的图片的原始名称',
    upload_time date not null comment '上传图片的时间',
    primary key(id)
)   comment '保存上传到FastDFS中的图片信息表';

写实体类20_fdfs/img_manager/img_pojo/src/main/java/com/bjsxt/img/pojo/Image.java:

/**
 * 自定义实体类型,代表一个上传的文件对象
 * 不适用逆向工程,是为了带着多写点代码
 */
public class Image implements Serializable {
    private long id;
    private String groupName;
    private String remoteFileName;
    private String originFileName;
    private Date uploadTime;

    public String getUrl() {
        return groupName + "/"  + remoteFileName;
    }

    public void setUrl(String url) {
        // url属性为推导属性。url的值,是由groupName和remoteFileName组合得到的。

    }

这里的url是推到属性,比如一个人如果有了姓和名,那么就不需要再弄一个name属性了,不过数据库里可以有。
mybatis逆向工程:指的是连Pojo类都不写,直接建好表后由mybatis生成

10.2 img_mapper模块

引入依赖:

<dependencies>
    <dependency>
        <groupId>com.bjsxt</groupId>
        <artifactId>img_pojo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

定义好接口20_fdfs/img_manager/img_mapper/src/main/java/com/bjsxt/img/mapper/ImageMapper.java:

/**
 * 图片数据访问接口,链接数据库MySql实现CURD操作
 */
public interface ImageMapper {
    int insert(Image image);
    int deleteByPK(LocateRegistry id);
    Image selectById(Long id);
    List<Image> select();
}

在resources中定义好mapper的xml规则20_fdfs/img_manager/img_mapper/src/main/resources/mybatis/mapper/ImageMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bjsxt.img.mapper.ImageMapper">
    <!-- 映射规则-->
    <resultMap id="ImageMap" type="com.bjsxt.img.pojo.Image">
        <id property="id" column="id"></id>
        <result property="groupName" column="group_name"></result>
        <result property="remoteFileName" column="remote_file_name"></result>
        <result property="originFileName" column="origin_file_name"></result>
        <result property="uploadTime" column="upload_time"></result>
        <result property="url" column="url"></result>
    </resultMap>
    <insert id="insert" keyProperty="id"  useGeneratedKeys="true"   keyColumn="id">
        insert into img_manage(id, url, group_name, remote_file_name, origin_file_name, upload_time)
        values(DEFAULT, #{url}, #{groupName}, #{remoteFileName}, #{originFileName}, #{uploadTime})
    </insert>
    <delete id="deleteByPK">
        delete from img_manage
        where id = #{id}
    </delete>
    <select id="selectById" resultMap="ImageMap">
        select id, url, group_name, remote_file_name, origin_file_name, upload_time
        from img_manage
        where id = #{id}
    </select>
    <select id="select" resultMap="ImageMap">
        select id, url, group_name, remote_file_name, origin_file_name, upload_time
        from img_manage
    </select>
</mapper>

最后配置一下接口文件等20_fdfs/img_manager/img_mapper/src/main/resources/application-db.yml:

mybatis:  #sql映射文件
  mapper-locations: classpath:mybatis/mapper/*.xml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
    username: root
    password:

10.3 img_service_api

把刚刚写好的实体类引入依赖:

<dependencies>
    <dependency>
        <groupId>com.bjsxt</groupId>
        <artifactId>img_pojo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

新建一个模块,并且在其中定义好需要实现的接口
20_fdfs/img_manager/img_service_api/src/main/java/com/bjsxt/img/serviceapi/ImageServiceAPI.java:

/**
 * 图片服务标准接口
 * dubbo的Provider需要实现这个接口提供服务
 * dubbo的Consumer需要使用这个接口调用服务
 */
public interface ImageServiceAPI {
    // 新增图片
    int save(Image image);
    // 删除图片
    int remove(Long id);
    // 查看图片详情
    Image getById(Long id);
    // 分页查看图片信息,使用PageHelper实现分页
    // 返回的结果是: {row=[{图片对象}] total=总计图片数量, currentPage=当前页面,pages=总计页数, size=每页行数}
    Map<String, Object> getImages();
}

10.4 img_service_provider

将必要的东西引入依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.bjsxt</groupId>
        <artifactId>img_service_api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.bjsxt</groupId>
        <artifactId>img_mapper</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>  

实现api中定义的方法20_fdfs/img_manager/img_service_provider/src/main/java/com/bjsxt/img/service/impl/ImageServiceImpl.java:

/**
 * 服务提供者,实现服务标准
 */
@Service    //有了这个注解,dubbo会把服务自动注入进zookeeper
public class ImageServiceImpl implements ImageServiceAPI {
    // 注入Mapper对象
    @Autowired
    private ImageMapper imageMapper;

    /**
     * 新增图片到数据库
     * @param image 要新增的数据对象
     * @return
     */
    @Override
    @Transactional
    public int save(Image image) {
        return imageMapper.insert(image);
    }

    /**
     * 删除数据
     * @param id 要删除的数据主键
     * @return
     */
    @Override
    @Transactional
    public int remove(Long id) {
        return imageMapper.deleteByPK(id);
    }

    /**
     * 主键查询
     * @param id 要查询的数据主键
     * @return
     */
    @Override
    public Image getById(Long id) {
        return imageMapper.selectById(id);
    }

    /**
     * 分页查询
     * @param page 第几页
     * @param rows 第几行
     * @return
     */
    @Override
    public Map<String, Object> getImages(int page, int rows) {
        // 使用分页查询
        PageHelper.startPage(page, rows);
        // 分页查询,返回的结果是PageHelper封装的List实现的类型Page
        List<Image> list = imageMapper.select();
        // 使用PageInfo辅助工具对象,实现分页数据的获取
        PageInfo<Image> info = new PageInfo<>(list);
        // 创建返回结果
        Map<String ,Object> result = new HashMap<>();
        result.put("total", info.getTotal());   // 总计数据行数
        result.put("rows", list);   // 当前页面的数据集合
        result.put("currentPage", page);    // 当前是第几页
        result.put("pages", info.getPages());   // 总计多少页
        return result;
    }
}

写好启动类20_fdfs/img_manager/img_service_provider/src/main/java/com/bjsxt/img/ImageProviderApp.java:

@SpringBootApplication
@EnableDubbo
@MapperScan(basePackages = {"com.bjsxt.img.mapper"})
@EnableDubboConfig
public class ImageProviderApp {
    public static void main(String[] args) {
        SpringApplication.run(ImageProviderApp.class, args);
    }
}

配置好项目20_fdfs/img_manager/img_service_provider/src/main/resources/application.yml:

spring:
  profiles:
    active: db
dubbo:
  application:  # dubbo应用必须提供一个唯一的命名,同名的dubbo应用自动组成集群
    name: img_manage_provider
  protocol:
    name: dubbo
    port: 20880
  registry:
    address: zookeeper://192.168.2.129:2181
  config-center:
    timeout: 30000
server:
  port: 8081
pagehelper:
  helper-dialect: mysql

10.5 img_service_consumer

首先配置文件:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.bestwu</groupId>
        <artifactId>fastdfs-client-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
</dependencies>

可以参考之前项目实现的FastDFS工具类来实现,注意把所有方法的static删掉,不需要静态,只要一个实例类即可20_fdfs/img_manager/img_service_consumer/src/main/java/com/bjsxt/img/utils/FastDFSUtils.java:

public class FastDFSUtils {
    // 通过static初始化代码块,读取配置文件,初始化客户端链接对象
    // 客户端链接对象,用于实现文件读写操作时
    private static StorageClient storageClient;

    public FastDFSUtils() {
        // 提供默认配置
        Properties properties = new Properties();
        properties.setProperty("fastdfs.connect_timeout_in_seconds", "5");
        properties.setProperty("fastdfs.network_timeout_in_seconds", "30");
        properties.setProperty("fastdfs.charset", "UTF-8");
        properties.setProperty("fastdfs.http_tracker_http_port", "8080");
        properties.setProperty("fastdfs.tracker_servers", "localhost:22122");
        init(properties);
    }

    public FastDFSUtils(Properties properties) {

    }

    private void init(Properties properties) {
        try {
            // 读取配置文件,借助springboot实现配置
            ClientGlobal.initByProperties(properties);
            // 获取文件名称
            String path = Thread.currentThread().getContextClassLoader()
                    .getResource("").getPath() +  "fdfs_config.conf";
            // 加载配置文件
            ClientGlobal.init(path);
            // 创建Tracker客户端对象
            TrackerClient trackerClient = new TrackerClient();
            // 创建tacker服务器对象
            TrackerServer trackerServer = trackerClient.getConnection();
            // 创建Storage服务器对象
            StorageServer storageServer = trackerClient.getStoreStorage(trackerServer);
            // 创建Storage客户端对象
            storageClient = new StorageClient(trackerServer, storageServer);
        }   catch (Exception e) {
            e.printStackTrace();
            // 初始代码块出错,一定要抛出错误,停止虚拟机
            throw new ExceptionInInitializerError(e);
        }
    }
    /**
     * 使用StorageClient对象,实现文件上传
     */
    public String[] uploadFile(byte[] datas, String fileName, String authName) {
        try {
            // 获取文件的拓展名
            String extName = fileName.substring(fileName.lastIndexOf(".") + 1);
            // 创建文件拓展信息, 包括文件的原始名称,文件的大小,文件的上传者姓名
            NameValuePair[] metaDatas = new NameValuePair[3];
            metaDatas[0] = new NameValuePair("fileName", fileName);
            metaDatas[1] = new NameValuePair("fileSize", datas.length + "");
            metaDatas[2] = new NameValuePair("auth", authName);
            /**
             * 文件上传
             * 获取文件的拓展名
             * String[] storageClient.upload_file(byte[], String extName, NameValuePair[] metaDatas)
             * datas - 要上传的文件的内容
             * extName - 上传的文件的拓展名
             * metaDatas - 上传文件的拓展信息是什么 如:文件的原始名称、文件的容量大小、文件的上传者等等。
             */
            String[] result = storageClient.upload_file(datas, extName, metaDatas);
            // 上传成功 无异常 返回字符串数组
            // 字符串数组长度为2    0下标位置   卷名|组名   1下标位置是文件名(目录/文件)
            // fdfs为了保证上传的文件原始名称冲突内容不冲突的情况而覆盖的问题,存储文件的时候,会提供一个uuid文件名称。
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            return null;    // 异常发生 返回null 代表上传失败
        }
    }
    /**
     * 文件下载
     * @Param 传入一个数组,用于保存下载文件的扩展信息,如果传入null,则不需要文件拓展信息。如果不是null,则需要文件的拓展信息
     * @return下载的文件内容
     */
    public byte[] downloadFile(String groupName, String fileName, NameValuePair[] metaDatas) {
        try {
            /**
             * byte[] storageClient.download_file(String groupName, String fileName)
             * groupName    -   卷名 | 组名
             * fileName     -   文件名,是文件保存在
             */
            byte[] datas =  storageClient.download_file(groupName, fileName);
            // 要下载的文件的扩展信息
            if(metaDatas != null) {
                // 一个引用值,外部变量指向的是相同的数据
                NameValuePair[] tmp = storageClient.get_metadata(groupName, fileName);
                // 把查询到的文件拓展信息保存到传入的数组中
                for(int i = 0;  i < tmp.length; i++) {
                    metaDatas[i] = tmp[i];
                }
            }
            // 返回下载的文件内容
            return datas;
        } catch (Exception e) {
            e.printStackTrace();
            return null;    //下载失败,返回Null
        }
    }
    /**
     * 文件删除,FastDFS会自动删除这个文件对应的metaData
     */
    public boolean deleteFile(String groupName, String fileName) {
        try {
            int result =  storageClient.delete_file(groupName, fileName);
            // 返回结果为0代表删除成功,其他是删除失败。
            return result == 0;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

然后实现一个工具类,

/**
 * FastDFS客户端工具类型需要使用的自动装配对象
 * 用来创建工具类型的对象
 */
@Configuration
public class FastDFSAutoConfigure {
    /**
     * 依托SpringBoot配置文件特性,读取配置文件中配置的信息
     */
    @Value("${fdfs.connect-timeout}")
    private String connectTimeout;
    @Value("${fdfs.network-timeout}")
    private String networkTimeout;
    @Value("${fdfs.charset}")
    private String charSet;
    @Value("${fdfs.http.tracker_http_port}")
    private String trackerHttpPort;
    @Value("${fdfs.tracker_server}")
    private String trackerServer;

    /**
     * 创建工具类型的对象
     * @return
     */
    @Bean
    public FastDFSUtils fastDFSUtils() {
        // 根据SpringBoot配置文件,创建一个Properties对象
        Properties properties = new Properties();
        properties.setProperty("fastdfs.connect_timeout_in_seconds", connectTimeout);
        properties.setProperty("fastdfs.network_timeout_in_seconds", networkTimeout);
        properties.setProperty("fastdfs.charset", charSet);
        properties.setProperty("fastdfs.http_tracker_http_port", trackerHttpPort);
        properties.setProperty("fastdfs.tracker_servers", trackerServer);
        FastDFSUtils fastDFSUtils = new FastDFSUtils(properties);
        return fastDFSUtils;
    }
}

10.5.1 查看图片

首先写一个逻辑类20_fdfs/img_manager/img_service_consumer/src/main/java/com/bjsxt/img/controller/ImageController.java:

@GetMapping(value = {"/", "/index"})
public String toIndex(@RequestParam(value = "page", defaultValue = "1") int page,
                      @RequestParam(value = "rows", defaultValue = "5") int rows,
                      Model model) {
    // 调用服务逻辑,分页查询
    Map<String, Object> result = this.imageService.getImages(page, rows);
    // 查询结果使用请求作用域传递给页面
    model.addAttribute("datas", result);
    return "index";
}

用service类来实现20_fdfs/img_manager/img_service_consumer/src/main/java/com/bjsxt/img/service/impl/ImageServiceImpl.java:

@Service
public class ImageServiceImpl implements ImageService {
    // 注入远程代理对象
    @Reference
    private ImageServiceAPI imageServiceAPI;

    @Override
    public int save(Image image) {
        return 0;
    }

    @Override
    public int remove(Long id) {
        return 0;
    }


    public Image getById(Long id) {
        return imageServiceAPI.getById(id);
    }

    @Override
    public Map<String, Object> getImages(int page, int rows) {
        return imageServiceAPI.getImages(page, rows);
    }
}

前端代码:

<head>
    <meta charset="UTF-8">
    <title>图片管理-分页查看</title>
</head>
<body>

<div style="width: 800px; margin: auto; padding-top: 50px; text-align: center">
    <a href="/toUpload">上传图片</a>
</div>
    <div style="width: 800px; margin: auto; padding-top: 50px; text-align: center">
        <table border="1">
            <caption>图片列表</caption>
            <thead>
            <tr>
                <th>序号</th>
                <th>卷名</th>
                <th>文件原始名</th>
                <th>上传时间</th>
                <th>预览</th>
                <th>操作</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="imageObj : ${datas.rows}">
                <th th:text="${imageObj.id}">序号</th>
                <th th:text="${imageObj.groupName}">卷名</th>
                <th th:text="${imageObj.originFileName}">文件原始名</th>
                <th th:text="${#dates.format(imageObj.uploadTime, 'yyyy-MM-dd HH:mm:ss')}">上传时间</th>
                <th><img style="height: 50px;" th:src="@{'http://192.168.2.129:8888/' + ${imageObj.url}}"></th>
                <th>
                    <a>下载</a>
                    <a>删除</a>
                </th>
            </tr>
            <tr>
                <th colspan="6">
                    <span th:text="${#strings.concat('总计', datas.total, '条')}"></span>
                    &nbsp;&nbsp;
                    <a th:if="${datas.currentPage} !=1" th:href="@{/index(page=datas.currentPage-1,rows=datas.size)}">上一页</a>
                    <a th:if="${datas.currentPage} != datas.pages" th:href="@{/index(page=datas.currentPage+1,rows=datas.size)}">上一页</a>
                    <a>下一页</a>
                    <span th:text="${#strings.concat('总计', datas.pages, '页')}"></span>
                </th>
            </tr>
            </tbody><img style="height: 50px;" src="http://192.168.2.129:8888/group1/M00/00/00/wKgCgWJBMduASUGRAAuBRYY3e1M933.png">
        </table>
    </div>
</body>

10.5.2 上传图片

有点不太明白业务逻辑,所以没做

10.5.3 删除图片

@GetMapping("/remove")
public String remove(Long id) {
    // 从数据库查询id对应的Image对象
    Image image = imageService.getById(id);
    int rows = imageService.remove(id);    // 删除数据库中的数据
    if(rows == 1) {
        // 删除数据成功,需要从FastDFS中删除对应的图片
        fastDFSUtils.deleteFile(image.getGroupName(), image.getRemoteFileName());
    }
    return "redirect:/index?page=1&rows=5";
}

前端代码:

<th>
  <a th:href="@{/download(id=${imageObj.id)}">下载</a>
  <a th:href="@{/remove(id=${imageObj.id})}">删除</a>
</th>

10.5.4 下载图片

前端代码:

<th>
    <a th:href="@{/download(id=${imageObj.id)}">下载</a>
    <a th:href="@{/remove(id=${imageObj.id})}">删除</a>
</th>      

下载:

/**
 * 下载图片
 * 1. 从数据库查询id对应的数据
 * 2. 根据查询结果到FastDFS中招文件数据
 * 3. 文件下载
 */
@GetMapping("/download")
public void download(Long id, HttpServletResponse httpServletResponse) {
    try {
        // 查询数据
        Image images = imageService.getById(id);
        // 从FastDFS中查询要下载的文件内容
        NameValuePair[] metaDatas = new NameValuePair[3];
        byte[] datas =  fastDFSUtils.downloadFile(images.getGroupName(), images.getRemoteFileName(),metaDatas);
        // 使用响应输出流向客户端输出文件内容并提示下载
        // 设置响应头为流输出
        httpServletResponse.setContentType("application/octet-stream");
        // 获取文件的原始名称
        String fileName = "";
        for(NameValuePair nvp: metaDatas) {
            if(nvp.getName().equals("fileName")) {
                fileName = nvp.getValue();

            }
        }
        // 编码处理,避免响应头设置的中文出现乱码
        fileName = URLEncoder.encode(fileName, "UTF-8");
        // 设置响应头,并标记附件文件名为fileName
        httpServletResponse.setHeader("content-disposition", "attachment;filename="+fileName);
        // 输出文件内容到客户端
        httpServletResponse.getOutputStream().write(datas);
        // 刷新输出缓冲流
        httpServletResponse.getOutputStream().flush();
    } catch (Exception e) {
        e.printStackTrace();
        return;
    }
}
posted @ 2022-03-25 14:52  明卿册  阅读(59)  评论(0)    收藏  举报