springboot02

91 阅读19分钟

十、响应处理

1. 【源码分析】- ReturnValueHandler原理

图片

假设给前端自动返回json数据,需要引入相关的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- web场景自动引入了json场景 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-json</artifactId>
    <version>2.3.4.RELEASE</version>
    <scope>compile</scope>
</dependency>

控制层代码如下:

@Controller
public class ResponseTestController {
    @ResponseBody	// 利用返回值处理器里面的消息转换器进行处理
    @GetMapping(value = "/test/person")
    public Person getPerson() {
        Person person = new Person();
        person.setAge(28);
        person.setBirth(new Date());
        person.setUserName("zhangsan");
        return person;
    }
}

上面有提到过ReturnValueHandler。现在直接看重点:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
    // ...
    @Nullable
    protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
                                              HttpServletResponse response,
                                              HandlerMethod handlerMethod) throws Exception {
        ServletWebRequest webRequest = new ServletWebRequest(request, response);
        try {
            // ...
            ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
            
            if(this.argumentResolvers != null) {
                invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
            }
            if(this.returnValueHandlers != null) {	// <---关注点
                invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
            }
            // ...
            invocableMethod.invokeAndHandle(webRequest, mavContainer);	// 看下块代码
            if(asyncManager.isConcurrentHandlingStarted()) {
                return null;
            }
            return getModelAndView(mavContainer, modelFactory, webRequest);
        }
        finally {
            webRequest.requestCompleted();
        }
    }
}
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
    public void invokeAndHandle(ServletWebReuqest webRequest, ModelAndViewContainer mavContainer, Object... provideArgs) throws Exception {
        Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
        // ...
        try {
            // 看下代码块
            this.returnValueHandlers.handleReturnValue(
            	returnValue, getReturnValueType(returnValue), mavContainer, WebRequest);
        } catch(Exception ex) {
            // ...
        }
    }
}
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {
    // ...
    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        // slectHandler()实现下面
        HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
        if(handler == null) {
            throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        }
        // 开始处理
        handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
    }
    
    @Nullable
    private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
        boolean isAsyncValue = isAsyncReturnValue(value, returnType);
        for(HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
            if(isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
                continue;
            }
            if(handler.supportsReturnType(returnType)) {
                return handler;
            }
        }
        return null;
    }
}

@ResponseBody注解,即RequestResponseBodyMethodProcessor,它实现HandlerMethodReturnValueHandler接口

public class RequestResponseBodyMethodProcessor extends AbstractMEssageConverterMethodProcessor {
    // ...
    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        mavContainer.setRequestHandled(true);
        ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
        ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
        
        // 使用消息转换器进行写出操作,下节介绍:
        // Try even with null return value. ResponseBodyAdvice could get involved.
        writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
    }
}

2. 【源码分析】- HTTPMessageConverter原理

返回值处理器ReturnValueHandler原理:

  1. 返回值处理器判断是否支持这种类型返回值supportsReturnType
  2. 返回值处理器调用handleReturnValue进行处理
  3. RequestResponseBodyMethodProcessor可以处理返回值标了@ResponseBody注解的。
    1. 利用MessageConverters进行处理 将数据写为json
      1. 内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
      2. 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
      3. SpringMVC会挨个遍历所有容器底层的HttpMessageConverter,看谁能处理?
        1. 得到MappingJackson2HttpMessageConverter可以将对象写为json
        2. 利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
// RequestResponseBodyMethodProcessor继承这类
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler {
    // ...
    // 承接上一节内容
    protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        Object body;
        Class<?> valueType;
        Type targetType;
        
        if(value instanceof CharSequence) {
            body = value.toString();
            valueType = String.class;
            targetType = String.class;
        } else {
            body = value;
            valueType = getReturnValueType(body, returnType);
            targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
        }
        
        // ...
        
        // 内容协商(浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型)
        MediaType selectedMediaType = null;
        MediaType contentType = outputMessage.getHeaders().getContentType();
        boolean isContentTypePreset = contentType != null && contentType.isConcrete();
        if(isContentTypePreset) {
            if(logger.isDebugEnabled()) {
                logger.debug("Found 'Content-Type:" + contentType + "' in response");
            }
            selectedMediaType = contentType;
        } else {
            HttpServletRequest request = inputMessage.getServletRequest();
            List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
            // 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
            List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
            
            if(body != null && producibleTypes.isEmpty()) {
                throw new HttpMessageNotWritableException(
                            "No converter found for return value of type: " + valueType);
            }
            List<MediaType> mediaTypesToUse = new ArrayList<>();
            for(MediaType requestedType : acceptableTypes) {
                for(MediaType producibleType : produibleTypes) {
                    if(requestedType.isCompatibleWith(producibleType)) {
                        mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
                    }
                }
            }
            if(mediaTypesToUse.isEmpty()) {
                if(body != null) {
                    throw new HttpMediaTypeNotAcceptableException(producibleTypes);
                }
                if(logger.isDebugEnabled()) {
                    logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
                }
                return;
            }
            
            MediaType.sorBySpecificityAndQuality(mediaTypesToUse);
            
            // 选择一个MediaType
            for(MediaType mediaType : mediaTypesToUse) {
                if(mediaType.isConcrete()) {
                    selectedMediaType = mediaType;
                    break;
                } else if(mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
                    selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
                    break;
                }
            }
            
            if(logger.isDebygEnabled()) {
                logger.debug("Using '" + selectedMediaType + "', given " +
                            acceptableTypes + " and supported " + producibleTypes);
            }
        }
        
        if(selectedMediaType != null) {
            selectedMediaType = selectedMediaType.removeQualityValue();
            //本节主角:HttpMessageConverter
            for(HttpMessageConverter<?> converter : this.messageConverters) {
                GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
                
                // 判断是否可写
                if(genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) {
                    body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage outputMessage);
                    if(body != null) {
                        Object theBody = body;
                        LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
                        addContentDispositionHeader(inputMessage, outputMessage);
                        // 开始写入
                        if(genericConverter != null) {
                            genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                        } else {
                            ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                        }
                    } else {
                        if(logger.isDebugEnabled()) {
                            logger.debug("Nothing to write: null body");
                        }
                    }
                    return;
                }
            }
        }
    }
}

