1. 程式人生 > >SpringMVC原始碼剖析5:訊息轉換器HttpMessageConverter與@ResponseBody註解

SpringMVC原始碼剖析5:訊息轉換器HttpMessageConverter與@ResponseBody註解

轉自 SpringMVC關於json、xml自動轉換的原理研究[附帶原始碼分析]

本系列文章首發於我的個人部落格:https://h2pl.github.io/

歡迎閱覽我的CSDN專欄:Spring原始碼解析 https://blog.csdn.net/column/details/21851.html

部分程式碼會放在我的的Github:https://github.com/h2pl/

目錄

  • 前言
  • 現象
  • 原始碼分析
  • 例項講解
  • 關於配置
  • 總結
  • 參考資料

前言

SpringMVC是目前主流的Web MVC框架之一。 

如果有同學對它不熟悉,那麼請參考它的入門blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html

現象

本文使用的demo基於maven,是根據入門blog的例子繼續寫下去的。

我們先來看一看對應的現象。 我們這裡的配置檔案 *-dispatcher.xml中的關鍵配置如下(其他常規的配置檔案不在講解,可參考本文一開始提到的入門blog):

(檢視配置省略)

<mvc:resources location="/static/" mapping="/static/**"/>
<mvc:annotation-driven/>
<context:component-scan base-package="org.format.demo.controller"/>

pom中需要有以下依賴(Spring依賴及其他依賴不顯示):

<dependency>
  <groupId>org.codehaus.jackson</groupId>
  jackson-core-asl
  <version>1.9.13</version>
</dependency>
<dependency>
  <groupId>org.codehaus.jackson</groupId>
  jackson-mapper-asl
  <version>1.9.13</version>
</dependency>

這個依賴是json序列化的依賴。

ok。我們在Controller中新增一個method:

@RequestMapping("/xmlOrJson")
@ResponseBody public Map

直接訪問地址:

我們看到,短短几行配置。使用@ResponseBody註解之後,Controller返回的物件 自動被轉換成對應的json資料,在這裡不得不感嘆SpringMVC的強大。

我們好像也沒看到具體的配置,唯一看到的就是*-dispatcher.xml中的一句配置:

那麼spring到底是如何實現java物件到json物件的自動轉換的呢? 為什麼轉換成了json資料,如果想轉換成xml資料,那該怎麼辦?

原始碼分析

本文使用的spring版本是4.0.2。  

在講解

HttpMessageConverter介面就是Spring提供的http訊息轉換介面。有關這方面的知識大家可以參考"參考資料"中的第二條連結,裡面講的很清楚。

下面開始分析

這句程式碼在spring中的解析類是:

在AnnotationDrivenBeanDefinitionParser原始碼的152行parse方法中:

分別例項化了RequestMappingHandlerMapping,ConfigurableWebBindingInitializer,RequestMappingHandlerAdapter等諸多類。

其中RequestMappingHandlerMapping和RequestMappingHandlerAdapter這兩個類比較重要。

RequestMappingHandlerMapping處理請求對映的,處理@RequestMapping跟請求地址之間的關係。

RequestMappingHandlerAdapter是請求處理的介面卡,也就是請求之後處理具體邏輯的執行,關係到哪個類的哪個方法以及轉換器等工作,這個類是我們講的重點,其中它的屬性messageConverters是本文要講的重點。

私有方法:getMessageConverters

從程式碼中我們可以,RequestMappingHandlerAdapter設定messageConverters的邏輯:

1.如果

message-converters的子節點配置如下:

<mvc:annotation-driven>
  <mvc:message-converters>
    <bean class="org.example.MyHttpMessageConverter"/>
    <bean class="org.example.MyOtherHttpMessageConverter"/>
  </mvc:message-converters>
</mvc:annotation-driven>

2.message-converters子節點不存在或它的屬性register-defaults為true的話,加入其他的轉換器:ByteArrayHttpMessageConverter、StringHttpMessageConverter、ResourceHttpMessageConverter等。

我們看到這麼一段:

