目录

一、项目搭建

1.1 创建module

1.2 添加依赖

1.3 编写配置

1.4 启动类

二、图片上传

2.1 后台接口

2.1.1 Controller

2.1.2 Service

2.1.3 测试

2.2 绕过网关

2.2.1 Zuul的路由过滤

2.2.2 Nginx的rewrite指令

2.3 跨域问题

2.4 目前存在的问题

三、FastDFS

3.1 分布式文件系统介绍

3.2 什么是FastDFS

3.3 FastDFS的架构

3.3.1 架构图

3.3.2 上传和下载流程

3.4 FastDFS的安装

3.5 Java客户端

3.5.1 引入依赖

3.5.3 编写FastDFS属性

3.5.4 测试

3.5.5 修改上传逻辑

3.5.6 域名解析

3.5.7 最终效果


上传微服务

文件的上传并不只是在品牌管理中有需求,以后的其它服务也可能需要,因此需要创建一个独立的微服务,专门处理各种上传。

一、项目搭建

1.1 创建module

1.2 添加依赖

需要EurekaClient和web依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.service</groupId>
    <artifactId>ly-upload</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

1.3 编写配置

server:
  port: 8082
spring:
  application:
    name: upload-service
  servlet:
    multipart:
      max-file-size: 5MB # 限制文件上传的大小
# Eureka
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
    prefer-ip-address: true
    ip-address: 127.0.0.1
    instance-id: ${spring.application.name}:${server.port}

需要注意的是,应该添加限制文件大小的配置 。

1.4 启动类

@SpringBootApplication
@EnableDiscoveryClient
public class LyUploadService {
    public static void main(String[] args) {
        SpringApplication.run(LyUploadService.class, args);
    }
}

二、图片上传

2.1 后台接口

2.1.1 Controller

编写controller需要知道4个内容:

  • 请求方式:上传肯定是POST

  • 请求路径:/upload/image

  • 请求参数:文件,参数名是file,SpringMVC会封装为一个接口:MultipleFile

  • 返回结果:上传成功后得到的文件的url路径

/**
 * @Author: 98050
 * Time: 2018-08-09 14:36
 * Feature:
 */
@RestController
@RequestMapping("upload")
public class UploadController {
    @Autowired
    private UploadServiceImpl uploadServiceImpl;

    @PostMapping("image")
    public ResponseEntity<String> uploadImage(@RequestParam("file")MultipartFile file){
        String url= this.uploadServiceImpl.upload(file);
        if(StringUtils.isBlank(url)){
            //url为空,证明上传失败
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
        return ResponseEntity.ok(url);
    }
}

2.1.2 Service

在上传文件过程中,我们需要对上传的内容进行校验:

  1. 校验文件大小

  2. 校验文件的媒体类型

  3. 校验文件的内容

文件大小在Spring的配置文件中设置,已经被校验过了。

接口

/**
 * @Author: 98050
 * Time: 2018-08-09 14:44
 * Feature:
 */
public interface UploadService {
    /**
     * 文件上传
     * @param file
     * @return
     */
    String upload(MultipartFile file);
}

实现类

/**
 * @Author: 98050
 * Time: 2018-08-09 14:44
 * Feature:
 */
@Service
public class UploadServiceImpl implements UploadService {

    private static final Logger logger = LoggerFactory.getLogger(UploadController.class);

    // 支持的文件类型
    private static final List<String> suffixes = Arrays.asList("image/png", "image/jpeg");

    public String upload(MultipartFile file) {
        try {
            // 1、图片信息校验
            // 1)校验文件类型
            String type = file.getContentType();
            if (!suffixes.contains(type)) {
                logger.info("上传失败,文件类型不匹配:{}", type);
                return null;
            }
            // 2)校验图片内容
            BufferedImage image = ImageIO.read(file.getInputStream());
            if (image == null) {
                logger.info("上传失败,文件内容不符合要求");
                return null;
            }
            // 2、保存图片
            // 2.1、生成保存目录
            File dir = new File("G:\\LeYou\\upload");
            if (!dir.exists()) {
                dir.mkdirs();
            }
            // 2.2、保存图片
            file.transferTo(new File(dir, file.getOriginalFilename()));

            // 2.3、拼接图片地址
            String url = "http://image.leyou.com/upload/" + file.getOriginalFilename();

            return url;
        } catch (Exception e) {
            return null;
        }
    }
}

这里有一个问题:为什么图片地址需要使用另外的url?