HTTPMessageConverter接口:

/**
 * Strategy interface for converting from and to HTTP requests and responses.
 */
public interface HttpMessageConverter<T> {

	/**
	 * Indicates whether the given class can be read by this converter.
	 */
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

	/**
	 * Indicates whether the given class can be written by this converter.
	 */
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

	/**
	 * Return the list of {@link MediaType} objects supported by this converter.
	 */
	List<MediaType> getSupportedMediaTypes();

	/**
	 * Read an object of the given type from the given input message, and returns it.
	 */
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

	/**
	 * Write an given object to the given output message.
	 */
	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;

}

HttpMessageConverter:看是否支持将 此Class类型的对象,转为MediaType类型的数据。

例子:Person对象转为JSON,或者JSON转为Person,这将用到MappingJackson2HttpMessageConverter

图片

public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    // ...
}

关于MappingJackson2HttpMessageConverter的实例化请看下节。

关于HttpMessageConverters的初始化

DispatcherServlet的初始化时会调用initHandlerAdapters(ApplicationContext context)

public class DispatcherServlet extends FrameworkServlet {
    // ...
    private void initHandlerAdapters(ApplicationContext context) {
        this.handlerAdapters = null;
        
        if(this.detectAllHandlerAdapters) {
            // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
            Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, flase);
            if(!matchingBeans.isEmpty()) {
                this.handlerAdapters = new ArrayList<>(matchingBeans.values());
                // We keep HandlerAdapters in sorted order.
                AnnotationAwareOrderComparator.sort(this.handlerAdapters);
            }
        }
    }
}

上述代码会加载ApplicationContext的所有HandlerAdapter,用来处理@RequestMappingRequestMappingHandlerAdapter实现HandlerAdapter接口,RequestMappingHandlerAdapter也被实例化。

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
    // ...
    private List<HttpMessageConverter<?>> messageConverters;
    // ...
    
    public RequestMappingHandlerAdapter() {
        this.messageConverters = new ArrayList<>(4);
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        if(!shouldIgnoreXml) {
            try {
                this.messageConverters.add(new SourceHttpMessageConverter<>());
            }
            catch(Error err) {
                // Ignore when no TrandformerFactory implementation is available
            }
        }
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    }
}

在构造器中看到一堆HttpMessageConverter。接着,重点查看AllEncopassingFormHttpMessageConverter类:

public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter {
    /**
	 * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to
	 * ignore XML, i.e. to not initialize the XML-related infrastructure.
	 * <p>The default is "false".
	 */
    private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore");

	private static final boolean jaxb2Present;

	private static final boolean jackson2Present;

	private static final boolean jackson2XmlPresent;

	private static final boolean jackson2SmilePresent;

	private static final boolean gsonPresent;

	private static final boolean jsonbPresent;

	private static final boolean kotlinSerializationJsonPresent;

    static {
        ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();
        jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
        jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
						ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
		jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
		jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
		gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
		jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
		kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
    }
    
    public AllEncompassingFormHttpMessageConverter() {
        if(!shouldIgnoreXml) {
            try {
                addPartConverter(new SourceHttpMessageConverter<>());
            }
            catch(Error err) {
                // Ignore when no TransformerFactory implementation is available
            }
            
            if(jaxb2Present && !jackson2XmlPresent) {
                addPartConverter(new Jaxb2RootElementHttpMessageConverter());
            }
        }
        
        if (jackson2Present) {
			addPartConverter(new MappingJackson2HttpMessageConverter());//<----重点看这里
		}
		else if (gsonPresent) {
			addPartConverter(new GsonHttpMessageConverter());
		}
		else if (jsonbPresent) {
			addPartConverter(new JsonbHttpMessageConverter());
		}
		else if (kotlinSerializationJsonPresent) {
			addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
		}

		if (jackson2XmlPresent && !shouldIgnoreXml) {
			addPartConverter(new MappingJackson2XmlHttpMessageConverter());
		}

		if (jackson2SmilePresent) {
			addPartConverter(new MappingJackson2SmileHttpMessageConverter());
		}
    }
}

public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
    // ...
    private List<HttpMessageConverter<?>> partConverters = new ArrayList<>();
    // ...
    public void addPartConverter(HttpMessageConverter<?> partConverter) {
        Assert.notNull(partConverter, "'partConverter' must not be null");
		this.partConverters.add(partConverter);
    }
    // ....
}

