文件服务器的搭建架构有很多种,如基于nginx+vsftp、nginx+fastDFS等架构,其中vsftp或fastDFS用于文件读写、上传存储、下载,nginx用于映射文件 ,方便http访问静态文件,实现在线预览图片或下载等功能。这种架构使用起来很方便,而且fastDFS支持集群、主从备份等,因而很采公司或开发人员的青睐,但这种构架是无法满足某些场景的需求,如增加文件访问权限校验、时效等,仅凭nginx无法做到,一般可能需要结合luna针对nginx开发模块,而采用Java、Spring则可以很方便实现这个功能。Spring对于静态文件访问有着较好的支持,并支持多种文件映射,如本地文件、jar、ftp等文件类型,Spring框架本身可解析大部分文件类型。
文件服务器的基本原理就是将文件以流或字节输出到客户端。
Spring静态文件配置
相信熟悉tomcat的同学都知道tomcat可作为静态文件服务器,将文件放入webapps目录下,即可用host+path来访问或下载文件。
SpringMVC框架也支持对于静态文件mapping的方式来实现文件服务器功能,配置有如下两种。
1.xml配置
在主springmvc主配置文件中添加
<mvc:annotation-driven />
<mvc:resources mapping="/images/**" location="/images/" />
/images/**映射到 ResourceHttpRequestHandler进行处理,location指定静态资源的位置.可以是web application根目录下、jar包里面,这样可以把静态资源压缩到jar包中。这样当访问http://host/images/{file_path}
时,则会到/images/
目录下去找相应的文件。
location配置支持系统文件、ftp文件、jar文件,对应的配置为file://
、ftp://
、jar://
,支持Http网络, DFS协议地址, VFS协议地址,jar包,可参考File、FTP等协议说明。
2.springboot配置
public class FileServerConfig extends WebMvcConfigurerAdapter{
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**").addResourceLocations("file://");
}
}
以上配置则可实现对于系统文件的访问,相当于一个小的文件服务器,采用springmvc做为http访问入口、本地文件系统或ftp文件系统做为文件仓库。
Spring Resources访问原理
以上是利用spring做文件服务器的使用配置,那么spring是如何做到这一点的?
其实在配置resources时,当访问url时,spring 分发交给对应的mappingHandler去处理,而静态文件则由ResourceHttpRequestHandler.handleRequest处理。主要过程是查找资源、解析资源类型、设置content-type,response输出流。
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1.获取资源文件,Resource是spring对静态资源的高度封装,可以看成是文件流或字节
Resource resource = this.getResource(request);
if(resource == null) {
logger.trace("No matching resource found - returning 404");
response.sendError(404);
} else if(HttpMethod.OPTIONS.matches(request.getMethod())) {
response.setHeader("Allow", this.getAllowHeader());
} else {
//2.判断Http请求头,是否有断点续传、解析文件类型设置http返回流ContentType
this.checkRequest(request);
if((new ServletWebRequest(request, response)).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified - returning 304");
} else {
this.prepareResponse(response);
MediaType mediaType = this.getMediaType(request, resource);
if(mediaType != null) {
if(logger.isTraceEnabled()) {
logger.trace("Determined media type \'" + mediaType + "\' for " + resource);
}
} else if(logger.isTraceEnabled()) {
logger.trace("No media type found for " + resource + " - not sending a content-type header");
}
if("HEAD".equals(request.getMethod())) {
this.setHeaders(response, resource, mediaType);
logger.trace("HEAD request - skipping content");
} else {
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if(request.getHeader("Range") == null) {
this.setHeaders(response, resource, mediaType);
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
} else {
response.setHeader("Accept-Ranges", "bytes");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request);
try {
List ex = inputMessage.getHeaders().getRange();
response.setStatus(206);
if(ex.size() == 1) {
ResourceRegion resourceRegion = ((HttpRange)ex.get(0)).toResourceRegion(resource);
this.resourceRegionHttpMessageConverter.write(resourceRegion, mediaType, outputMessage);
} else {
this.resourceRegionHttpMessageConverter.write(HttpRange.toResourceRegions(ex, resource), mediaType, outputMessage);
}
} catch (IllegalArgumentException var9) {
response.setHeader("Content-Range", "bytes */" + resource.contentLength());
response.sendError(416);
}
}
}
}
}
}
1.根据request获取资源文件
org.springframework.web.servlet.resource.ResourceHttpRequestHandler#getResource(HttpServletRequest request)
protected Resource getResource(HttpServletRequest request) throws IOException {
// 1.获取文件路径,资源请求request在过滤链中解析了path并封装到attribute中
String path = (String)request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
if(path == null) {
throw new IllegalStateException("Required request attribute \'" + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "\' is not set");
} else {
path = this.processPath(path);
if(StringUtils.hasText(path) && !this.isInvalidPath(path)) {
if(path.contains("%")) {
try {
if(this.isInvalidPath(URLDecoder.decode(path, "UTF-8"))) {
if(logger.isTraceEnabled()) {
logger.trace("Ignoring invalid resource path with escape sequences [" + path + "].");
}
return null;
}
} catch (IllegalArgumentException var6) {
;
}
}
DefaultResourceResolverChain resolveChain = new DefaultResourceResolverChain(this.getResourceResolvers());
//2.ResolveResource解析资源文件路径,并将文件封装到Resource并返回
Resource resource = resolveChain.resolveResource(request, path, this.getLocations());
if(resource != null && !this.getResourceTransformers().isEmpty()) {
DefaultResourceTransformerChain transformChain = new DefaultResourceTransformerChain(resolveChain, this.getResourceTransformers());
resource = transformChain.transform(request, resource);
return resource;
} else {
return resource;
}
} else {
if(logger.isTraceEnabled()) {
logger.trace("Ignoring invalid resource path [" + path + "]");
}
return null;
}
}
}
根据Resource resource = resolveChain.resolveResource(request, path, this.getLocations());
查找调用关系,可定准到抽象类org.springframework.web.servlet.resource.AbstractResourceResolver#resolveResourceInternal
,该抽象类是用于解析文件,不同文件协议对其有实现,如下图。
继续阅读代码,spring通过xml配置定义的locations遍历(org.springframework.web.servlet.resource.PathResourceResolver#getResource(java.lang.String, javax.servlet.http.HttpServletRequest, java.util.List<? extends org.springframework.core.io.Resource>)
)查找对应的文件路径,并封装到Resource实现类。
2.Resource
Resource是spring对各种静态资源文件的高度封装接口,主要方法及继承类如下图。
Spring如何解析文件类型
以上是解决文件来源及文件流的问题,开发过文件下载接口的读者可能比较熟悉,如果在http response返回时不设置content-type即采用默认的text/html
或*/*
则只能实现流下载,而不能实现如浏览器在线预览的功能,这都是由于content-type未设置为文件内容的具体类型,浏览器接收到response不能根据content-type正确解析流内容,无法调用内核插件如pdf来显示相关内容,而弹出下载窗口。
org.springframework.web.servlet.resource.ResourceHttpRequestHandler#getMediaType(javax.servlet.http.HttpServletRequest, org.springframework.core.io.Resource)
找到调用链来看看Spring具体是如何解析content-type
1.getMediaType
方法
protected MediaType getMediaType(HttpServletRequest request, Resource resource) {
MediaType mediaType = this.getMediaType(resource);
// 查找具体实现类调用
return mediaType != null?mediaType:this.contentNegotiationStrategy.getMediaTypeForResource(resource);
}
2.org.springframework.web.accept.ServletPathExtensionContentNegotiationStrategy#getMediaTypeForResource
this.contentNegotiationStrategy.getMediaTypeForResource(resource)
的具体实现是由ServletPathExtensionContentNegotiationStrategy类覆盖方法。
public MediaType getMediaTypeForResource(Resource resource) {
MediaType mediaType = null;
if(this.servletContext != null) {
// servletContext由tomcat或jetty容器实现
String superMediaType = this.servletContext.getMimeType(resource.getFilename());
if(StringUtils.hasText(superMediaType)) {
mediaType = MediaType.parseMediaType(superMediaType);
}
}
if(mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
MediaType superMediaType1 = super.getMediaTypeForResource(resource);
if(superMediaType1 != null) {
mediaType = superMediaType1;
}
}
return mediaType;
}
Web容器在启动的过程中,会为每个Web应用程序创建一个对应的ServletContext对象,它代表了当前的Web应用,为Spring IoC容器提供宿主环境,在部署Web工程的时候,Web容器会读取web.xml,创建ServletContext,当前Web工程所有部分都共享这个Context。
ServletContext
的继承关系如下图
SpringBoot启动容器时加载MimeMappings类
AbstractConfigurableEmbeddedServletContainer初始化ServletContext对象时指定MimeMappings为DEFAULT,被Spring用来解析文件名,其中定义了大量的http content-type,有兴趣的读者可以顺着以上的逻辑找到对应的实现。