MENU

SpringBoot的学习第三篇

{timeline}

{timeline-item color="#19be6b"}

2022-05-29 17:05:15 星期日
{collapse}
{collapse-item label="

响应处理" open}

【源码分析】-ReturnValueHandler原理

l3r2u0o6.png

假设给前端自动返回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(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
            Object... providedArgs) 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 {

        //selectHandler()实现在下面
        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);
    }

}
【源码分析】-HTTPMessageConverter原理
  • 返回值处理器ReturnValueHandler原理:

    • 返回值处理器判断是否支持这种类型返回值 supportsReturnType
    • 返回值处理器调用 handleReturnValue 进行处理
    • RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
    • 利用 MessageConverters 进行处理 将数据写为json

      • 内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
      • 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
      • SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
      • 得到MappingJackson2HttpMessageConverter可以将对象写为json
      • 利用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 : 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_APPLICATION_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, 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
l3r32dka.png

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, false);
            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 TransformerFactory implementation is available
            }
        }
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    }

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

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 关联起来?请看下节。

ReturnValueHandlerMappingJackson2HttpMessageConverter关联
再次回顾RequestMappingHandlerAdapter

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean {
    
    ...
    @Nullable
    private HandlerMethodReturnValueHandlerComposite returnValueHandlers;//我们关注的returnValueHandlers
    
       
    @Override
    @Nullable//本方法在AbstractHandlerMethodAdapter
    public final ModelAndView 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 = getDataBinderFactory(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
        
        ...
        
        if (this.returnValueHandlers == null) {//赋值returnValueHandlers
            List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
            this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
        }
    }
    
    private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);

        ...
        // 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 TransformerFactory implementation is available
            }
        }
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    }

    ...
              
}

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

【源码分析】-内容协商原理

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

引入XML依赖:

 <dependency>
     <groupId>com.fasterxml.jackson.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. HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
  3. 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
  4. 找到支持操作Person的converter,把converter支持的媒体类型统计出来。
  5. 客户端需要application/xml,服务端有10种MediaType。
  6. 进行内容协商的最佳匹配媒体类型
  7. 用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。

    //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 : 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_APPLICATION_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, 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;
                     }
                 }
             }
             ...
         }
    【源码分析】-基于请求参数的内容协商原理

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

获取客户端(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);
              MediaType.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或xml格式的数据。

    【源码分析】-自定义MessageConverter

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

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

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

@Configuration(proxyBeanMethods = false)
public class WebConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {

            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
                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) throws IOException, HttpMessageNotReadableException {
        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发请求,返回自定义协议数据  [appliaction/x-guigu]   xxxxConverter
     *          属性值1;属性值2;
     *
     * 步骤:
     * 1、添加自定义的MessageConverter进系统底层
     * 2、系统底层就会统计出所有MessageConverter能操作哪些类型
     * 3、客户端内容协商 [guigu--->guigu]
     *
     * 作业:如何以参数的方式进行内容协商
     * @return
     */
    @ResponseBody  //利用返回值处理器里面的消息转换器进行处理
    @GetMapping(value = "/test/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 ),将返回自定义协议数据的写出。

响应处理-【源码分析】-浏览器与PostMan内容协商完全适配

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

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

@Configuration(proxyBeanMethods = false)
public class WebConfig /*implements WebMvcConfigurer*/ {

    //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> mediaTypes = 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 headeStrategy = new HeaderContentNegotiationStrategy();

                configurer.strategies(Arrays.asList(parameterStrategy, headeStrategy));
            }
        }
    }
    
    ...
    
}

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

{/collapse-item}
{collapse-item label="

视图解析-Thymeleaf初体验" }

{callout color="#f0ad4e"}
hymeleaf is a modern server-side Java template engine for both web and standalone environments.

Thymeleaf’s main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.

With modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.—— Link
{/callout}
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 ThymeleafAutoConfiguration {
    ...
}

自动配好的策略

  • 所有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
    基本语法

  • 表达式

    表达式名字语法用途
    变量取值${…}获取请求域、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>
    
  • 属性优先级
    l3r4gdwy.png
    官方文档 - 10 Attribute Precedence

{/collapse-item}
{collapse-item label="

web实验-后台管理系统基本功能"}

项目创建
使用IDEA的Spring Initializr。

  • thymeleaf、
  • web-starter、
  • devtools、
  • lombok
    登陆页面
    /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>

    登录控制层

    @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;
}

{/collapse-item}
{/collapse}

{/timeline-item}
{timeline-item color="#ed4014"}
学习结束!!!
{/timeline-item}
{/timeline}

{mtitle title="结束"/}

Leave a Comment