AllEncompassingFormHttpMessageConverter类构造器看到MappingJackson2HttpMessageConverter类的实例化,AllEncompassingFormHttpMessageConverter包含MappingJackson2HttpMessageConverter

ReturnValueHandler是怎么与MappingJackson2HttpMessageConverter关联起来?请看下节。

ReturnValueHandler与MappingJackson2HttpMessageConverter关联

再次回顾RequestMappingHandlerAdapter

public class RequestMappingHandlerAdapter extends AbstarctHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
    // ...
    @Nullable
    private HandlerMethodReturnValueHandlerComposite returnValueHandlers;	// 我们关注的returnValueHandlers
    
    @Override
    @Nullable	// 本方法在AbstactHandlerMethodAdapter
    public finalModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return handleInternal(request, response, (HandlerMethod) handler);
    }
    
    @Override
    protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        ModelAndView mav;
        // ...
        mav = invokeHandlerMethod(request, response, handlerMethod);
        // ...
        return mav;
    }
    
    @Nullable
    protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        ServletWebRequest webRequest = new ServletWebRequest(request, response);
        try {
            WebDataBinderFactory binderFactory = getDataBindFactory(handlerMethod);
            ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
            
            ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
			if (this.argumentResolvers != null) {
				invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
			}
			if (this.returnValueHandlers != null) {//<---我们关注的returnValueHandlers
				invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
			}
            
            ...
            
			invocableMethod.invokeAndHandle(webRequest, mavContainer);
			if (asyncManager.isConcurrentHandlingStarted()) {
				return null;
			}

			return getModelAndView(mavContainer, modelFactory, webRequest);
        }
        finally {
            webRequest.requestCompleted();
        }
    }
    
    @Override
    public void afterPropertiesSet() {
        // Do this first, it may add ResponseBody advice beans
        // ...
        // Annotation-based return value types
        // 这里就是ReturnValueHandler与MappingJackson2HttpMessageConverter关联 的关键点
        handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),	// <-------MessageConverters也就是传参传进来的
                                   this.contentNegotiationManager, this.requestResponseBodyAdvice));
        // ...
        return handlers;
    }
    // ...
    public List<HttpMessageConverter<?>> getMessageConverters() {
        return this.messageConverters;
    }
    
    // RequestMappingHandlerAdapter构造器已初始化部分messageConverters
    public RequestMappingHandlerAdapter() {
        this.messageConverters = new ArrayList<>(4);
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        if(!shouldIgnoreXml) {
            try {
                this.messageConverters.add(new SourceHttpMessageConverter<>());
            }
            catch(Error err) {
                // Ignore when no TrandformerFactory implementation is available
            }
        }
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    }
}

应用中WebMvcAutoConfiguration(底层是WebMvcConfigurationSupport实现)传入更多messageConverters,其中就包含MappingJackson2HttpMessageConverter

3. 【源码分析】- 内容协商远离

根据客户端接收能力不同,返回不同媒体类型的数据。

引入XML依赖:

<dependency>
	<groupId>com.fasterxml.jackjson.dataformat</groupId>
  <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

可用Postman软件分别测试返回json和xml:只需要改变请求头中Accept字段(application/json、application/xml)。

Http协议中规定的,Accept字段告诉服务器本客户端可以接收的数据类型。

内容协商远离:

  1. 判断当前响应头中是否已经有确定的媒体类型MediaType
  2. 获取客户端(PostMan、浏览器)支持接受的内容类型。(获取客户端Accept请求头字段application/xml)(这一步在下一节有详细介绍)
    1. contentNegotiationManager内容协商管理器 默认使用基于请求头的策略
    2. HeaderContentNegotationStrategy确定客户端可以接收的内容类型
  3. 遍历循环所有当前系统的MessageConverter,看谁支持操作这个对象(Person)
  4. 找到支持操作Person的converter,把converter支持的媒体类型统计出来。
  5. 客户端需要application/xml,服务端游10种MediaType。
  6. 进行内容协商的最佳匹配媒体类型。
  7. 支持将对象转为 最佳匹配媒体类型 的converter。调用它进行转化。