這些boolean屬性是哪裡來的呢,它們是AnnotationDrivenBeanDefinitionParser的靜態變數。

 其中ClassUtils中的isPresent方法如下:

看到這裡,讀者應該明白了為什麼本文一開始在pom檔案中需要加入對應的jackson依賴,為了讓json轉換器jackson成為預設轉換器之一。

下面我們看如何通過訊息轉換器將java物件進行轉換的。

RequestMappingHandlerAdapter在進行handle的時候,會委託給HandlerMethod(具體由子類ServletInvocableHandlerMethod處理)的invokeAndHandle方法進行處理,這個方法又轉接給HandlerMethodReturnValueHandlerComposite處理。

HandlerMethodReturnValueHandlerComposite維護了一個HandlerMethodReturnValueHandler列表。HandlerMethodReturnValueHandler是一個對返回值進行處理的策略介面,這個介面非常重要。關於這個介面的細節,請參考樓主的另外一篇部落格:http://www.cnblogs.com/fangjian0423/p/springMVC-request-param-analysis.html。然後找到對應的HandlerMethodReturnValueHandler對結果值進行處理。

最終找到RequestResponseBodyMethodProcessor這個Handler(由於使用了@ResponseBody註解)。

RequestResponseBodyMethodProcessor的supportsReturnType方法:

然後使用handleReturnValue方法進行處理:

我們看到,這裡使用了轉換器。  

具體的轉換方法:

至於為何是請求頭部的Accept資料,讀者可以進去debug這個getAcceptableMediaTypes方法看看。 我就不羅嗦了~~~

 ok。至此,我們走遍了所有的流程。

現在,回過頭來看。為什麼一開始的demo輸出了json資料?

我們來分析吧。

由於我們只配置了

很明顯,我們看到了2個xml和1個json轉換器。 要看能不能轉換,得看HttpMessageConverter介面的public boolean canWrite(Class<?> clazz, MediaType mediaType)方法是否返回true來決定的。

我們先分析SourceHttpMessageConverter:

它的canWrite方法被父類AbstractHttpMessageConverter重寫了。

發現SUPPORTED_CLASSES中沒有Map類(本文demo返回的是Map類),因此不支援。

下面看Jaxb2RootElementHttpMessageConverter:

這個類直接重寫了canWrite方法。

需要有XmlRootElement註解。 很明顯,Map類當然沒有。

最終MappingJackson2HttpMessageConverter匹配,進行json轉換。(為何匹配,請讀者自行檢視原始碼)

例項講解

 我們分析了轉換器的轉換過程之後,下面就通過例項來驗證我們的結論吧。

首先,我們先把xml轉換器實現。

之前已經分析,預設的轉換器中是支援xml的。下面我們加上註解試試吧。

由於Map是jdk原始碼中的部分,因此我們用Employee來做demo。

因此,Controller加上一個方法:

@RequestMapping("/xmlOrJsonSimple")
@ResponseBody public Employee xmlOrJsonSimple() { return employeeService.getById(1);
}

實體中加上@XmlRootElement註解

結果如下:

我們發現,解析成了xml。

這裡為什麼解析成xml,而不解析成json呢?

之前分析過,訊息轉換器是根據class和mediaType決定的。

我們使用firebug看到:

我們發現Accept有xml,沒有json。因此解析成xml了。

我們再來驗證,同一地址,HTTP頭部不同Accept。看是否正確。

$.ajax({
    url: "${request.contextPath}/employee/xmlOrJsonSimple",
    success: function(res) {
        console.log(res);
    },
    headers: { "Accept": "application/xml" }
});

$.ajax({
    url: "${request.contextPath}/employee/xmlOrJsonSimple",
    success: function(res) {
        console.log(res);
    },
    headers: { "Accept": "application/json" }
});

驗證成功。

關於配置

如果不想使用

為何會覆蓋,請參考樓主的另外一篇部落格:http://www.cnblogs.com/fangjian0423/p/spring-Ordered-interface.html

    `` 

或者如果只想換messageConverters的話。

