SpringBoot,通过RestTemplate 或者 Spring Cloud Feign,上传文件(支持多文件上传),服务端接口是MultipartFile接收。

将文件的字节流,放入ByteArrayResource中,并重写getFilename方法。

然后将ByteArrayResource放入MultiValueMap中(如果是Feign调用,方法里传参就是MultiValueMap),

然后进行上传时,Spring会自动识别到Map中的文件数据,然后通过FormHttpMessageConverter,将数据转成form表单型的multipart/formdata请求。

这里有个坑!

Spring web 4里面的FormHttpMessageConverter在将文件转成formdata时,会将文件名转成Byte[],但是使用的编码却是写死 US-ASCII,该编码不支持中文,使用该编码转换后,中文变成?号,是无法转回来的。

我想到的解决方法:

1.将spring版本升到5,Spring5里面,该编码是可以传入修改的。Springboot,默认UTF8

2.客户端进行一次编码,比如URLEncoder。然后服务端进行Decoder。

贴部分代码:

Feign

调用方,使用Spring 的MultiValueMap类,将文件File 转成 Resource,如果多个文件,则可以循环 用 add 方法,放入一个key下。

MultiValueMap是允许一key多值的。

或者,将多个Resource放入list,然后将list  put 进 map中。

接收方

接收,可以用

(MultiValueMap map)

如果有其他的 值。

则是(MultiValueMap map,String XXX,String  AAA)

多文件,则是

(MultiValueMap[] map,String XXX,String  AAA)

或者用对象接收,也可以,不需要 @RequestBody 注解,这个注解是接收 http body里的json的。

(Bean bean),bean对象里,则是  MultiValueMap[] map,String XXX,String  AAA

======================  分割线  =================================

我在查询Feign上传文件时,还查到了另一种方式,就是专门给Feign方式提供的feign form相关Jar包,

引入Jar包后,然后进行相关配置,便可以在Feign方法中,参数直接传递MultipartFile。

该方法,或许也可以解决Spring4的编码问题。

===================     分隔线:2018-11-8补充 关于 feign form用法      ====================

先引入相关jar:

<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.2.2</version>
</dependency>

  

@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
} @Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder();
}

  

feign 调用方法 写法 :

save(@RequestPart MultipartFile file,@RequestParam("khbh") String khbh)

但是如果,参数多时,一个一个写较为麻烦,可以用
save(Map<String,?> param)

但是,经过测试,发现如果 map中value是null,会出现异常。(原因好像是因为,在将 值写入 formdata时,没有null判断)

========== 上面的用的 feign form