// RequestResponseBodyMethodProcessor继承这类
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturn ValueHandler {
  // ...
  
  // 和上一节代码一致
  protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter return Type, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    
    Object body;
    Class<?> valueType;
    Type targetType;
    
    if(value instanceof CharSequence) {
      body = value.toString();
      valueType = String.class;
      targetType = String.class;
    } else {
      body = value;
      valueType = getReturnValueType(body, returnType);
      targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
    }
    
    // ...
    
    // 本节重点
    // 内容协商(浏览器默认以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型)
    MediaType selectedMediaType = null;
    MediaType contentType = outputMessage.getHeaders().getContentType();
    boolean isContentTypePreset = contentType != null && contentType.isConcrete();
    if(isContentTypePreset) {
      if(logger.isDebugEnabled()) {
        logger.debug("Found 'Content-Type: " + contentType + "' in response");
      }
      selectedMediaType = contentType;
    } else {
      HttpServletRequest request = inputMessage.getServletRequest();
      List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
      // 服务器最终根据自身的能力,决定服务器能生产出什么样内容类型的数据
      List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
      
      if(body != null && producibleTypes.isEmpty()) {
        throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
      }
      List<MediaType> mediaTypesToUse = new ArrayList<>();
      for(MediaType requestedType : acceptableTypes) {
        for(MediaType producibleType : producibleTypes) {
          if(requestedType.isCompatibleWith(producibleType)) {
            mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
          }
        }
      }
      if(mediaTypesToUse.isEmpty()) {
        if(body != null) {
          throw new HttpMediaTypeNotAcceptableException(producibleTypes);
        }
        if(logger.isDebugEnabled()) {
          logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
        }
        return;
      }
      
      MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
      
      // 选择一个MediaType
      for(MediaType mediaType : mediaTypesToUse) {
        if(mediaType.isConcrete()) {
          selectedMediaType = mediaType;
          break;
        } else if(mediaType.isPresentIn(ALL_APLICATION_MEDIA_TYPES)) {
          selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
         	break;
        }
      }
      if(logger.isDebugEnabled()) {
        logger.debug("Using '" + selectedMediaType + "', given " + acceptableTypes + " and supported " + producibleTypes);
      }
    }
    
    if(selectedMediaType != null) {
      selectedMediaType = selectedMediaType.removeQualityValue();
      // 本节主角:HttpMessageConverter
      for(HttpMessageConverter<?>converter : this.messageConverters) {
        GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
        
        // 判断是否可写
        if(genericConverter != null ?
          	((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectdMediaType) : converter.canWrite(valueType, selectedMediaType)) {
          body = getAdivce().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage);
          if(body != null) {
            Object theBody = body;
            LogFormatUtils.traceDebug(logger, traceOn -> "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
            addContentDispositionHeader(inputMessage, outputMessage);
            // 开始写入
            if(genericConverter != null) {
              genericConverter.write(body, targetType, selectedMediaType, outputMessage);
            } else {
              ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
            }
          } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Nothing to write: null body");
            }
          }
          return;
        }
      }
    }
  }
}

4. 【源码分析】- 基于请求参数的内容协商原理

上一节内容协商原理的第二步:

获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段application/xml)

  • contentNegotiationManager内容协商管理器 默认使用基于请求头的策略
  • HeaderContentNegotiationStrategy确定客户端可以接收的内容类型
// RequestResponseBodyMethodProcessor继承这类
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler {
  // ...
  
  // 和上一节代码一致
  protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
                ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
                throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

            Object body;
            Class<?> valueType;
            Type targetType;
    
    				// ...
    
    				// 本节重点
    				// 内容协商(浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型)
    MediaType selectedMediaType = null;
    MediaType contentType = outputMessage.getHeaders().getContentType();
    boolean isContentTypePreset = contentType != null && contentType.isConcrete();
    if(isContentTypePreset) {
      if(logger.isDebugEnabled()) {
        logger.debug("Found 'Content-Type:" + contentType + "' in response");
      }
      selectedMediaType = contentType;
    } else {
      HttpServletRequest request = inputMessage.getServletRequest();
      List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
      // 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
      List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
      
      // ...
    }
    
    // 在AbstractMessageConverterMethodArgumentResolver类内
    private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
      // 内容协商管理器 默认使用基于请求头的策略
      return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
    }
}
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
  // ...
  
  public ContentNegotiationManager() {
    this(new HeaderContentNegotiationStrategy());		// 内容协商管理器 默认使用基于请求头的策略
  }
  
  @Override
  public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
    for(ContentNegotiationStrategy strategy : this.strategies) {
      List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
      if(mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
        continue;
      }
      return mediaTypes;
    }
    return MEDIA_TYPE_ALL_LIST;
  }
  // ...
}
// 基于请求头的策略
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
  
  /**
	 * {@inheritDoc}
	 * @throws HttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed
	 */
  @Override
  public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
    String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
    if(headerValueArray == null) {
      return MEDIA_TYPE_ALL_LIST;
    }
    
    List<String> headerValues = Arrays.asList(headerValueArray);
    try {
      List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
      MediaTypes.sortBySpecificityAndQuality(mediaTypes);
      return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
    }
    catch (InvalidMediaTypeException ex) {
      throw new HttpMediaTypeNotAcceptableException(
      	"Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
      )
    }
  }
}
开启浏览器参数方式内容协商功能

为了方便内容协商,开启基于请求参数的内容协商功能。

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true		# 开启请求参数内容协商模式

内容协商管理器,就会多了一个ParameterContentNegotiationStrategy(由Spring容器注入)

public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
  private String parameterName = "format";
  
  /**
	 * Create an instance with the given map of file extensions and media types.
	 */
  public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
    super(mediaTypes);
  }
  
  /**
	 * Set the name of the parameter to use to determine requested media types.
	 * <p>By default this is set to {@code "format"}.
	 */
  public void setParameterName(String parameterName) {
    Assert.notNull(parameterName, "'parameterName' is required");
    this.parameterName = parameterName;
  }
  
  public String getParameterName) {
    return this.parameterName;
  }
  
  @Override
  @Nullable
  protected String getMediaTypeKey(NativeWebRequest request) {
    return request.getParameter(getParameterName());
  }
  
  // --- 以下方法在AbstractMappingContentNegotiationStrategy类
  @Override
  public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
    return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
  }
  
  /**
	 * An alternative to {@link #resolveMediaTypes(NativeWebRequest)} that accepts
	 * an already extracted key.
	 * @since 3.2.16
	 */
  public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key) throws HttpMediaTypeNotAcceptableException {
    if(StringUtils.hasText(key)) {
      MediaType mediaType = lookupMediaType(key);
      if(mediaType != null) {
        handleMatch(key, mediaType);
        return Collections.singletonList(mediaType);
      }
      mediaType = handleNoMatch(webRequest, key);
      if(mediaType != null) {
        addMapping(key, mediaType);
        return Collections.singletonList(mediaType);
      }
    }
    return MEDIA_TYPE_ALL_LIST;
  }
}