<mvc:annotation-driven>
  <mvc:message-converters>
    <bean class="org.example.MyHttpMessageConverter"/>
    <bean class="org.example.MyOtherHttpMessageConverter"/>
  </mvc:message-converters>
</mvc:annotation-driven>

如果還想用其他converters的話。

以上是spring-mvc jar包中的converters。

這裡我們使用轉換xml的MarshallingHttpMessageConverter。

這個converter裡面使用了marshaller進行轉換

我們這裡使用XStreamMarshaller。  

json沒有轉換器,返回406.

至於xml格式的問題,大家自行解決吧。 這裡用的是XStream~。

使用這種方式,pom別忘記了加入xstream的依賴:

<dependency>
  <groupId>com.thoughtworks.xstream</groupId>
  xstream
  <version>1.4.7</version>
</dependency>

總結

 寫了這麼多,可能讀者覺得有點羅嗦。 畢竟這也是自己的一些心得,希望都能說出來與讀者共享。

剛接觸SpringMVC的時候,發現這種自動轉換機制很牛逼,但是一直沒有研究它的原理,目前,算是了了一個小小心願吧,SpringMVC還有很多內容,以後自己研究其他內容的時候還會與大家一起共享的。

文章難免會出現一些錯誤,希望讀者們能指明出來。

參考資料

http://my.oschina.net/HeliosFly/blog/205343

http://my.oschina.net/lichhao/blog/172562

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html

詳解RequestBody和@ResponseBody註解

概述 在SpringMVC中,可以使用@RequestBody和@ResponseBody兩個註解,分別完成請求報文到物件和物件到響應報文的轉換,底層這種靈活的訊息轉換機制,就是Spring3.x中新引入的HttpMessageConverter即訊息轉換器機制。

Http請求的抽象 還是回到請求-響應,也就是解析請求體,然後返回響應報文這個最基本的Http請求過程中來。我們知道,在servlet標準中,可以用javax.servlet.ServletRequest介面中的以下方法:

public ServletInputStream getInputStream() throws IOException; 

來得到一個ServletInputStream。這個ServletInputStream中,可以讀取到一個原始請求報文的所有內容。同樣的,在javax.servlet.ServletResponse介面中,可以用以下方法:

public ServletOutputStream getOutputStream() throws IOException;

來得到一個ServletOutputStream,這個ServletOutputSteam,繼承自java中的OutputStream,可以讓你輸出Http的響應報文內容。

讓我們嘗試著像SpringMVC的設計者一樣來思考一下。我們知道,Http請求和響應報文字質上都是一串字串,當請求報文來到java世界,它會被封裝成為一個ServletInputStream的輸入流,供我們讀取報文。響應報文則是通過一個ServletOutputStream的輸出流,來輸出響應報文。

我們從流中,只能讀取到原始的字串報文,同樣,我們往輸出流中,也只能寫原始的字元。而在java世界中,處理業務邏輯,都是以一個個有業務意義的物件為處理維度的,那麼在報文到達SpringMVC和從SpringMVC出去,都存在一個字串到java物件的阻抗問題。這一過程,不可能由開發者手工轉換。我們知道,在Struts2中,採用了OGNL來應對這個問題,而在SpringMVC中,它是HttpMessageConverter機制。我們先來看兩個介面。

HttpInputMessage 這個類是SpringMVC內部對一次Http請求報文的抽象,在HttpMessageConverter的read()方法中,有一個HttpInputMessage的形參,它正是SpringMVC的訊息轉換器所作用的受體“請求訊息”的內部抽象,訊息轉換器從“請求訊息”中按照規則提取訊息,轉換為方法形參中宣告的物件。

package org.springframework.http;

import java.io.IOException;
import java.io.InputStream;

public interface HttpInputMessage extends HttpMessage {

    InputStream getBody() throws IOException;

}