下面 有从网络上查到的,是类似于  feign form的解决方式:
http://b-l-east.iteye.com/blog/2373462
糞坑-SpringCloud中使用Feign的坑
示例如下:
@FeignClient("service-resource")
//@RequestMapping("/api/test")
public interface TestResourceItg { @RequestMapping(value = "/api/test/raw", method = RequestMethod.POST, consumes = "application/x-www-form-urlencoded")
public String raw1(@PathVariable("subject") String subject, // 标题
@RequestParam("content") String content); // 内容 } 说明:
*使用RequestMapping中的consumes指定生成的请求的Content-Type
*RequestParam指定的参数会拼接在URL之后,如: ?name=xxx&age=18
*PathVariable指定的参数会放到一个LinkedHashMap<String, ?>传入到feign的Encoder中进行处理,而在Spring中实现了该接口的Encoder为SpringEncoder,而该实现又会使用Spring中的HttpMessageConverter进行请求体的写入。 坑:
*不要在接口类名上使用RequestMapping,虽然可以使用,但同时SpringMVC会把该接口的实例当作Controller开放出去,这个可以在启动的Mapping日志中查看到
*使用默认的SpringEncoder,在不指定consumes时,PathVariable中的参数会生成JSON字符串发送,且默认情况下不支持Form表单的生成方式,原因为:FormHttpMessageConverter只能处理MultiValueMap,而使用PathVariable参数被放在了HashMap中。默认更不支持文件上传。其实已经有支持处理各种情况的HttpMessageConverter存在。 填坑:
*支持Form表单提交:只需要编写一个支持Map的FormHttpMessageConverter即可,内部可调用FormHttpMessageConverter的方法简化操作。
*支持文件上传:只需要把要上传的文件封装成一个Resource(该Resource一定要实现filename接口,这个是把请求参数解析成文件的标识),使用默认的ResourceHttpMessageConverter处理即可。
*支持处理MultipartFile参数:编写一个支持MultipartFile的MultipartFileHttpMessageConverter即可,内部可调用ResourceHttpMessageConverter实现,同时注意需要将其添加至FormHttpMessageConverter的Parts中,并重写FormHttpMessageConverter的getFilename方法支持从MultipartFile中获取filename
*所有的HttpMessageConverter直接以@Bean的方式生成即可,spring会自动识别添加 完美支持表单和文件上传:
方案一:
使用附件中的MapFormHttpMessageConverter.java和MultipartFileHttpMessageConverter.java
在Spring中进行如下配置即可
@Bean
public MapFormHttpMessageConverter mapFormHttpMessageConverter(MultipartFileHttpMessageConverter multipartFileHttpMessageConverter) {
MapFormHttpMessageConverter mapFormHttpMessageConverter = new MapFormHttpMessageConverter();
mapFormHttpMessageConverter.addPartConverter(multipartFileHttpMessageConverter);
return mapFormHttpMessageConverter;
} @Bean
public MultipartFileHttpMessageConverter multipartFileHttpMessageConverter() {
return new MultipartFileHttpMessageConverter();
}
方案二:
使用FeignSpringFormEncoder.java
在Spring中配置如下:
@Bean
public Encoder feignEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
return new FeignSpringFormEncoder(messageConverters);
} 推荐使用方案一
方案二为参考https://github.com/pcan/feign-client-test而来,未测

  

上面方案中所用代码,贴在下面:

package com.access.service.saas.cmpt.utl;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.web.multipart.MultipartFile; /**
* @author elvis.xu
* @since 2017-05-09 11:17
*/
public class MultipartFileHttpMessageConverter implements HttpMessageConverter<MultipartFile> {
protected List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
protected ResourceHttpMessageConverter resourceHttpMessageConverter; public MultipartFileHttpMessageConverter() {
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
resourceHttpMessageConverter = new ResourceHttpMessageConverter();
} public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
this.supportedMediaTypes = supportedMediaTypes;
} @Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.unmodifiableList(this.supportedMediaTypes);
} @Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
} @Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (!MultipartFile.class.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
return true;
}
for (MediaType supportedMT : getSupportedMediaTypes()) {
if (supportedMT.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
} @Override
public MultipartFile read(Class<? extends MultipartFile> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
} @Override
public void write(MultipartFile file, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
MultipartFileResource multipartFileResource = new MultipartFileResource(file);
resourceHttpMessageConverter.write(multipartFileResource, contentType, outputMessage);
} public static class MultipartFileResource extends InputStreamResource { private final String filename;
private final long size; public MultipartFileResource(MultipartFile multipartFile) throws IOException {
this(multipartFile.getOriginalFilename(), multipartFile.getSize(), multipartFile.getInputStream());
} public MultipartFileResource(String filename, long size, InputStream inputStream) {
super(inputStream);
this.size = size;
this.filename = filename;
} @Override
public String getFilename() {
return this.filename;
} @Override
public InputStream getInputStream() throws IOException, IllegalStateException {
return super.getInputStream(); //To change body of generated methods, choose Tools | Templates.
} @Override
public long contentLength() throws IOException {
return size;
} }
}

  

