SpringBoot2整合SpringSecurity+Swagger3系列
本章继续介绍Spring MVC的自动配置。在默认情况下,包含以下这些Bean
- ContentNegotiatingViewResolver 和 BeanNameViewResolver
这两个类都是属于ViewResolver(视图解析器)的范畴,所谓的ViewResolver就是根据名称解析为对应的View对象的工具。对应的接口只有一个方法
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
最简单的ViewResolver非BeanNameViewResolver莫属,直接在通过viewName作为beanName查找对应的Bean即可。
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = obtainApplicationContext();
if (!context.containsBean(viewName)) {
// Allow for ViewResolver chaining...
return null;
}
if (!context.isTypeMatch(viewName, View.class)) {
if (logger.isDebugEnabled()) {
logger.debug("Found bean named '" + viewName + "' but it does not implement View");
}
// Since we're looking into the general ApplicationContext here,
// let's accept this as a non-match and allow for chaining as well...
return null;
}
return context.getBean(viewName, View.class);
}
ContentNegotiatingViewResolver作为ViewResolver 的实现,它根据请求文件名或 Accept 标头解析视图。ContentNegotiatingViewResolver本身不解析视图,而是委托给其他ViewResolver。ContentNegotiatingViewResolver内部有一个viewResolvers列表用于存放可用的视图解析器。默认情况下,这些其他视图解析器会从应用程序上下文中自动获取,但也可以使用 viewResolvers 属性显式设置它们。以下为Bean在初始化的过程中从容器中获取ViewResolver实现并排序的对应源码。
@Override
protected void initServletContext(ServletContext servletContext) {
Collection<ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList<>(matchingBeans.size());
for (ViewResolver viewResolver : matchingBeans) {
if (this != viewResolver) {
this.viewResolvers.add(viewResolver);
}
}
}
else {
for (int i = 0; i < this.viewResolvers.size(); i++) {
ViewResolver vr = this.viewResolvers.get(i);
if (matchingBeans.contains(vr)) {
continue;
}
String name = vr.getClass().getName() + i;
obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
}
}
AnnotationAwareOrderComparator.sort(this.viewResolvers);
this.cnmFactoryBean.setServletContext(servletContext);
}
请注意,为了使此视图解析器正常工作,需要将 order 属性设置为比其他属性更高的优先级(默认为 Ordered.HIGHEST_PRECEDENCE)。
private int order = Ordered.HIGHEST_PRECEDENCE;
此视图解析器使用请求的媒体类型为请求选择合适的视图。请求的媒体类型通过配置的 ContentNegotiationManager 确定。一旦确定了请求的媒体类型,这个解析器就会查询每个委托视图解析器以获得一个视图,并确定请求的媒体类型是否与视图的内容类型兼容)。返回最兼容的视图。
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
" given " + requestedMediaTypes.toString() : "";
if (this.useNotAcceptableStatusCode) {
if (logger.isDebugEnabled()) {
logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
}
return NOT_ACCEPTABLE_VIEW;
}
else {
logger.debug("View remains unresolved" + mediaTypeInfo);
return null;
}
}
此外,此视图解析器公开 defaultViews 属性,允许您覆盖视图解析器提供的视图。请注意,这些默认视图是作为候选提供的,并且仍然需要请求内容类型(通过文件扩展名、参数或 Accept 标头,如上所述)。例如,如果请求路径是 /view.html,则此视图解析器将查找具有 text/html 内容类型(基于 html 文件扩展名)的视图。带有 text/html 请求 Accept 标头的 /view 请求具有相同的结果。在上面章节通过HandlerAdapter处理之后返回ModelAndView。
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
如果ModelAndView不为空,则会进入渲染(render)。
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
其实在现在的SpringBoot项目当中,都是直接通过JSON来传输的(REST-style services),所以视图的作用越来越弱了。ViewResolver的地位早已不再了。
An InternalResourceViewResolver named ‘defaultViewResolver’. This one locates physical resources that can be rendered by using the DefaultServlet (including static resources and JSP pages, if you use those). It applies a prefix and a suffix to the view name and then looks for a physical resource with that path in the servlet context (the defaults are both empty but are accessible for external configuration through spring.mvc.view.prefix and spring.mvc.view.suffix). You can override it by providing a bean of the same type.
在默认情况,Spring将classpath下面/static
、/public
、/resources
、/META-INF/resources
目录作为静态资源目录,还有ServletContext的根目录。用户通过实现WebMvcConfigurer覆盖addResourceHandlers来配置自己的静态资源目录。
比如在Swagger3当中
public class SwaggerUiWebMvcConfigurer implements WebMvcConfigurer {
private final String baseUrl;
public SwaggerUiWebMvcConfigurer(String baseUrl) {
this.baseUrl = baseUrl;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String baseUrl = StringUtils.trimTrailingCharacter(this.baseUrl, '/');
registry.
addResourceHandler(baseUrl + "/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
.resourceChain(false);
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController(baseUrl + "/swagger-ui/")
.setViewName("forward:" + baseUrl + "/swagger-ui/index.html");
}
}
所谓的添加addResourceHandler,就是创建一个ResourceHandlerRegistration对象,后续的addResourceLocations和resourceChain都是设置内部的属性。并将创建的对象添加到ResourceHandlerRegistry的registrations属性当中。
private final List<ResourceHandlerRegistration> registrations = new ArrayList<>();
/**
* Add a resource handler for serving static resources based on the specified URL path patterns.
* The handler will be invoked for every incoming request that matches to one of the specified
* path patterns.
* <p>Patterns like {@code "/static/**"} or {@code "/css/{filename:\\w+\\.css}"} are allowed.
* See {@link org.springframework.util.AntPathMatcher} for more details on the syntax.
* @return a {@link ResourceHandlerRegistration} to use to further configure the
* registered resource handler
*/
public ResourceHandlerRegistration addResourceHandler(String... pathPatterns) {
ResourceHandlerRegistration registration = new ResourceHandlerRegistration(pathPatterns);
this.registrations.add(registration);
return registration;
}
设置ResourceHandlerRegistration的属性
/**
* Add one or more resource locations from which to serve static content.
* Each location must point to a valid directory. Multiple locations may
* be specified as a comma-separated list, and the locations will be checked
* for a given resource in the order specified.
* <p>For example, {{@code "/"}, {@code "classpath:/META-INF/public-web-resources/"}}
* allows resources to be served both from the web application root and
* from any JAR on the classpath that contains a
* {@code /META-INF/public-web-resources/} directory, with resources in the
* web application root taking precedence.
* <p>For {@link org.springframework.core.io.UrlResource URL-based resources}
* (e.g. files, HTTP URLs, etc) this method supports a special prefix to
* indicate the charset associated with the URL so that relative paths
* appended to it can be encoded correctly, e.g.
* {@code [charset=Windows-31J]https://example.org/path}.
* @return the same {@link ResourceHandlerRegistration} instance, for
* chained method invocation
*/
public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) {
this.locationValues.addAll(Arrays.asList(resourceLocations));
return this;
}
/**
* Configure a chain of resource resolvers and transformers to use. This
* can be useful, for example, to apply a version strategy to resource URLs.
* <p>If this method is not invoked, by default only a simple
* {@link PathResourceResolver} is used in order to match URL paths to
* resources under the configured locations.
* @param cacheResources whether to cache the result of resource resolution;
* setting this to "true" is recommended for production (and "false" for
* development, especially when applying a version strategy)
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
* @since 4.1
*/
public ResourceChainRegistration resourceChain(boolean cacheResources) {
this.resourceChainRegistration = new ResourceChainRegistration(cacheResources);
return this.resourceChainRegistration;
}
比如上面SwaggerUiWebMvcConfigurer的addResourceHandlers最终添加的对象如下所示
当创建resourceHandlerMapping这个Bean的时候,就会触发registry的getHandlerMapping方法。
@Bean
@Nullable
public HandlerMapping resourceHandlerMapping() {
Assert.state(this.applicationContext != null, "No ApplicationContext set");
Assert.state(this.servletContext != null, "No ServletContext set");
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
this.servletContext, mvcContentNegotiationManager(), mvcUrlPathHelper());
addResourceHandlers(registry);
AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
if (handlerMapping == null) {
return null;
}
handlerMapping.setPathMatcher(mvcPathMatcher());
handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
handlerMapping.setInterceptors(getInterceptors());
handlerMapping.setCorsConfigurations(getCorsConfigurations());
return handlerMapping;
}
在getHandlerMapping时,就是将已经注册的registrations转变为ResourceHttpRequestHandler对象。ResourceHttpRequestHandler的继承结构如下所示,看起来很复杂,其实它主要是作为一个HttpRequestHandler而存在,最后用于处理静态请求并响应(发送资源)。最主要的方法是handleRequest。
ResourceHandlerRegistry#getHandlerMapping
方法遍历注册的ResourceHandlerRegistration列表转为ResourceHttpRequestHandler对象。 然后创建一个SimpleUrlHandlerMapping对象。
/**
* Return a handler mapping with the mapped resource handlers; or {@code null} in case
* of no registrations.
*/
@Nullable
protected AbstractHandlerMapping getHandlerMapping() {
if (this.registrations.isEmpty()) {
return null;
}
Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>();
for (ResourceHandlerRegistration registration : this.registrations) {
for (String pathPattern : registration.getPathPatterns()) {
ResourceHttpRequestHandler handler = registration.getRequestHandler();
if (this.pathHelper != null) {
handler.setUrlPathHelper(this.pathHelper);
}
if (this.contentNegotiationManager != null) {
handler.setContentNegotiationManager(this.contentNegotiationManager);
}
handler.setServletContext(this.servletContext);
handler.setApplicationContext(this.applicationContext);
try {
handler.afterPropertiesSet();
}
catch (Throwable ex) {
throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", ex);
}
urlMap.put(pathPattern, handler);
}
}
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
handlerMapping.setOrder(this.order);
handlerMapping.setUrlMap(urlMap);
return handlerMapping;
}
创建了SimpleUrlHandlerMapping对象,其中的urlMap记录了请求路径和ResourceHttpRequestHandler的映射。
然后再设置这个handlerMapping的其他属性
handlerMapping.setPathMatcher(mvcPathMatcher());
handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
handlerMapping.setInterceptors(getInterceptors());
handlerMapping.setCorsConfigurations(getCorsConfigurations());
那么这个resourceHandlerMapping有啥用呢?怎么与静态资源处理相联系的呢?在DispatcherServlet通过请求获取HandlerExecutionChain的时候,其实就是遍历容器中的所有HandlerMapping,然后通过HandlerMapping来获取处理器并构造处理器链的。在前面已经介绍过这一块了。而刚才创建的resourceHandlerMapping正是在这里列表当中
当前台请求路径为/swagger-ui/swagger-ui.css?v=3.0.0
的时候,就会匹配到这个resourceHandlerMapping。此时日志如下
返回的拦截器链中的handler正是上面介绍的ResourceHttpRequestHandler。
在DispatchServlet当中,接下来请求会获取一个HandlerAdapter。
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
HttpRequestHandlerAdapter正是对应的HandlerAdapter。其中的supports方法和handle方法如下所示,可以看到这个HandlerAdapter支持HttpRequestHandler类型的handler。(从这里也不难看出并不是所有的handler都是MethodHandler
)。
@Override
public boolean supports(Object handler) {
return (handler instanceof HttpRequestHandler);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
((HttpRequestHandler) handler).handleRequest(request, response);
return null;
}
就下来就是通过HandlerAdapter来处理请求了,从上面handle方法可以看出,其实就是直接由HttpRequestHandler来处理器请求,而返回的ModelAndView对象为空。由于没有视图返回,所以所有响应都是在HttpRequestHandler中来处理了。
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
终于来到了最重要的方法了。
/**
* Processes a resource request.
* <p>Checks for the existence of the requested resource in the configured list of locations.
* If the resource does not exist, a {@code 404} response will be returned to the client.
* If the resource exists, the request will be checked for the presence of the
* {@code Last-Modified} header, and its value will be compared against the last-modified
* timestamp of the given resource, returning a {@code 304} status code if the
* {@code Last-Modified} value is greater. If the resource is newer than the
* {@code Last-Modified} value, or the header is not present, the content resource
* of the resource will be written to the response with caching headers
* set to expire one year in the future.
*/
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// For very general mappings (e.g. "/") we need to check 404 first
Resource resource = getResource(request);
首先就是根据请求路径映射到静态资源路径,并返回静态资源对象。
if (resource == null) {
logger.debug("Resource not found");
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
response.setHeader("Allow", getAllowHeader());
return;
}
// Supported methods and required session
checkRequest(request);
// Header phase
if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified");
return;
}
// Apply cache settings, if any
prepareResponse(response);
// Check the media type for the resource
MediaType mediaType = getMediaType(request, resource);
// Content phase
if (METHOD_HEAD.equals(request.getMethod())) {
setHeaders(response, resource, mediaType);
return;
}
经过一些处理之后,就要处理响应了。
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if (request.getHeader(HttpHeaders.RANGE) == null) {
Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
setHeaders(response, resource, mediaType);
首先就是设置头信息
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
最后通过ResourceHttpMessageConverter(这是另一个HttpMessageConverter
)来进行序列化。
这里又设置了一些默认的头信息,然后执行子类的writeInternal方法,并且直接刷新流信息。
public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
}));
}
else {
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}
默认头信息如下
protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
writeContent(resource, outputMessage);
}
protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
try {
InputStream in = resource.getInputStream();
try {
StreamUtils.copy(in, outputMessage.getBody());
}
catch (NullPointerException ex) {
// ignore, see SPR-13620
}
finally {
try {
in.close();
}
catch (Throwable ex) {
// ignore, see SPR-12999
}
}
}
catch (FileNotFoundException ex) {
// ignore, see SPR-12999
}
}
可以看到ResourceHttpMessageConverter的序列化就是将资源作为流写入到相应流里面。ResourceHttpRequestHandler通过ResourceHttpMessageConverter将查找到的静态资源通过流的方式写入到响应流当中,然后立刻刷新。那么此处还有一个问题,就是为啥是ResourceHttpMessageConverter呢?其实ResourceHttpRequestHandler实现了InitializingBean接口,在对应的回调方法中进行的设置。如下所示
@Override
public void afterPropertiesSet() throws Exception {
resolveResourceLocations();
if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) {
logger.warn("Locations list is empty. No resources will be served unless a " +
"custom ResourceResolver is configured as an alternative to PathResourceResolver.");
}
if (this.resourceResolvers.isEmpty()) {
this.resourceResolvers.add(new PathResourceResolver());
}
initAllowedLocations();
// Initialize immutable resolver and transformer chains
this.resolverChain = new DefaultResourceResolverChain(this.resourceResolvers);
this.transformerChain = new DefaultResourceTransformerChain(this.resolverChain, this.resourceTransformers);
if (this.resourceHttpMessageConverter == null) {
this.resourceHttpMessageConverter = new ResourceHttpMessageConverter();
}
if (this.resourceRegionHttpMessageConverter == null) {
this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter();
}
this.contentNegotiationStrategy = initContentNegotiationStrategy();
}
在这里如果在初始化之前没有设置ResourceHttpRequestHandler的resourceHttpMessageConverter属性的话,就使用ResourceHttpMessageConverter。以上就是通过addResourceHandlers方法为啥能达到静态资源的处理的整个逻辑了。