上传微服务:
图片上传属于文件上传的一种。文件的上传并不只是在品牌管理中有需求,以后的其它服务也可能需要,因此我们创建一个独立的微服务,专门处理各种上传。
1. 搭建项目
1.1 引入依赖
我们需要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.page.service</groupId>
<artifactId>ly-upload</artifactId>
<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>
<!--引入ly-common后便会用到-->
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>ly-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
</dependency>
</dependencies>
</project>
1.2 配置
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:
ip-address: 127.0.0.1
prefer-ip-address: true
fdfs:
so-timeout: 1501
connect-timeout: 601
thumb-image: # 缩略图
width: 60
height: 60
tracker-list: # tracker地址
- 192.168.184.130:22122
ly:
upload:
baseUrl: http://image.leyou.com/
allowTypes:
- image/png
- image/bmp
- image/jpeg
1.3 启动类
@SpringBootApplication
@EnableDiscoveryClient
public class LyUploadService {
public static void main(String[] args) {
SpringApplication.run(LyUploadService.class, args);
}
}
2. 业务
2.1 web
2.1.1 页面分析
- 请求方式:上传肯定是POST
- 请求路径:/upload/image
- 请求参数:文件,参数名是file,SpringMVC会封装为一个接口:MultipleFile
- 返回结果:上传成功后得到的文件的url路径
2.1.2 实现业务
@RestController
@RequestMapping("upload")
public class UploadController {
@Autowired
private UploadService uploadService;
// 图片上传
@PostMapping("image")
public ResponseEntity<String> uploadImage(@RequestParam("file")MultipartFile file){
return ResponseEntity.ok(uploadService.uploadImage(file));
}
}
2.2 service
在上传文件过程中,我们需要对上传的内容进行校验:
- 校验文件大小
- 校验文件的媒体类型
- 校验文件的内容
文件大小在Spring的配置文件中设置,因此已经会被校验,我们不用管。
@Service
public class 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("D:\\heima\\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;
}
}
}
2.3 忽略路由前缀
注:
Zuul的路由功能中,会忽略路由匹配的路径前缀,不过我们的Controller中有 /upload 路径,此时如果通过网关访问,我们的地址应该是http://api.leyou.com/api/upload/upload/image, 这是因为路由匹配的前缀 /upload 在请求转发时会被自动忽略。
这样地址看起来非常臃肿,因此我们可以禁止忽略路由前缀,
在网关中application.yml中配置:
这样,路由前缀也会作为地址一部分转发到微服务,那么我们就可以http://api.leyou.com/api/upload/image 正常访问了
2.4 绕过网关缓存
默认情况下,所有的请求都经过Zuul网关的代理.
Zuul底层就是一个servlet,通常情况下zuul会将请求交给Spring Dispatch去处理,SpringMVC去控制路由。这种情况下,zuul就会去缓存这个请求。如果有直接通过zuul但是不需要缓存:比如大文件的上传服务,这时候就应该跳过SpringDispatch,在地址前加一个 /zuul.
普通请求并不会有什么影响,但是对于图片上传(文件传输),如果也经过Zuul网关的代理,文件就会经过多次网路传输,造成不必要的网络负担。在高并发时,可能导致网络阻塞,Zuul网关不可用,这样我们的整个系统就瘫痪了。
所以,我们上传文件的请求需要绕过请求的缓存,直接通过路由到达目标微服务。
页面请求路径:
由于不能修改页面请求地址,我们需要修改到以 /zuul为前缀(在 /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;
proxy_set_header Host $host;
// 配置
location /api/upload {
rewrite "^/(.*)$" /zuul/$1;
}
location / {
proxy_pass http://192.168.1.109:10010;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
2.5 之前上传的缺陷
之前,我们把文件保存在服务器机器,就会有下面的问题:
- 单机器存储,存储能力有限
- 无法进行水平扩展,因为多台机器的文件无法共享,会出现访问不到的情况
- 数据没有备份,有单点故障风险
- 并发能力差
这个时候,最好使用分布式文件存储来代替本地文件存储。
3. FastDFS
3.1 FastDFS介绍
分布式文件系统:FastDFS
3.2 java客户端
3.1.1 引入依赖
在ly-upload的pom文件中添加:
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
</dependency>
3.1.2 引入配置类
@Configuration
@Import(FdfsClientConfig.class)
// 解决jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {
}
3.1.3 编写FastDFS属性
在ly-upload的 application.yml 文件中添加:
3.3 改造上传逻辑
只需要把原来保存文件的逻辑去掉,然后上传到FastDFS即可。
@Service
@Slf4j
@EnableConfigurationProperties(UploadProperties.class)
public class UploadService {
@Autowired
private FastFileStorageClient storageClient;
@Autowired
private UploadProperties prop;
public String uploadImage(MultipartFile file) {
try {
//校验文件类型
String contentType = file.getContentType();
if(!prop.getAllowTypes().contains(contentType)){
throw new LyException(ExceptionEnum.INVALID_FILE_TYPE);
}
//校验文件内容
BufferedImage image = ImageIO.read(file.getInputStream());
if(image==null)
throw new LyException(ExceptionEnum.INVALID_FILE_TYPE);
//上传到FastDFS
String extension = StringUtils.substringAfterLast(file.getOriginalFilename(),".");
StorePath storePath = storageClient.uploadFile(file.getInputStream(), file.getSize(), extension, null);
//返回路径
return prop.getBaseUrl() + storePath.getFullPath();
} catch (IOException e) {
log.error("[文件上传] 上传文件失败",e);
throw new LyException(ExceptionEnum.UPLOAD_FILE_ERROR);
}
}
}
在上述中,我们对文件的返回路径进行了抽取,如下:
同时,应当在ly-upload的 application.yml文件中添加:
3.4 导入图片到虚拟机
之前我们已经在数据库导入了数百条品牌及商品数据,不过这些数据中所需要的图片是无法访问的,需要我们把图片放到虚拟机对应的nginx服务器。(上传到Linux的 /leyou/static目录
然后修改linux中的nginx配置:)
vim /opt/nginx/config/nginx.conf
将以image开头的地址代理到本地路径:
server {
listen 80;
server_name image.taotao.com;
# 监听域名中带有group的,交给FastDFS模块处理
location ~/group([0-9])/ {
ngx_fastdfs_module;
}
# 其它图片都走本地目录
location /images/ {
root /leyou/static;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}