package com.access.service.saas.cmpt.utl;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Map; import feign.RequestTemplate;
import feign.codec.EncodeException; import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.support.SpringEncoder;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.multipart.MultipartFile; /**
* @author elvis.xu
* @since 2017-04-11 15:33
*/
public class FeignSpringFormEncoder extends SpringEncoder { protected ObjectFactory<HttpMessageConverters> messageConverters;
protected HttpHeaders multipartHeaders = new HttpHeaders();
public static final Charset UTF_8 = Charset.forName("UTF-8"); public FeignSpringFormEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
super(messageConverters);
this.messageConverters = messageConverters;
multipartHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
} protected static boolean isFormRequest(Type type) {
return MAP_STRING_WILDCARD.equals(type);
} protected static boolean isMultipart(Object body, Type bodyType) {
if (isFormRequest(bodyType)) {
Map<String, ?> map = (Map<String, ?>) body;
for (Map.Entry<String, ?> entry : map.entrySet()) {
Object value = entry.getValue();
if (isMultipartFile(value) || isMultipartFileArray(value)) {
return true;
}
}
}
return false;
} protected static boolean isMultipartFile(Object obj) {
return obj instanceof MultipartFile;
} protected static boolean isMultipartFileArray(Object o) {
return o != null && o.getClass().isArray() && MultipartFile.class.isAssignableFrom(o.getClass().getComponentType());
} @Override
public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
if (isMultipart(requestBody, bodyType)) {
encodeMultipartFormRequest((Map<String, ?>) requestBody, request);
} else {
super.encode(requestBody, bodyType, request);
}
} /**
* Encodes the request as a multipart form. It can detect a single {@link MultipartFile}, an
* array of {@link MultipartFile}s, or POJOs (that are converted to JSON).
*
* @param formMap
* @param template
* @throws EncodeException
*/
private void encodeMultipartFormRequest(Map<String, ?> formMap, RequestTemplate template) throws EncodeException {
if (formMap == null) {
throw new EncodeException("Cannot encode request with null form.");
}
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
for (Map.Entry<String, ?> entry : formMap.entrySet()) {
Object value = entry.getValue();
if (isMultipartFile(value)) {
map.add(entry.getKey(), encodeMultipartFile((MultipartFile) value));
} else if (isMultipartFileArray(value)) {
encodeMultipartFiles(map, entry.getKey(), Arrays.asList((MultipartFile[]) value));
} else {
map.add(entry.getKey(), encodeJsonObject(value));
}
}
encodeRequest(map, multipartHeaders, template);
} /**
* Wraps a single {@link MultipartFile} into a {@link HttpEntity} and sets the
* {@code Content-type} header to {@code application/octet-stream}
*
* @param file
* @return
*/
private HttpEntity<?> encodeMultipartFile(MultipartFile file) {
HttpHeaders filePartHeaders = new HttpHeaders();
filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
try {
Resource multipartFileResource = new MultipartFileResource(file.getOriginalFilename(), file.getSize(), file.getInputStream());
return new HttpEntity<>(multipartFileResource, filePartHeaders);
} catch (IOException ex) {
throw new EncodeException("Cannot encode request.", ex);
}
} /**
* Fills the request map with {@link HttpEntity}s containing the given {@link MultipartFile}s.
* Sets the {@code Content-type} header to {@code application/octet-stream} for each file.
*
* @param map the current request map.
* @param name the name of the array field in the multipart form.
* @param files
*/
private void encodeMultipartFiles(LinkedMultiValueMap<String, Object> map, String name, List<? extends MultipartFile> files) {
HttpHeaders filePartHeaders = new HttpHeaders();
filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
try {
for (MultipartFile file : files) {
Resource multipartFileResource = new MultipartFileResource(file.getOriginalFilename(), file.getSize(), file.getInputStream());
map.add(name, new HttpEntity<>(multipartFileResource, filePartHeaders));
}
} catch (IOException ex) {
throw new EncodeException("Cannot encode request.", ex);
}
} /**
* Wraps an object into a {@link HttpEntity} and sets the {@code Content-type} header to
* {@code application/json}
*
* @param o
* @return
*/
private HttpEntity<?> encodeJsonObject(Object o) {
HttpHeaders jsonPartHeaders = new HttpHeaders();
jsonPartHeaders.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(o, jsonPartHeaders);
} /**
* Calls the conversion chain actually used by
* {@link org.springframework.web.client.RestTemplate}, filling the body of the request
* template.
*
* @param value
* @param requestHeaders
* @param template
* @throws EncodeException
*/
private void encodeRequest(Object value, HttpHeaders requestHeaders, RequestTemplate template) throws EncodeException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
HttpOutputMessage dummyRequest = new HttpOutputMessageImpl(outputStream, requestHeaders);
try {
Class<?> requestType = value.getClass();
MediaType requestContentType = requestHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : messageConverters.getObject().getConverters()) {
if (messageConverter.canWrite(requestType, requestContentType)) {
((HttpMessageConverter<Object>) messageConverter).write(value, requestContentType, dummyRequest);
break;
}
}
} catch (IOException ex) {
throw new EncodeException("Cannot encode request.", ex);
}
HttpHeaders headers = dummyRequest.getHeaders();
if (headers != null) {
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
template.header(entry.getKey(), entry.getValue());
}
}
/*
we should use a template output stream... this will cause issues if files are too big,
since the whole request will be in memory.
*/
template.body(outputStream.toByteArray(), UTF_8);
} /**
* Dummy resource class. Wraps file content and its original name.
*/
static class MultipartFileResource extends InputStreamResource { private final String filename;
private final long size; public MultipartFileResource(String filename, long size, InputStream inputStream) {
super(inputStream);
this.size = size;
this.filename = filename;
} @Override
public String getFilename() {
return this.filename;
} @Override
public InputStream getInputStream() throws IOException, IllegalStateException {
return super.getInputStream(); //To change body of generated methods, choose Tools | Templates.
} @Override
public long contentLength() throws IOException {
return size;
} } /**
* Minimal implementation of {@link org.springframework.http.HttpOutputMessage}. It's needed to
* provide the request body output stream to
* {@link org.springframework.http.converter.HttpMessageConverter}s
*/
private class HttpOutputMessageImpl implements HttpOutputMessage { private final OutputStream body;
private final HttpHeaders headers; public HttpOutputMessageImpl(OutputStream body, HttpHeaders headers) {
this.body = body;
this.headers = headers;
} @Override
public OutputStream getBody() throws IOException {
return body;
} @Override
public HttpHeaders getHeaders() {
return headers;
} }
}

  