  • 图片不能保存在服务器内部,这样会对服务器产生额外的加载负担

  • 一般静态资源都应该使用独立域名,这样访问静态资源时不会携带一些不必要的cookie,减小请求的数据量

2.1.3 测试

使用Postman发送请求

结果:

2.2 绕过网关

图片上传是文件的传输,如果也经过Zuul网关的代理,文件就会经过多次网路传输,造成不必要的网络负担。在高并发时,可能导致网络阻塞,Zuul网关不可用。这样我们的整个系统就瘫痪了。所以,我们上传文件的请求就不经过网关来处理了。

2.2.1 Zuul的路由过滤

  • Zuul中提供了一个ignored-patterns属性,用来忽略不希望路由的URL路径,示例: zuul.ignored-patterns: /upload/**
  • 路径过滤会对一切微服务进行判定。 Zuul还提供了ignored-services属性,进行服务过滤: zuul.ignored-services: upload-servie

这里采用直接忽略服务的方法:

zuul:
  ignored-services:
    - upload-service # 忽略upload-service服务

2.2.2 Nginx的rewrite指令

此时,上传功能还是有问题的,因为前端页面所有的请求路径都是走的网关http://api.leyou.com,而且原则上不能把除了网关以为的服务对外暴露,所以需要对nginx的配置进行修改。让上传图片功能假装经过网关(因为请求地址为http://api.leyou.com/api/upload/image),但是在nginx中进行过滤,把这个请求直接代理到上传微服务端口,而不经过网关端口(商品管理微服务先经过网关,然后网关再去调用相应的微服务)。

修改nginx的配置,将以/api/upload开头的请求拦截下来,转交到真实的服务地址:

    server {
        listen       80;
        server_name  api.leyou.com;

        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        location /api/upload {
			proxy_pass http://127.0.0.1:8082;
			proxy_connect_timeout 1000;
			proxy_read_timeout 1000;
        }

        location / {
			proxy_pass http://127.0.0.1:10010;
			proxy_connect_timeout 1000;
			proxy_read_timeout 1000;
        }
    }

但是,还有问题。因为现在的请求路径是http://127.0.0.1:8002/api/upload/image,它比正确的请求路径多了一个/api。所以需要使用nginx提供的rewrite指令,对地址进行重写。

    server {
        listen       80;
        server_name  api.leyou.com;

        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        location /api/upload {
			proxy_pass http://127.0.0.1:8082;
			proxy_connect_timeout 1000;
			proxy_read_timeout 1000;
            rewrite "^/api/(.*)$" /$1 break; 
        }

        location / {
			proxy_pass http://127.0.0.1:10010;
			proxy_connect_timeout 1000;
			proxy_read_timeout 1000;
        }
    }

解析

  • 首先,映射路径是/api/upload,而下面一个映射路径是 / ,根据最长路径匹配原则,/api/upload优先级更高。也就是说,凡是以/api/upload开头的路径,都会被第一个配置处理

  • proxy_pass:反向代理,这次代理到8082端口,也就是upload-service服务

  • rewrite "^/api/(.*)$" /$1 break,路径重写:

    • "^/api/(.*)$":匹配路径的正则表达式,用了分组语法,把/api/以后的所有部分当做1组

    • /$1:重写的目标路径,这里用$1引用前面正则表达式匹配到的分组(组编号从1开始),即/api/后面的所有。这样新的路径就是除去/api/以外的所有,就达到了去除/api前缀的目的

    • break:指令,常用的有2个,分别是:last、break

      这里不能选择last,否则以新的路径/upload/image来匹配,就不会被正确的匹配到8082端口了

      • last:重写路径结束后,将得到的路径重新进行一次路径匹配

      • break:重写路径结束后,不再重新匹配路径。

2.3 跨域问题

经过上面修改,最后还会报错:

跨域问题。所以需要在upload-service中添加一个CorsFilter,因为网关中配置了CorsFilter,但是上传微服务并没有走网关,所以需要重新配。

package com.leyou.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * @author li
 * @time:2018/8/9
 * 处理跨域请求的过滤器
 */
@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        //1.添加CORS配置信息
        CorsConfiguration config = new CorsConfiguration();
        //1) 允许的域,不要写*,否则cookie就无法使用了
        config.addAllowedOrigin("http://manage.leyou.com");
        //2) 是否发送Cookie信息
        config.setAllowCredentials(false);
        //3) 允许的请求方式
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("POST");
        config.addAllowedHeader("*");