然后,浏览器地址输入带format参数的URl:

http://localhost:8080/test/person?format=json
或
http://localhost:8080/test/person?format=xml

这样,后段会根据参数format的值,返回对应json或zml格式的数据。

5. 【源码分析】- 自定义MessageConverter

实现多协议数据兼容。json、xml、x-guigu(自创)

  • @ResponseBody响应数据出去 调用RequestResponseBodyMethodProcessor处理
  • Processor处理方法返回值。通过MessageConverter处理
  • 所有MessageConverter合起来可以支持各种媒体类型数据的操作(读、写)
  • 内容协商找到最终的messageConverter

SpringMVC的什么功能,一个入口给容器中添加一个WebMvcConfigurer

@Configuration(proxyBeanMethods = false)
public class WebConfig {
  @Bean
  public WebMvcConfigurer webMvcConfigurer() {
    return new WebMvcConfigurer() {
      @Override
      public void extendMessageConverters(List<HttpMessageConverter<?>> converts) {
        converters.add(new GuiguMessageConverter());
      }
    }
  }
}
/**
 * 自定义的Converter
 */
public class GuiguMessageConverter implements HttpMessageConverter<Person> {
  @Override
  public boolean canRead(Class<?> clazz, MediaType mediaType) {
    return false;
  }
  
  @Override
  public boolean canWrite(Class<?> clazz, MediaType mediaType) {
    return clazz.isAssignableFrom(Person.class);
  }
  
  /**
   * 服务器要统计所有MessageConverter都能写出哪些内容类型
   *
   * application/x-guigu
   * @return
   */
  @Override
  public List<MediaType> getSupportedMediaTypes() {
    return MediaType.parseMediaTypes("application/x-guigu");
  }
  
  @Override
  public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) {
    return null;
  }
  
  @Override
  public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
    // 自定义协议数据的写出
    String data = person.getUserName() + ";" + person.getAge() + ";" + person.getBirth();
    
    // 写出去
    OutputStream body = outputMessage.getBody();
    body.write(data.getBytes());
  }
}
import java.util.Date;

@Controller
public class ResponseTestController {
  
  /**
   * 1、浏览器发请求直接返回 xml		[application/xml]			jacksonXmlConverter
   * 2、如果ajax请求 返回 json		[application/json]		jacksonJsonConverter
   * 3、如果硅谷app发请求,返回自定义协议数据 	[application/x-guigu]		xxxxConverter
   *					属性值1;属性值2;
   *
   * 步骤:
   * 1、添加自定义的MessageConverter进系统底层
   * 2、系统底层就会统计出所有MessageConverter能操作哪些类型
   * 3、客户端内容协商	[guigu--->guigu]
   *
   */
  @ResponseBody		// 利用返回值处理器里面的消息转换器进行处理
  @GetMapping(value = "/text/person")
  public Person getPerson() {
    Person person = new Person();
    person.setAge(28);
    person.setBirth(new Date());
    person.setUserName("zhangsan");
    return person;
  }
}

用Postman发送/test/person(请求头Accept:application/x-guigu),将返回自定义协议数据的写出。

6. 【源码分析】- 浏览器与PostMan内容协商完全适配

假设你想基于自定义请求参数的自定义内容协商功能。

换句话,在地址栏输入http://localhost:8080/test/person?format=gg返回数据,跟http://localhost:8080/test/person且请求参数Accept:application/x-guigu的返回自定义协议数据的一致。

@Configuration(proxyBeanMethods = false)
public class WebConfig /*implements WebMvcConfigure*/ {
  
  // 1. WebMvcConfigurer定制化SpringMVC的功能
  @Bean
  public WebMvcConfigurer webMvcConfigurer() {
    return new WebMvcConfigurer() {
      /**
       * 自定义内容协商策略
       * @param configurer
       */
      @Override
      public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        // Map<String, MediaType> mediaTypes
        Map<String, MediaType> mediaTyps = new HashMap<>();
        mediaTypes.put("json", MediaType.APPLICATION_JSON);
        mediaTypes.put("xml", MediaType.APPLICATION_XML);
        // 自定义媒体类型
        mediaTypes.put("gg", MediaType.parseMediaType("application/x-guigu"));
        // 指定支持解析哪些参数对应的哪些媒体类型
        ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
        // parameterStrategy.setParameterName("ff");
        
        // 还需添加请求头处理策略,否则accept:application/json、application/xml则会失效
        HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
        
        configurer.strategies(Arrays.asList(parameterStrategy, headerStrategy));
      }
    }
  }
  // ...
}

日后开发要注意,有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。

十一、视图解析