package com.access.service.saas.cmpt.utl;

import java.io.IOException;
import java.util.List;
import java.util.Map; import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile; /**
* @author elvis.xu
* @since 2017-05-09 10:58
*/
public class MapFormHttpMessageConverter implements HttpMessageConverter<Map<String, ?>> { protected FormHttpMessageConverter formHttpMessageConverter; public MapFormHttpMessageConverter() {
this.formHttpMessageConverter = new MultipartFormHttpMessageConverter();
} public void addPartConverter(HttpMessageConverter<?> partConverter) {
this.formHttpMessageConverter.addPartConverter(partConverter);
} @Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return formHttpMessageConverter.canRead(clazz, mediaType);
} @Override
public List<MediaType> getSupportedMediaTypes() {
return formHttpMessageConverter.getSupportedMediaTypes();
} @Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (!Map.class.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
} @Override
public Map<String, ?> read(Class<? extends Map<String, ?>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return formHttpMessageConverter.read(null, inputMessage);
} public void write(Map<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
MultiValueMap<String, Object> multiMap = null;
if (map != null) {
if (map instanceof MultiValueMap) {
multiMap = (MultiValueMap<String, Object>) map;
} else {
multiMap = new LinkedMultiValueMap<>();
for (Map.Entry<String, ?> entry : map.entrySet()) {
multiMap.add(entry.getKey(), entry.getValue());
}
}
}
formHttpMessageConverter.write(multiMap, contentType, outputMessage);
} public static class MultipartFormHttpMessageConverter extends FormHttpMessageConverter {
@Override
protected String getFilename(Object part) {
String rt = super.getFilename(part);
if (rt == null && part instanceof MultipartFile) {
return ((MultipartFile) part).getOriginalFilename();
}
return null;
}
}
}

  

