背景

前端同学需要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 &lt;mime type> &lt;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);
        }
    }


}

效果

Spring Boot 返回Content-Type解决方案_java

总结

遇到问题、多思考、多想想解决方案,代码更加简单。