Spring Boot 返回Content-Type解決方案

背景

前端同學需要Content-Type 字段返回,根據文件的類型不同返回不同的類型;還有就是直接打開一個下載鏈接,對於Chrome這樣的瀏覽器其實支持自適應預覽的效果。https://tool.oschina.net/commons/ 這裏的鏈接中有好多好多的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 https://tool.oschina.net/commons/
     * 訪問路徑 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);
        }
    }


}

效果

image.png

總結

遇到問題、多思考、多想想解決方案,代碼更加簡單。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章