RestTemplate OR Spring Cloud Feign 上传文件的更多相关文章

  1. spring cloud feign 上传文件报not a type supported by this encoder解决方案

    上传文件调用外部服务报错: not a type supported by this encoder 查看SpringFormEncoder类的源码: public class SpringFormE ...

  2. spring mvc(注解)上传文件的简单例子

    spring mvc(注解)上传文件的简单例子,这有几个需要注意的地方1.form的enctype=”multipart/form-data” 这个是上传文件必须的2.applicationConte ...

  3. spring mvc CommonsMultipartResolver上传文件异常处理

    近期已经上线的项目出现了一个异常 严重: Servlet.service() for servlet JeeCmsAdmin threw exception org.apache.commons.fi ...

  4. springMVC+spring+hibernate注解上传文件到数据库,下载,多文件上传

    数据库 CREATE TABLE `annex` ( `id` bigint() NOT NULL AUTO_INCREMENT, `realName` varchar() DEFAULT NULL, ...

  5. spring boot(十七)上传文件

    上传文件是互联网中常常应用的场景之一,最典型的情况就是上传头像等,今天就带着带着大家做一个Spring Boot上传文件的小案例. 1.pom包配置 我们使用Spring Boot最新版本1.5.9. ...

  6. 利用spring的CommonsMultipartResolver上传文件

    1.CommonsMultipartResolver是spring里面提供的一个上传方式,效率我不知道,但是加入spring容器管理还是很不错的. 2.先看依赖包pom.xml <project ...

  7. Spring使用mutipartFile上传文件报错【Failed to instantiate [org.springframework.web.multipart.MultipartFile]】

    报错场景: 使用SSM框架实现文件上传时报“Failed to instantiate [org.springframework.web.multipart.MultipartFile]”错,控制器源 ...

  8. Spring Boot (30) 上传文件

    文件上传 上传文件和下载文件是Java Web中常见的一种操作,文件上传主要是将文件通过IO流传输到服务器的某一个文件夹下. 导入依赖 在pom.xml中添加上spring-boot-starter- ...

  9. Spring Boot应用上传文件时报错

    问题描述 Spring Boot应用(使用默认的嵌入式Tomcat)在上传文件时,偶尔会出现上传失败的情况,后台报错日志信息如下:"The temporary upload location ...

随机推荐

  1. [五]SpringBoot 之 连接数据库(JPA-Hibernate)

    在具体介绍之前,先了解下什么是JPA JPA全称JavaPersistence API.JPA通过JDK5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中. http: ...

  2. Linux 内核分析第八周学习笔记

    Linux 内核分析第八周学习笔记 zl + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-10 ...

  3. python基础(5)

    使用dict和set dict Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度. 举个例子 ...

  4. LibreOJ #541. 「LibreOJ NOIP Round #1」七曜圣贤(单调队列)

    被以前自己瞎YY的东西坑了T T...单调队列的确是可以维护这种操作的.... 显然这题可以转化成维护不在车上的东西的最小值, 支持插入和删去最早出现的值,然后就可以用单调队列了T T #includ ...

  5. selenium - webdriver - 定位一组元素

    八种方法: find_elements_by_id() find_elements_by_name() find_elements_by_class_name() find_elements_by_t ...

  6. 图片虚拟目录--即图片保存在window硬盘上面

    这个是图片保存在电脑的硬盘上面的图片上传设置,既不是在web工程中,也不是在专门的图片服务器中,下面是配置方法: r 这里的Document base 我们这里设置为F:\images 如果在浏览器访 ...

  7. 几何+思维 Samara University ACM ICPC 2016-2017 Quarterfinal Qualification Contest K. Revenge of the Dragon

    题目链接:http://codeforces.com/gym/101149/problem/K 题目大意: 给你两个点a,b.一个人在a点,一个人在b点,b点的人要追杀a的点,他的跑步速度是a的两倍. ...

  8. BZOJ 2083 vector的巧用+二分

    2083: [Poi2010]Intelligence test Time Limit: 10 Sec  Memory Limit: 259 MBSubmit: 469  Solved: 227[Su ...

  9. 关于 Capella 需要纠正的语音

    li { font-size: 18px; } 关于 Capella 需要纠正的语音 持续更新 浊塞音声带要振动 区分 [θ]/[ð] 和 [t̪],注意舌位 [ɫ] 的舌位,切记不能圆唇 [æ] 的 ...

  10. HDP安全之集成kerberos/LDAP、ranger(knox自带LDAP)

    ----------------------目录导航见左上角------------------------------- 环境 HDP 3.0.1.0 (已有) JDK   1.8.0_91 (已有 ...