1. Thymeleaf初体验

Thymeleaf官方文档

thymeleaf使用

引入Starter

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

自动配置号了thymeleaf

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class TheymeleafAutoConfiguration {
  // ...
}

自动配好的策略

  • 所有thymeleaf的配置值都在ThymeleafProperties
  • 配置好了SpringTemplateEngine
  • 配置好了ThymeleafViewResolver
  • 我们只需要直接开发页面
public static final String DEFAULT_PREFIX = "classpath:/templates/"; // 模版放置处
public static final String DEFAULT_SUFFIX = ".html";	// 文件后缀名

编写一个控制层:

@Controller
public class ViewTestController {
  @GetMapping("/hello")
  public String hello(Model model) {
    // model中的数据会被放在请求域中 request.setAttribute("a", aa)
    model.addAttribute("msg","一定要大力发展工业文化");
    model.addAttribute("link","http://www.baidu.com");
    return "success";
  }
}

/templates/success.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 th:text="${msg}">nice</h1>
<h2>
    <a href="www.baidu.com" th:href="${link}">去百度</a>  <br/>
    <a href="www.google.com" th:href="@{/link}">去百度</a>
</h2>
</body>
</html>
server:
  servlet:
    context-path: /app #设置应用名

这个设置后,URL要插入/app,如http://localhost:8080/app/hello.html

2. 基本语法

表达式
表达式名字语法用途
变量取值${...}获取请求域、session域、对象等值
选择变量*{...}获取上下文对象值
消息#{...}获取国际化等值
链接@{...}生成链接
片段表达式~{...}jsp:include作用,引入公共页面片段
字面量
  • 文本值: ‘one text’ , ‘Another one!’ ,…
  • 数字: 0 , 34 , 3.0 , 12.3 ,…
  • 布尔值: true , false
  • 空值: null
  • 变量: one,two,… 变量不能有空格
文本操作
  • 字符串拼接:+
  • 变量替换:|The name is ${name}|
数学运算
  • 运算符:+, -, *, /, %
布尔值
  • 运算符:and,or
  • 一元运算符:!,not
比较运算符
  • 比较: > , < , >= , <= ( gt , lt , ge , le )
  • 等式: == , != ( eq , ne )
条件运算
  • If-then: (if) ? (then)
  • If-then-else: (if) ? (then) : (else)
  • Default: (value) ?: (defaultvalue)
特殊操作
  • 无操作:_
设置属性值- th:attr
  • 设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
  </fieldset>
</form>
  • 设置多个值
<img src="../../images/gtvglogo.png"  
     th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

官方文档 - 5 Setting Attribute Values

迭代
<tr th:each="prod : ${prods}">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
条件运算
<a href="comments.html"
	th:href="@{/product/comments(prodId=${prod.id})}"
	th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
      <p th:case="'admin'">User is an administrator</p>
      <p th:case="#{roles.manager}">User is a manager</p>
      <p th:case="*">User is some other thing</p>
</div>
属性优先级
OrderFeatureAttributes
1Fragment inclusionth:insert th:replace
2Fragment iterationth:each
3Conditional evaluationth:if th:unless th:switch th:case
4Local variable definitionth:object th:with
5General attribute modificationth:attr th:attrprepend th:attrappend
6Specific attribute modificationth:value th:href th:src ...
7Text(tag body modification)th:text th:utext
8Fragment specificationth:fragment
9Fragment removalth:remove

官方文档 - 10 Attribute Precedence

十二、web实验

1. 项目创建

使用IDEA的Spring Initializr。

  • thymeleaf
  • web-starter
  • devtools
  • lombok

2. 登陆页面

/static放置css,js等静态资源

/templates/login.html登录页

<html lang="en" xmlns:th="http://www.thymeleaf.org"><!-- 要加这玩意thymeleaf才能用 -->

<form class="form-signin" action="index.html" method="post" th:action="@{/login}">

    ...
    
    <!-- 消息提醒 -->
    <label style="color: red" th:text="${msg}"></label>
    
    <input type="text" name="userName" class="form-control" placeholder="User ID" autofocus>
    <input type="password" name="password" class="form-control" placeholder="Password">
    
    <button class="btn btn-lg btn-login btn-block" type="submit">
        <i class="fa fa-check"></i>
    </button>
    
    ...
    
</form>

/templates/main.html主页

thymeleaf內联写法:

<p>Hello, [[${session.user.name}]]!</p>

3. 登陆控制层

@Controller
public class IndexController {
  /**
   * 来登录页
   * @return
   */
  @GetMapping(value = {"/", "/login"})
  public String loginPage() {
    return "login";
  }
  
  @PostMapping("/login")
  public String main(User user, HttpSession session, Model model) {	// RedirectAttributes
    
    if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())) {
      // 把登陆成功的用户保存起来
      session.setAttribute("loginUser", user);
      // 登陆成功重定向到main.html; 重定向防止表单重复提交
      return "redirect:/main.html";
    } else {
      model.addAttribute("msg", "账号密码错误");
      // 回到登陆页面
      return "login";
    }
  }
  
  /**
   * 去main页面
   * @return
   */
  @GetMapping("/main.html")
  public String mainPage(HttpSession session, Model model) {
    // 最好用拦截器,过滤器
    Object loginUser = session.getAttribute("loginUser");
    if(loginUser != null) {
      return "main";
    } else {
      // session过期,没有登陆过
      // 回到登陆页面
      model.addAttribute("msg", "请重新登陆");
      return "login";
    }
  }
}
模型
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
  private String userName;
  private String password;
}