        //2.添加映射路径,我们拦截一切请求
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);

        //3.返回新的CorsFilter.
        return new CorsFilter(configSource);
    }
}

最终测试:

但是,图片却显示不出来,访问图片地址没有响应。

这是因为并没有任何服务器对应image.leyou.com这个域名。 

2.4 目前存在的问题

上传本身没有任何问题,问题出在保存文件的方式,现在是保存在服务器机器,就会存在下面的问题:

  • 单机器存储,存储能力有限

  • 无法进行水平扩展,因为多台机器的文件无法共享,会出现访问不到的情况

  • 数据没有备份,有单点故障风险

  • 并发能力差

这个时候,最好使用分布式文件存储来代替本地文件存储。

三、FastDFS

3.1 分布式文件系统介绍

分布式文件系统(Distributed File System)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连。

通俗来讲:

  • 传统文件系统管理的文件就存储在本机。

  • 分布式文件系统管理的文件存储在很多机器,这些机器通过网络连接,要被统一管理。无论是上传或者访问文件,都需要通过管理中心来访问

3.2 什么是FastDFS

FastDFS是由淘宝的余庆先生所开发的一个轻量级、高性能的开源分布式文件系统。用纯C语言开发,功能丰富:

  • 文件存储

  • 文件同步

  • 文件访问(上传、下载)

  • 存取负载均衡

  • 在线扩容

适合有大容量存储需求的应用或系统。同类的分布式文件系统有谷歌的GFS、HDFS(Hadoop)、TFS(淘宝)等。

3.3 FastDFS的架构

3.3.1 架构图

FastDFS两个主要的角色:Tracker Server 和 Storage Server 。

  • Tracker Server:跟踪服务器,主要负责调度storage节点与client通信,在访问上起负载均衡的作用,和记录storage节点的运行状态,是连接client和storage节点的枢纽。

  • Storage Server:存储服务器,保存文件和文件的meta data(元数据),每个storage server会启动一个单独的线程主动向Tracker cluster中每个tracker server报告其状态信息,包括磁盘使用情况,文件同步情况及文件上传下载次数统计等信息

  • Group:文件组,多台Storage Server的集群。上传一个文件到同组内的一台机器上后,FastDFS会将该文件即时同步到同组内的其它所有机器上,起到备份的作用。不同组的服务器,保存的数据不同,而且相互独立,不进行通信。

  • Tracker Cluster:跟踪服务器的集群,有一组Tracker Server(跟踪服务器)组成。

  • Storage Cluster :存储集群,有多个Group组成。

3.3.2 上传和下载流程

上传

  1. Client通过Tracker server查找可用的Storage server。

  2. Tracker server向Client返回一台可用的Storage server的IP地址和端口号。

  3. Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并进行文件上传。

  4. 上传完成,Storage server返回Client一个文件ID,文件上传结束。

下载:

  1. Client通过Tracker server查找要下载文件所在的的Storage server。

  2. Tracker server向Client返回包含指定文件的某个Storage server的IP地址和端口号。

  3. Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并指定要下载文件。

  4. 下载文件成功。

3.4 FastDFS的安装

请参考《Centos安装FastDFS》

3.5 Java客户端

