RestTemplate OR Spring Cloud Feign 上传文件

Stella981
• 阅读 1079

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

RestTemplate OR  Spring Cloud Feign 上传文件

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

MultiValueMap是允许一key多值的。

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

RestTemplate OR  Spring Cloud Feign 上传文件

 接收方

接收,可以用

(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;
        }
    }
}
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Wesley13 Wesley13
3年前
Java多线程导致的的一个事物性问题
业务场景我们现在有一个类似于文件上传的功能,各个子站点接受业务,业务上传文件,各个子站点的文件需要提交到总站点保存,文件是按批次提交到总站点的,也就是说,一个批次下面约有几百个文件。      考虑到白天提交这么多文件会影响到子站点其他系统带宽,我们将分站点的文件提交到总站点这个操作过程独立出来,放到晚上来做,具体时间是晚上7:00到早上7:00。
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这