HttpOutputMessage 這個類是SpringMVC內部對一次Http響應報文的抽象,在HttpMessageConverter的write()方法中,有一個HttpOutputMessage的形參,它正是SpringMVC的訊息轉換器所作用的受體“響應訊息”的內部抽象,訊息轉換器將“響應訊息”按照一定的規則寫到響應報文中。

package org.springframework.http;

import java.io.IOException;
import java.io.OutputStream;

public interface HttpOutputMessage extends HttpMessage {

    OutputStream getBody() throws IOException;

}

HttpMessageConverter 對訊息轉換器最高層次的介面抽象,描述了一個訊息轉換器的一般特徵,我們可以從這個介面中定義的方法,來領悟Spring3.x的設計者對這一機制的思考過程。

package org.springframework.http.converter;

import java.io.IOException;
import java.util.List;

import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;

public interface HttpMessageConverter<T> {

    boolean canRead(Class<?> clazz, MediaType mediaType);

    boolean canWrite(Class<?> clazz, MediaType mediaType);

    List<MediaType> getSupportedMediaTypes();

    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;

    void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;

}

HttpMessageConverter介面的定義出現了成對的canRead(),read()和canWrite(),write()方法,MediaType是對請求的Media Type屬性的封裝。舉個例子,當我們聲明瞭下面這個處理方法。

@RequestMapping(value="/string", method=RequestMethod.POST)
public @ResponseBody String readString(@RequestBody String string) {
    return "Read string '" + string + "'";
}

在SpringMVC進入readString方法前,會根據@RequestBody註解選擇適當的HttpMessageConverter實現類來將請求引數解析到string變數中,具體來說是使用了StringHttpMessageConverter類,它的canRead()方法返回true,然後它的read()方法會從請求中讀出請求引數,繫結到readString()方法的string變數中。

當SpringMVC執行readString方法後,由於返回值標識了@ResponseBody,SpringMVC將使用StringHttpMessageConverter的write()方法,將結果作為String值寫入響應報文,當然,此時canWrite()方法返回true。

我們可以用下面的圖,簡單描述一下這個過程。

RequestResponseBodyMethodProcessor 將上述過程集中描述的一個類是org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor,這個類同時實現了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler兩個介面。前者是將請求報文繫結到處理方法形參的策略介面,後者則是對處理方法返回值進行處理的策略介面。兩個介面的原始碼如下:

package org.springframework.web.method.support;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;

public interface HandlerMethodArgumentResolver {

    boolean supportsParameter(MethodParameter parameter);

    Object resolveArgument(MethodParameter parameter,
                           ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest,
                           WebDataBinderFactory binderFactory) throws Exception;

}

package org.springframework.web.method.support;

import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;

public interface HandlerMethodReturnValueHandler {

    boolean supportsReturnType(MethodParameter returnType);

    void handleReturnValue(Object returnValue,
                           MethodParameter returnType,
                           ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest) throws Exception;

}

RequestResponseBodyMethodProcessor這個類,同時充當了方法引數解析和返回值處理兩種角色。我們從它的原始碼中,可以找到上面兩個介面的方法實現。

對HandlerMethodArgumentResolver介面的實現:

public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(RequestBody.class);
}

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

    Object argument = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());

    String name = Conventions.getVariableNameForParameter(parameter);
    WebDataBinder binder = binderFactory.createBinder(webRequest, argument, name);

    if (argument != null) {
        validate(binder, parameter);
    }

    mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());

    return argument;
}

對HandlerMethodReturnValueHandler介面的實現

