乐优商城(六)搭建上传微服务
目录一、项目搭建1.1 创建module1.2 添加依赖1.3 编写配置1.4 启动类二、图片上传2.1 后台接口2.1.1 Controller2.1.2 Service2.1.3 测试2.2 绕过网关2.2.1 Zuul的路由过滤2.2.2 Nginx的rewrite指令2.3 跨域问题2.4 目前存在的问题三、FastDFS...
目录
上传微服务
文件的上传并不只是在品牌管理中有需求,以后的其它服务也可能需要,因此需要创建一个独立的微服务,专门处理各种上传。
一、项目搭建
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
在上传文件过程中,我们需要对上传的内容进行校验:
-
校验文件大小
-
校验文件的媒体类型
-
校验文件的内容
文件大小在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 上传和下载流程
上传:
-
Client通过Tracker server查找可用的Storage server。
-
Tracker server向Client返回一台可用的Storage server的IP地址和端口号。
-
Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并进行文件上传。
-
上传完成,Storage server返回Client一个文件ID,文件上传结束。
下载:
-
Client通过Tracker server查找要下载文件所在的的Storage server。
-
Tracker server向Client返回包含指定文件的某个Storage server的IP地址和端口号。
-
Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并指定要下载文件。
-
下载文件成功。
3.4 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 最终效果
更多推荐
所有评论(0)