4. 抽取公共页面

官方文档 - Template Layout

公共页面/templates/common.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"><!--注意要添加xmlns:th才能添加thymeleaf的标签-->
<head th:fragment="commonheader">
    <!--common-->
    <link href="css/style.css" th:href="@{/css/style.css}" rel="stylesheet">
    <link href="css/style-responsive.css" th:href="@{/css/style-responsive.css}" rel="stylesheet">
    ...
</head>
<body>
<!-- left side start-->
<div id="leftmenu" class="left-side sticky-left-side">
	...

    <div class="left-side-inner">
		...

        <!--sidebar nav start-->
        <ul class="nav nav-pills nav-stacked custom-nav">
            <li><a th:href="@{/main.html}"><i class="fa fa-home"></i> <span>Dashboard</span></a></li>
            ...
            <li class="menu-list nav-active"><a href="#"><i class="fa fa-th-list"></i> <span>Data Tables</span></a>
                <ul class="sub-menu-list">
                    <li><a th:href="@{/basic_table}"> Basic Table</a></li>
                    <li><a th:href="@{/dynamic_table}"> Advanced Table</a></li>
                    <li><a th:href="@{/responsive_table}"> Responsive Table</a></li>
                    <li><a th:href="@{/editable_table}"> Edit Table</a></li>
                </ul>
            </li>
            ...
        </ul>
        <!--sidebar nav end-->
    </div>
</div>
<!-- left side end-->


<!-- header section start-->
<div th:fragment="headermenu" class="header-section">

    <!--toggle button start-->
    <a class="toggle-btn"><i class="fa fa-bars"></i></a>
    <!--toggle button end-->
	...

</div>
<!-- header section end-->

<div id="commonscript">
    <!-- Placed js at the end of the document so the pages load faster -->
    <script th:src="@{/js/jquery-1.10.2.min.js}"></script>
    <script th:src="@{/js/jquery-ui-1.9.2.custom.min.js}"></script>
    <script th:src="@{/js/jquery-migrate-1.2.1.min.js}"></script>
    <script th:src="@{/js/bootstrap.min.js}"></script>
    <script th:src="@{/js/modernizr.min.js}"></script>
    <script th:src="@{/js/jquery.nicescroll.js}"></script>
    <!--common scripts for all pages-->
    <script th:src="@{/js/scripts.js}"></script>
</div>
</body>
</html>

/templates/table/basic_table.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
  <meta name="description" content="">
  <meta name="author" content="ThemeBucket">
  <link rel="shortcut icon" href="#" type="image/png">

  <title>Basic Table</title>
    <div th:include="common :: commonheader"> </div><!--将common.html的代码段 插进来-->
</head>

<body class="sticky-header">

<section>
<div th:replace="common :: #leftmenu"></div>
    
    <!-- main content start-->
    <div class="main-content" >

        <div th:replace="common :: headermenu"></div>
        ...
    </div>
    <!-- main content end-->
</section>

<!-- Placed js at the end of the document so the pages load faster -->
<div th:replace="common :: #commonscript"></div>


</body>
</html>

Difference between th:insert and th:replace (and th:include)

4. 遍历数据域页面bug修改

控制层代码:

@GetMapping("/dynamic_table")
public String dynamic_table(Model model) {
  // 表格内容的遍历
  List<User> users = Arrays.asList(new User("zhangsan", "123456"),
                                  new User("lisi", "123444")),
                                     new User("haha", "aaaaa"),
                                     new User("hehe ", "aaddd"));
  model.addAttribute("users", users);
  return "table/dynamic_table";
}

页面代码:

<table class="display table table-bordered" id="hidden-table-info">
    <thead>
        <tr>
            <th>#</th>
            <th>用户名</th>
            <th>密码</th>
        </tr>
    </thead>
    <tbody>
        <tr class="gradeX" th:each="user,stats:${users}">
            <td th:text="${stats.count}">Trident</td>
            <td th:text="${user.userName}">Internet</td>
            <td >[[${user.password}]]</td>
        </tr>
    </tbody>
</table>

十三、视图解析

1. 视图解析器与视图

视图解析原理流程:

  1. 目标方法处理的过程中(阅读DispatcherServlet源码),所有的数据都会被放在ModelAndViewContainer里面,其中包括数据和视图地址。
  2. 方法的参数时一个自定义类型对象(从请求参数中确定的),把他重新放在ModelAndViewContainer.
  3. 任何目标方法执行完成以后都会返回ModelAndView(数据和视图地址)。
  4. processDsipatchResult()处理派发结果(页面该如何响应)
    1. render(mv, request, response);进行页面渲染逻辑
      1. 根据方法的String返回值得到View对象【定义了页面的渲染逻辑】
        1. 所有的视图解析器尝试是否能根据当前返回值得到View对象
        2. 得到了redirect:/main.html --> Thymeleaf new RedirectView()
        3. ContentNegotiationViewResolver里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
        4. view.render(mv.getModelInternal(), request, response);视图对象调用自定义的render进行页面渲染工作。
      2. RedirectView如何渲染【重定向到一个页面】
      3. 获取目标url地址
      4. response.sendRedirect(encodedURL);