public boolean supportsReturnType(MethodParameter returnType) {
    return returnType.getMethodAnnotation(ResponseBody.class) != null;
}

    public void handleReturnValue(Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException {

    mavContainer.setRequestHandled(true);
    if (returnValue != null) {
        writeWithMessageConverters(returnValue, returnType, webRequest);
    }
}

看完上面的程式碼,整個HttpMessageConverter訊息轉換的脈絡已經非常清晰。因為兩個介面的實現,分別是以是否有@RequestBody和@ResponseBody為條件,然後分別呼叫HttpMessageConverter來進行訊息的讀寫。

如果你想問,怎麼樣跟蹤到RequestResponseBodyMethodProcessor中,請你按照前面幾篇博文的思路,然後到這裡spring-mvc-showcase下載原始碼回來,對其中HttpMessageConverter相關的例子進行debug,只要你肯下功夫,相信你一定會有屬於自己的收穫的。

思考 張小龍在談微信的本質時候說:“微信只是個平臺,訊息在其中流轉”。在我們對SpringMVC原始碼分析的過程中,我們可以從HttpMessageConverter機制中領悟到類似的道理。在SpringMVC的設計者眼中,一次請求報文和一次響應報文,分別被抽象為一個請求訊息HttpInputMessage和一個響應訊息HttpOutputMessage。

處理請求時,由合適的訊息轉換器將請求報文繫結為方法中的形參物件,在這裡,同一個物件就有可能出現多種不同的訊息形式,比如json和xml。同樣,當響應請求時,方法的返回值也同樣可能被返回為不同的訊息形式,比如json和xml。

在SpringMVC中,針對不同的訊息形式,我們有不同的HttpMessageConverter實現類來處理各種訊息形式。但是,只要這些訊息所蘊含的“有效資訊”是一致的,那麼各種不同的訊息轉換器,都會生成同樣的轉換結果。至於各種訊息間解析細節的不同,就被遮蔽在不同的HttpMessageConverter實現類中了。

相關推薦

SpringMVC原始碼剖析5訊息轉換HttpMessageConverter@ResponseBody註解

轉自 SpringMVC關於json、xml自動轉換的原理研究[附帶原始碼分析] 本系列文章首發於我的個人部落格:https://h2pl.github.io/ 歡迎閱覽我的CSDN專欄:Spring原始碼解析 https://blog.csdn.net/column/details/21851.html 部

JavaEE開發之SpringMVC中的自定義訊息轉換檔案上傳

上篇部落格我們詳細的聊了《》,本篇部落格依然是JavaEE開發中的內容,我們就來聊一下SpringMVC中的自定義訊息轉發器(HttpMessageConverter)和SpringMVC中的檔案上傳。訊息轉發器在日常開發中是比較常用的,其可以靈活的將使用者發過來的訊息按照自定義的格式進行解析,然後將解析的資

Boost原始碼剖析型別分類——type_traits

1. 分派 下面有一個模板函式,假設一個動物收容組織提供了它,他們接受所有無家可歸的可憐的小動物,於是他們向外界提供了一個函式接受註冊。函式看起來像這樣: AcceptAnimals(T animal) { ... }; 但是,如果他們想將

Spring MVC使用fastjson做訊息轉換預設Jackson的區別

spring mvc支援自定義HttpMessageConverter接收JSON格式的資料,使用fastjson作為訊息裝換器,只需要在spring的配置檔案中加入如下配置程式碼(需引入fast

java中的訊息轉換--HttpMessageConverter

目錄: HTTP請求響應模型;基於javax-servlet-api的請求、響應處理介面;基於SpringMVC的請求、響應介面; HTTP請求響應模型 一般情況下,我們使用的最簡單的HTTP請求響應模型就是B/S模型了。 1、客戶端發出http請求,http請求中包

SpringMVC原始碼分析3DispatcherServlet的初始化請求轉發

在我們第一次學Servlet程式設計,學java web的時候,還沒有那麼多框架。我們開發一個簡單的功能要做的事情很簡單,就是繼承HttpServlet,根據需要重寫一下doGet,doPost方法,跳轉到我們定義好的jsp頁面。Servlet類編寫完之後在web.xml裡註冊這個Servlet類。 除此之外

SpringMVC源碼剖析(五)-消息轉換HttpMessageConverter

var blog tro 20px 請求參數 wrap -i reat return SpringMVC源碼剖析(五)-消息轉換器HttpMessageConverter目錄[-]概述Http請求的抽象HttpInputMessageHttpOutputMessageHtt

SpringMVC 5處理器攔截

※. 處理器攔截器     SpringMVC的處理器攔截器類似於Servlet 開發中的過濾器Filter,     用於對處理器進行預處理和後處理。     1)常見應用場景         1、日誌記錄         2、許可權檢查         3、效能監控  

SpringMVC原始碼剖析(七)- HandlerExceptionResolver異常解析家族揭祕

在Spring MVC中,所有用於處理在請求處理過程中丟擲的異常,都要實現HandlerExceptionResolver介面。HandlerExceptionResolver是Spring MVC提供的非常好的通用異常處理工具,不過需要注意的是,它只能處理請求過程中丟擲的

SpringMVC訊息轉換

1. HttpMessageConverter訊息轉換器 概念:將請求資訊----》訊息轉換器-----》物件  物件--------》訊息轉換器-----》響應資訊體訊息轉換器它只針對@RequestBody/@ResponseBody註解或HttpEntity/Resp

SpringMVC原始碼剖析】 前奏夢開始的地方

SpringMVC原始碼剖析 縱覽:SpringMVC處理請求 本節作為SpringMVC原始碼剖析的基本,主要介紹以下三方面內容。 1.核心DispatcherServlet的載入方式 2.DispatcherServlet初始化過程 3.Sp

擴充套件springMVC訊息轉換來支援jsonp資料格式

1.JSONP是用來解決json跨域問題的技術。即將傳輸的json轉化為js指令碼。 Callback是JSONP的實現的一種方式,例:getData([{"id":1,"title":"XXXX"},{"id":2,"title":"YYYYY"}])。當瀏覽器讀取到fu

NSQ原始碼剖析(一)NSQD主要結構方法和訊息生產消費過程

目錄 1 概述 2 主要結構體及方法 2.1 NSQD 2.2 tcpServer 2.3 protocolV2 2.4 clientV2 2.5 Topic

Caffe框架原始碼剖析(5)—卷積層ConvolutionLayer

ConvolutionLayer是BaseConvolutionLayer的子類,功能較為簡單。類中不包含成員變數,僅包含幾個虛擬函式的實現。 conv_layer.hpp標頭檔案的定義如下: template <typename Dtype> class Convoluti

HttpMessageConverter訊息轉換

版權宣告:本文為博主原創文章,無需授權即可轉載,甚至無需保留以上版權宣告,轉載時請務必註明作者。 https://blog.csdn.net/weixin_43453386/article/details/83615829 HttpMessageConverter訊息轉換器

spring boot 專案重新搭建----------mvc配置型別轉換

實現WebMvcConfigurer介面: 1.configurePathMatch路徑配置: setUseSuffixPatternMatch : 設定是否是字尾模式匹配,如“/user”是否匹配/user.*,預設為true setUseTrailingSlash

spring boot 自定義訊息轉換

需求分析:在請求返回中,想統一返回的資料格式。在controller層中通過aop中的環繞通知實現了這個功能。但是在轉換的過程中,提示沒有自定義的格式轉換器。所有我們需要自己實現這個轉換器。 實現方式:通過繼承 AbstractHttpMessageConverter這個類來實現功能。 pu

自定義一個自己的訊息轉換

1.自定義個訊息轉換器,支援自己定義的content-type型別.首先準備好測試的controller類; @PostMapping("/") public Object test(@RequestBody UserInfo userInfo) { return

《STL原始碼剖析》筆記-配接adapters

配接器adapters在STL中起到轉換、連線的作用,包括仿函式配接器(function adapter)、容器配接器(container adapter)、迭代器配接器(iterator adapter)。 容器配接器 迭代器配接器 STL提供的迭代器配接

Sagit.Framework For IOS 開發框架入門教程5訊息彈窗STMsgBox

前言: 昨天剛寫了一篇IT連創業的文章:IT連創業系列:產品設計之答題模組,(歡迎大夥關注!) 感覺好久沒寫IOS的文章了,今天趁機,來補一篇,Sagit的教程。 今天主要是分享訊息彈窗功能,即STMsgBox的用法: STMsgBox為彈窗相關的功能的原始碼。 1、對外API功能呼叫說明: