背景
前端同学需要Content-Type 字段返回,根据文件的类型不同返回不同的类型;还有就是直接打开一个下载链接,对于Chrome这样的浏览器其实支持自适应预览的效果。javascript:void(0) 这里的链接中有好多好多的Content-type的类型!拿到这个问题,之前的伙伴有写了几个if else 判断图片、文档等等,但是支持不是很全面、谁知道之后还有什么样的格式类型返回呢?if else 不能从根本上解决问题。ps:作为程序员我也不想这么累,这种肯定有一种解决方案存在的。
解决方案
RestTemplate获取文件的contentType 这篇文章给了我大量的信息,作为spring 大佬这种解决方案是非常的多的,org/springframework/http/converter/ActivationMediaTypeFactory.java 这个类中详细的从读取文件,到文件名对应的Content-type的处理,这个类在spring boot 2.0+以上就不存在了。
spring-web-5.2.3.RELEASE.jar!/org/springframework/http/mime.types
最后的是文件后缀、前面是Content-Type
video/webm webm
video/x-f4v f4v
video/x-fli fli
video/x-flv flv
video/x-m4v m4v
video/x-matroska mkv mk3d mks
video/x-mng mng
video/x-ms-asf asf asx
video/x-ms-vob vob
video/x-ms-wm wm
video/x-ms-wmv wmv
video/x-ms-wmx wmx
video/x-ms-wvx wvx
video/x-msvideo avi
video/x-sgi-movie movie
video/x-smv smv
x-conference/x-cooltalk ice
spring-web 5.0+
org.springframework.http.MediaTypeFactory ,实现思路简单,相比 4.0+的版本更加的清晰,可以独立的使用
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* A factory delegate for resolving {@link MediaType} objects
* from {@link Resource} handles or filenames.
*
* @author Juergen Hoeller
* @author Arjen Poutsma
* @since 5.0
*/
public final class MediaTypeFactory {
private static final String MIME_TYPES_FILE_NAME = "/org/springframework/http/mime.types";
private static final MultiValueMap<String, MediaType> fileExtensionToMediaTypes = parseMimeTypes();
private MediaTypeFactory() {
}
/**
* Parse the {@code mime.types} file found in the resources. Format is:
* <code>
* # comments begin with a '#'<br>
* # the format is <mime type> <space separated file extensions><br>
* # for example:<br>
* text/plain txt text<br>
* # this would map file.txt and file.text to<br>
* # the mime type "text/plain"<br>
* </code>
* @return a multi-value map, mapping media types to file extensions.
*/
private static MultiValueMap<String, MediaType> parseMimeTypes() {
InputStream is = MediaTypeFactory.class.getResourceAsStream(MIME_TYPES_FILE_NAME);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.US_ASCII))) {
MultiValueMap<String, MediaType> result = new LinkedMultiValueMap<>();
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty() || line.charAt(0) == '#') {
continue;
}
String[] tokens = StringUtils.tokenizeToStringArray(line, " \t\n\r\f");
MediaType mediaType = MediaType.parseMediaType(tokens[0]);
for (int i = 1; i < tokens.length; i++) {
String fileExtension = tokens[i].toLowerCase(Locale.ENGLISH);
result.add(fileExtension, mediaType);
}
}
return result;
}
catch (IOException ex) {
throw new IllegalStateException("Could not load '" + MIME_TYPES_FILE_NAME + "'", ex);
}
}
/**
* Determine a media type for the given resource, if possible.
* @param resource the resource to introspect
* @return the corresponding media type, or {@code null} if none found
*/
public static Optional<MediaType> getMediaType(@Nullable Resource resource) {
return Optional.ofNullable(resource)
.map(Resource::getFilename)
.flatMap(MediaTypeFactory::getMediaType);
}
/**
* Determine a media type for the given file name, if possible.
* @param filename the file name plus extension
* @return the corresponding media type, or {@code null} if none found
*/
public static Optional<MediaType> getMediaType(@Nullable String filename) {
return getMediaTypes(filename).stream().findFirst();
}
/**
* Determine the media types for the given file name, if possible.
* @param filename the file name plus extension
* @return the corresponding media types, or an empty list if none found
*/
public static List<MediaType> getMediaTypes(@Nullable String filename) {
return Optional.ofNullable(StringUtils.getFilenameExtension(filename))
.map(s -> s.toLowerCase(Locale.ENGLISH))
.map(fileExtensionToMediaTypes::get)
.orElse(Collections.emptyList());
}
}
阿里云oss sdk aliyun-sdk-oss-3.8.1
在搜索mime.types 恰好搜索到了oss sdk 中的这个比较的丰富完整,作为oss 处理这里的content-type非常的丰富。
/aliyun-sdk-oss-3.8.1.jar!/mime.types
#Extentions MIME type
xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xltx application/vnd.openxmlformats-officedocument.spreadsheetml.template
potx application/vnd.openxmlformats-officedocument.presentationml.template
ppsx application/vnd.openxmlformats-officedocument.presentationml.slideshow
pptx application/vnd.openxmlformats-officedocument.presentationml.presentation
sldx application/vnd.openxmlformats-officedocument.presentationml.slide
docx application/vnd.openxmlformats-officedocument.wordprocessingml.document
dotx application/vnd.openxmlformats-officedocument.wordprocessingml.template
xlam application/vnd.ms-excel.addin.macroEnabled.12
xlsb application/vnd.ms-excel.sheet.binary.macroEnabled.12
apk application/vnd.android.package-archive
和spring boot 的处理,这里简化了很多。
package com.aliyun.oss.internal;
import static com.aliyun.oss.common.utils.LogUtils.getLog;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.StringTokenizer;
/**
* Utility class used to determine the mimetype of files based on file
* extensions.
*/
public class Mimetypes {
/* The default MIME type */
public static final String DEFAULT_MIMETYPE = "application/octet-stream";
private static Mimetypes mimetypes = null;
private HashMap<String, String> extensionToMimetypeMap = new HashMap<String, String>();
private Mimetypes() {
}
public synchronized static Mimetypes getInstance() {
if (mimetypes != null)
return mimetypes;
mimetypes = new Mimetypes();
InputStream is = mimetypes.getClass().getResourceAsStream("/mime.types");
if (is != null) {
getLog().debug("Loading mime types from file in the classpath: mime.types");
try {
mimetypes.loadMimetypes(is);
} catch (IOException e) {
getLog().error("Failed to load mime types from file in the classpath: mime.types", e);
} finally {
try {
is.close();
} catch (IOException ex) {
}
}
} else {
getLog().warn("Unable to find 'mime.types' file in classpath");
}
return mimetypes;
}
public void loadMimetypes(InputStream is) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line = null;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.startsWith("#") || line.length() == 0) {
// Ignore comments and empty lines.
} else {
StringTokenizer st = new StringTokenizer(line, " \t");
if (st.countTokens() > 1) {
String extension = st.nextToken();
if (st.hasMoreTokens()) {
String mimetype = st.nextToken();
extensionToMimetypeMap.put(extension.toLowerCase(), mimetype);
}
}
}
}
}
public String getMimetype(String fileName) {
String mimeType = getMimetypeByExt(fileName);
if (mimeType != null) {
return mimeType;
}
return DEFAULT_MIMETYPE;
}
public String getMimetype(File file) {
return getMimetype(file.getName());
}
public String getMimetype(File file, String key) {
return getMimetype(file.getName(), key);
}
public String getMimetype(String primaryObject, String secondaryObject) {
String mimeType = getMimetypeByExt(primaryObject);
if (mimeType != null) {
return mimeType;
}
mimeType = getMimetypeByExt(secondaryObject);
if (mimeType != null) {
return mimeType;
}
return DEFAULT_MIMETYPE;
}
private String getMimetypeByExt(String fileName) {
int lastPeriodIndex = fileName.lastIndexOf(".");
if (lastPeriodIndex > 0 && lastPeriodIndex + 1 < fileName.length()) {
String ext = fileName.substring(lastPeriodIndex + 1).toLowerCase();
if (extensionToMimetypeMap.keySet().contains(ext)) {
String mimetype = (String) extensionToMimetypeMap.get(ext);
return mimetype;
}
}
return null;
}
}
实践
maven 依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>20030203.000550</version>
</dependency>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
源码
如果感觉mime.types中的响应类型不够丰富,可以自己添加到classpath 下去全量覆盖的。
package com.wangji92.github.study.controller;
import com.aliyun.oss.internal.Mimetypes;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.io.IOUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 文件操作演示
*
* @author 汪小哥
* @date 27-02-2020
*/
@RestController
@RequestMapping("/api/fileOperation")
@Slf4j
public class FileOperationController {
@Autowired
private ResourceLoader resourceLoader;
@Autowired
private HttpServletRequest httpServletRequest;
@Autowired
private HttpServletResponse httpServletResponse;
/**
* 演示展示详情content type javascript:void(0)
* 访问路径 http://127.0.0.1:8080/api/fileOperation/downLoadFile?writeAttachment=false&fileName=test.mp3
* http://127.0.0.1:8080/api/fileOperation/downLoadFile?writeAttachment=false&fileName=demo.jpg
* @param fileName
* @param writeAttachment
*/
@RequestMapping("/downLoadFile")
public void downLoadByType(@RequestParam(required = false) String fileName, @RequestParam boolean writeAttachment) {
if (StringUtils.isEmpty(fileName)) {
fileName = "demo.jpg";
}
/**
* 阿里云和spring 提供的都可以 阿里云的种类丰富,更多们可以拷贝到classpath 下面从新扩展
* 阿里云 aliyun-sdk-oss-3.8.1.jar!/mime.types
* spring spring-web/5.2.3.RELEASE/spring-web-5.2.3.RELEASE.jar!/org/springframework/http/mime.types
* @see org.springframework.http.converter.ActivationMediaTypeFactory#loadFileTypeMapFromContextSupportModule 这个在spring web 4.3 版本中存在
* @see org.springframework.http.MediaTypeFactory#parseMimeTypes() 这种也是可以获取的,可以支持自己定制 按照格式处理吧
*/
// 使用阿里云提供的处理方式
String contentType = Mimetypes.getInstance().getMimetype(fileName);
log.info("aliyun oss mediaType {}", contentType);
// 使用spring 提供的处理方式
if (MediaTypeFactory.getMediaType(fileName).isPresent()) {
MediaType mediaType = MediaTypeFactory.getMediaType(fileName).get();
log.info("spring mediaType {}", mediaType.toString());
}
httpServletResponse.addHeader(HttpHeaders.CONTENT_TYPE, contentType);
if (writeAttachment) {
httpServletResponse.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName);
}
OutputStream outputStream = null;
InputStream inputStream = null;
try {
outputStream = httpServletResponse.getOutputStream();
Resource resource = resourceLoader.getResource("classpath:" + fileName);
inputStream = resource.getInputStream();
IOUtil.copy(inputStream, httpServletResponse.getOutputStream());
} catch (ClientAbortException e) {
log.info("ClientAbortException ");
} catch (Exception e) {
log.error("fileDownLoad error", e);
} finally {
IOUtil.shutdownStream(outputStream);
IOUtil.shutdownStream(inputStream);
}
}
}
效果
总结
遇到问题、多思考、多想想解决方案,代码更加简单。