视图解析:

  • 返回值以forward:开始:new InternalResourceView(forwardUrl); --> 转发 request.getRequestDispatcher(path).forward(request, response);
  • 返回值以redirect:开始:new RedirectView() --> render就是重定向
  • 返回值是普通字符串:new ThymeleafView() -->

十四、拦截器

1. 登录检查与静态资源放行

  • 编写一个拦截器实现HandlerInterceptor接口
  • 拦截器注册到容器中(实现WebMvcConfigureraddInterceptors()
  • 指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截)

编写一个拦截器实现HandlerInterceptor接口的拦截器:

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
  /**
   * 目标方法执行之前
   */
  @Override
  public boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String requestURI = request.getRequestURI();
    log.info("preHandle拦截的请求路径是{}", requestURI);
    
    // 登录检查逻辑
    HttpSession session = request.getSession();
    
    Object loginUser = session.getAttribute("loginUser");
    
    if(loginUser != null) {
      // 放行
      return true;
    }
    
    // 拦截住。微登录。跳转到登录页
    request.setAttribute("msg", "请先登录");
    // re.sendRedirect("/");
    request.getRequestDispatcher("/").forward(request, response);
    return false;
  }
  
  /**
  * 目标方法执行完成以后
  */
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    log.info("postHandle执行{}",modelAndView);
  }
  
  /**
   * 页面渲染以后
   */
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    log.info("afterCompletion执行异常{}",ex);
  }
}

拦截器注册到容器中&&指定拦截器规则:

@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginInterceptor())		// 拦截器注册到容器中
      			.addPathPatterns("/**")		// 所有请求都被拦截包括静态资源
      			.excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "images/**", "/js/**", "/aa/**");		// 放行的请求
  }
}

2. 【源码分析】- 拦截器的执行时机和原理

  1. 根据当前请求,找到HandleExecutionChain(可以处理请求的handler以及handler的所有 拦截器)
  2. 先来顺序执行 所有拦截器的preHandle()方法
    1. 如果当前拦截器preHandle()返回为true。则执行下一个拦截器的preHandle()
    2. 如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的afterCompletion();
  3. 如果任何一个拦截器返回false,直接跳出不执行目标方法。
  4. 所有拦截器都返回true,才执行目标方法。
  5. 倒序执行所有拦截器的postHandle()方法。
  6. 前面的步骤有任何异常都会直接倒序触发afterCompletion()
  7. 页面成功渲染完成以后,也会倒序触发afterCompletion()

![image-20211128191606965](/Users/hzc/Library/Application Support/typora-user-images/image-20211128191606965.png)

DispatcherServlet中涉及到HandlerInterceptor的地方:

public class DispatcherServlet extends FrameworkServlet {
  // ...
  protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    
    try {
      ModelAndView mv = null;
      Exception dispatchException = null;
      	// ...
      	// 该方法内调用HandlerInterceptor的preHandle()
      	if(!mappedHandler.applyPreHandle(processedRequest, response)) {
          return;
        }
      
      	// Actually invoke the handler.
      	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
      
      	// ...
      	// 该方法内调用HandlerInterceptor的postHandle()
      	mappendHandler.applyPostHandle(processedRequest, response, mv);
    }
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
  } catch(Exception ex) {
    // 该方法内调用HandlerInterceptor接口的afterCompletion方法
    triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
  }
  catch (Throwable err) {
    // 该方法内调用HandlerInterceptor接口的afterCompletion方法
    triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err));
  }
  finally {
    // ...
  }
}

private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable HandleExecutionChain mappedHandler, Exception ex) throws Exception {
  if(mappedHandler != null) {
    // 该方法内调用HandlerInterceptor接口的afterCompletion方法
    mappendHandler.triggerAfterCompletion(request, response, ex);
  }
  throw ex;
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
  // ...
  if(mappedHandler != null) {
    // 该方法内调用HandlerInterceptor接口的afterCompletion方法
    // Exception (if any) is already handled...
    mappedHandler.triggerAfterCompletion(request, response, null);
  }
}
public class HandlerExcutionChain {
  // ...
  boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    for(int i = 0; i < this.interceptorList.size(); i++) {
      HandlerInterceptor interceptor = this.interceptorList.get(i);
      // HandlerInterceptor的preHandle方法
      if(!interceptor.preHandle(request, response, this.handler)) {
        triggerAfterCompletion(request, response, null);
        return false;
      }
      this.interceptorIndex = i;
    }
    return true;
  }
  void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
    for(int i = this.interceptorList.size() - 1; i >= 0; i--) {
      HandlerInterceptor interceptor = this.interceptorList.get(i);
      
      // HandlerInterceptor接口的postHandle方法
      interceptor.postHandle(request, response, this.handler, mv);
    }
  }
  
  void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
    for(int i = this.interceptorIndex; i >= 0; i--) {
      HandlerInterceptor interceptor = this.interceptorList.get(i);
      try {
        // HandlerInterceptor接口的afterCompletion方法
        interceptor.afterCompletion(request, response, this.handler, ex);
      }
      catch(Throwable ex2) {
        logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
      }
    }
  }
}