这里推荐一个开源的FastDFS客户端,支持最新的SpringBoot2.0。

3.5.1 引入依赖

在父工程中,我们已经管理了依赖,版本为:

<fastDFS.client.version>1.26.2</fastDFS.client.version>

因此,这里我们直接引入坐标即可:

<dependency>
    <groupId>com.github.tobato</groupId>
    <artifactId>fastdfs-client</artifactId>
</dependency>

3.5.2 引入配置类

纯java配置

package com.leyou.config;

import com.github.tobato.fastdfs.FdfsClientConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.annotation.Import;
import org.springframework.jmx.support.RegistrationPolicy;

@Configuration
@Import(FdfsClientConfig.class)
/**
 * @author li
 * 解决jmx重复注册bean的问题
  */
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {
}

3.5.3 编写FastDFS属性

3.5.4 测试

package com.leyou.test;

import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.domain.ThumbImageConfig;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.leyou.LyUploadService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

@RunWith(SpringRunner.class)
@SpringBootTest(classes = LyUploadService.class)
public class FdfsTest {

    @Autowired
    private FastFileStorageClient storageClient;

    @Autowired
    private ThumbImageConfig thumbImageConfig;

    @Test
    public void testUpload() throws FileNotFoundException {
        File file = new File("G:\\LeYou\\upload\\spitter_logo_50.png");
        // 上传
        StorePath storePath = this.storageClient.uploadFile(
                new FileInputStream(file), file.length(), "png", null);
        // 带分组的路径
        System.out.println(storePath.getFullPath());
        // 不带分组的路径
        System.out.println(storePath.getPath());
    }

    @Test
    public void testUploadAndCreateThumb() throws FileNotFoundException {
        File file = new File("G:\\LeYou\\upload\\spitter_logo_50.png");
        // 上传并且生成缩略图
        StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(
                new FileInputStream(file), file.length(), "png", null);
        // 带分组的路径
        System.out.println(storePath.getFullPath());
        // 不带分组的路径
        System.out.println(storePath.getPath());
        // 获取缩略图路径
        String path = thumbImageConfig.getThumbImagePath(storePath.getPath());
        System.out.println(path);
    }
}

结果:

访问第一个路径:

访问最后一个路径(缩略图路径),加组名:

3.5.5 修改上传逻辑

package com.leyou.upload.service.serviceimpl;

import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.leyou.upload.service.UploadService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * @Author: 98050
 * Time: 2018-08-09 14:44
 * Feature:
 */
@Service
public class UploadServiceImpl implements UploadService {

    @Autowired
    private FastFileStorageClient storageClient;

    private static final Logger logger= LoggerFactory.getLogger(UploadServiceImpl.class);

    /**
     *     支持上传的文件类型
     */
    private static final List<String> suffixes = Arrays.asList("image/png","image/jpeg","image/jpg");


    @Override
    public String upload(MultipartFile file) {
        /**
         * 1.图片信息校验
         *      1)校验文件类型
         *      2)校验图片内容
         * 2.保存图片
         *      1)生成保存目录
         *      2)保存图片
         *      3)拼接图片地址
         */
        try {
            String type = file.getContentType();
            if (!suffixes.contains(type)) {
                logger.info("上传文件失败,文件类型不匹配:{}", type);
                return null;
            }
            BufferedImage image = ImageIO.read(file.getInputStream());
            if (image == null) {
                logger.info("上传失败,文件内容不符合要求");
                return null;
            }

            StorePath storePath = this.storageClient.uploadFile(
                  file.getInputStream(), file.getSize(), getExtension(file.getOriginalFilename()), null);
       
            String url = "http://image.leyou.com/"+storePath.getFullPath();
            return url;
        }catch (Exception e){
            return null;
        }
    }

    public String getExtension(String fileName){
        return StringUtils.substringAfterLast(fileName,".");
    }
}

3.5.6 域名解析

因为图片上传到的虚拟机地址是192.168.19.121,而image.leyou.com现在还无法解析到虚拟机地址,所以需要在本地hosts文件中添加一条:

3.5.7 最终效果

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