广告
返回顶部
首页 > 资讯 > 精选 >SpringMVC注解之@ResponseBody注解原理是什么
  • 485
分享到

SpringMVC注解之@ResponseBody注解原理是什么

2023-06-15 05:06:59 485人浏览 泡泡鱼
摘要

这篇文章主要介绍springMVC注解之@ResponseBody注解原理是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!一、介绍@ResponseBody 注解的作用是将方法的返回值通过适当的转换器转换为指定的

这篇文章主要介绍springMVC注解之@ResponseBody注解原理是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!

一、介绍

  • @ResponseBody 注解的作用是将方法的返回值通过适当的转换器转换为指定的格式之后,写入到 response 对象的 body 区,通常用来返回 JSON、XML 数据。

  • 使用了 @ResponseBody 注解标记的方法不再做视图解析

二、作用范围

  •  标记在方法上

  • 标记在类上

通过 @RestController 注解实现,此时所有的方法都将会被添加 @ResponseBody 注解

三、源码分析

具体为何调用了以下方法可以看我的另一篇文章。springmvc 执行流程解析

ServletInvocableHandlerMethod # invokeAndHandle

public void invokeAndHandle(ServletWEBRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);setResponseStatus(webRequest);if (returnValue == null) {if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {disableContentCachingIfNecessary(webRequest);mavContainer.setRequestHandled(true);return;}}else if (StringUtils.hasText(getResponseStatusReason())) {mavContainer.setRequestHandled(true);return;}mavContainer.setRequestHandled(false);Assert.state(this.returnValueHandlers != null, "No return value handlers");try {// 处理返回值this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);}catch (Exception ex) {if (logger.isTraceEnabled()) {logger.trace(fORMatErrorForReturnValue(returnValue), ex);}throw ex;}}

该方法中调用了 handleReturnValue() 方法去处理返回值。SpringMVC 中使用 RequestResponseBodyMethodProcessor 类来处理 @ResponseBody 标记的方法

RequestResponseBodyMethodProcessor # handleReturnValue

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);writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);}

该方法中通过 mavContainer.setRequestHandled(true); 设置请求已经被完全处理了,则后面不再做视图解析。然后调用了 writeWithMessageConverters() 方法。

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {// 存储响应体的信息Object body;// 返回值类型Class<?> valueType;// 目标类型Type targetType;// 返回值类型是否是 CharSequence // 是则将 返回值类型和目标类型设置为 String.classif (value instanceof CharSequence) {body = value.toString();valueType = String.class;targetType = String.class;}// 不是 CharSequence 类型,一般是我们的自定义类else {body = value;valueType = getReturnValueType(body, returnType);targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContaininGClass());}// 返回值类型是否是实现了 Resource 接口的资源// 这里我就不分析了if (isResourceType(value, returnType)) {outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&outputMessage.getServletResponse().getStatus() == 200) {Resource resource = (Resource) value;try {List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();outputMessage.getServletResponse().setStatus(httpstatus.PARTIAL_CONTENT.value());body = HttpRange.toResourceRegions(httpRanges, resource);valueType = body.getClass();targetType = RESOURCE_REGION_LIST_TYPE;}catch (IllegalArgumentException ex) {outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());}}}// 选中的媒体类型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);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();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) {// 使用类型转换器将请求写入到 response body 中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;}}}if (body != null) {Set<MediaType> producibleMediaTypes =(Set<MediaType>) inputMessage.getServletRequest().getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) {throw new HttpMessageNotWritableException("No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");}throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);}}

该方法中通过调用 getAcceptableMediaTypes() 方法获取到 acceptableTypes,getProducibleMediaTypes() 方法获取到 producibleTypes,然后调用 isCompatibleWith() 方法比较 acceptableTypes 和 producibleTypes,获取到两者都兼容的类型。最后通过调用 isConcrete() 获取到一个具体使用的媒体类型。

AbstractMessageConverterMethodProcessor # getProducibleMediaTypes

protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {Set<MediaType> mediaTypes =(Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);if (!CollectionUtils.isEmpty(mediaTypes)) {return new ArrayList<>(mediaTypes);}else if (!this.allSupportedMediaTypes.isEmpty()) {List<MediaType> result = new ArrayList<>();// 遍历类型转化器,获取支持的媒体类型for (HttpMessageConverter<?> converter : this.messageConverters) {if (converter instanceof GenericHttpMessageConverter && targetType != null) {if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {result.addAll(converter.getSupportedMediaTypes());}}else if (converter.canWrite(valueClass, null)) {result.addAll(converter.getSupportedMediaTypes());}}return result;}else {return Collections.singletonList(MediaType.ALL);}}

该方法中通过遍历类型转换器,根据类型转换器获取到支持的媒体类型。常见的类型转化器有 StringHttpMessageConverter 支持转换为 String 类型,MappingJackson2HttpMessageConverter 支持转换为 json 类型,MappingJackson2XmlHttpMessageConverter 支持转换为 XML 类型。
以转换为 JSON 数据为例。我们最终选择的媒体类型就是 “application/json” ,然后调用 AbstractGenericHttpMessageConverter # write 方法将数据写入到 response body 中。

AbstractGenericHttpMessageConverter # write

public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType,HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {final HttpHeaders headers = outputMessage.getHeaders();// 添加响应头// 设置 Content-Type 为application/jsonaDDDefaultHeaders(headers, t, contentType);if (outputMessage instanceof StreamingHttpOutputMessage) {StreamingHttpOutputMessage streaminGoutputMessage = (StreamingHttpOutputMessage) outputMessage;streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {@Overridepublic OutputStream getBody() {return outputStream;}@Overridepublic HttpHeaders getHeaders() {return headers;}}));}else {// 写入数据到 response body 中writeInternal(t, type, outputMessage);outputMessage.getBody().flush();}}

该方法中设置了响应头的 Content-Type 为 application/json,然后调用 writeInternal() 方法写数据

AbstractJackson2HttpMessageConverter # writeInternal

protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)throws IOException, HttpMessageNotWritableException {// 获取媒体类型为 application/jsonMediaType contentType = outputMessage.getHeaders().getContentType();// 获取 JSON 数据的编码为 UTF-8JsonEncoding encoding = getJsonEncoding(contentType);// 获取到 HttpServletResponse 的输出流对象// 用于将数据写入到 response body 中OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());// 生成 JSON 数据的类JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream, encoding);try {writePrefix(generator, object);Object value = object;Class<?> serializationView = null;FilterProvider filters = null;JavaType javaType = null;if (object instanceof MappingJacksonValue) {MappingJacksonValue container = (MappingJacksonValue) object;value = container.getValue();serializationView = container.getSerializationView();filters = container.getFilters();}if (type != null && TypeUtils.isAssignable(type, value.getClass())) {// 获取 java 类型,一般是我们自定义的类javaType = getJavaType(type, null);}// 用于操作可序列化对象的类// 我们自定义的类一定要实现 Serializable 接口,并设置 get/set 方法ObjectWriter objectWriter = (serializationView != null ?this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());if (filters != null) {objectWriter = objectWriter.with(filters);}if (javaType != null && javaType.isContainerType()) {objectWriter = objectWriter.forType(javaType);}SerializationConfig config = objectWriter.getConfig();if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {objectWriter = objectWriter.with(this.ssePrettyPrinter);}// 写入数据objectWriter.writeValue(generator, value);writeSuffix(generator, object);generator.flush();generator.close();}catch (InvalidDefinitionException ex) {throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);}catch (JsonProcessingException ex) {throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);}}

该方法中通过调用 outputMessage.getBody() 方法获取到了 HttpServletResponse 的输出流对象,用于将数据输出到 response body 中。并设置了 JSON 数据的编码格式为 UTF-8,然后通过 ObjectWriter 对象操作我们自定义的可序列化的对象,将该对象转换为 JSON 格式输出到 response body 中。

AbstractJackson2HttpMessageConverter # getJsonEncoding

protected JsonEncoding getJsonEncoding(@Nullable MediaType contentType) {if (contentType != null && contentType.getCharset() != null) {Charset charset = contentType.getCharset();JsonEncoding encoding = ENCODINGS.get(charset.name());if (encoding != null) {return encoding;}}return JsonEncoding.UTF8;}

设置 JSON 数据的编码格式为 UTF-8

ServletServerHttpResponse # getBody

public OutputStream getBody() throws IOException {this.bodyUsed = true;writeHeaders();return this.servletResponse.getOutputStream();}

获取 HttpServletResponse 的输出流

到这里我们已经实现了将数据转化为 JSON 格式输出到 response body 中了。

还记得前面提到的 mavContainer.setRequestHandled(true) 这个方法吗,前面我说了调用了这个方法后,就不再做视图解析了,我们这里再具体分析一下。

RequestMappingHandlerAdapter # invokeHandlerMethod

protected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {...invocableMethod.invokeAndHandle(webRequest, mavContainer);...return getModelAndView(mavContainer, modelFactory, webRequest);... }

可以看到 getModelAndView() 方法是在 invokeAndHandle() 方法之后调用了,也就是在调用 getModelAndView() 方法前,我们已经调用了 mavContainer.setRequestHandled(true) 方法了。getModelAndView() 方法就是做视图解析的,我们来看一下该方法。

RequestMappingHandlerAdapter # getModelAndView

private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {modelFactory.updateModel(webRequest, mavContainer);// 是否已经完全处理了,若为 true,则直接返回 null//  mavContainer.setRequestHandled(true) 已设置为 true 了if (mavContainer.isRequestHandled()) {return null;}// 下面的代码是做视图解析ModelMap model = mavContainer.getModel();ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());if (!mavContainer.isViewReference()) {mav.setView((View) mavContainer.getView());}if (model instanceof RedirectAttributes) {Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);if (request != null) {RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);}}return mav;}

可以看到,该方法一开始就调用了 mavContainer.isRequestHandled() 方法,如果为 true,则返回 null,并进行下面的视图解析。而 mavContainer.setRequestHandled(true) 方法已经将其设置为 true 了。这就是为什么加了 @ResponseBody 注解的方法不做视图解析的原因。

四、总结

  • @ResponseBody 注解即可加在方法中,也可以通过 @RestController 注解加在类上

  • 类上添加了 @RestController 注解等效于为该类的所有方法上添加 @ResponseBody 注解

  • @ResponseBody 通过各种类型转换器实现数据的转换,如将数据转换为 String、JSON、XML 等格式。并将数据写入到 response body 中。而且它们使用的都是 UTF-8 编码。

  • 对于自定义的 Java 类转换为 JSON 格式的数据,该类要是可序列化的。

  • 使用了 @ResponseBody 注解标记的方法不再做视图解

以上是“SpringMVC注解之@ResponseBody注解原理是什么”这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注编程网精选频道!

--结束END--

本文标题: SpringMVC注解之@ResponseBody注解原理是什么

本文链接: https://www.lsjlt.com/news/278183.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • SpringMVC注解之@ResponseBody注解原理是什么
    这篇文章主要介绍SpringMVC注解之@ResponseBody注解原理是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!一、介绍@ResponseBody 注解的作用是将方法的返回值通过适当的转换器转换为指定的...
    99+
    2023-06-15
  • SpringMVC注解之@ResponseBody注解原理
    目录一、介绍二、作用范围三、源码分析四、总结一、介绍 @ResponseBody 注解的作用是将方法的返回值通过适当的转换器转换为指定的格式之后,写入到 response ...
    99+
    2022-11-12
  • @ResponseBody注解作用和原理
         @ResponseBody这个注解通常使用在控制层(controller)的方法上,其作用是将方法的返回值以特定的格式写入到response的body区域,进而将数据返回给客户端。当方法上面...
    99+
    2023-06-02
  • spring注解校验原理是什么
    这篇文章主要介绍“spring注解校验原理是什么”,在日常操作中,相信很多人在spring注解校验原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”spring注解校验原理是什么”的疑惑有所帮助!接下来...
    99+
    2023-06-17
  • SpringMVC @RequestMapping注解有什么作用
    本篇内容介绍了“SpringMVC @RequestMapping注解有什么作用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1、...
    99+
    2023-07-05
  • SpringMVC使用注解配置方式是什么
    本篇内容主要讲解“SpringMVC使用注解配置方式是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“SpringMVC使用注解配置方式是什么”吧!SpringMVC注解配置方式使用配置类和注...
    99+
    2023-06-30
  • Spring注解驱动之BeanDefinitionRegistryPostProcessor原理解析
    目录BeanDefinitionRegistryPostProcessor概述案例实践源码分析小结BeanDefinitionRegistryPostProcessor概述 可以看...
    99+
    2022-11-13
  • Spring注解驱动之BeanFactoryPostProcessor原理解析
    目录概述BeanFactoryPostProcessor的调用时机案例实践源码分析小结概述 我们现在来学习一下Spring里面的一些扩展原理,希望大家通过这些原理的学习,对Sprin...
    99+
    2022-11-13
  • sql注入报错之注入原理实例解析
    目录前言0x010x020x03总结前言 我相信很多小伙伴在玩sql注入报错注入时都会有一个疑问,为什么这么写就会报错?曾经我去查询的时候,也没有找到满意的答案,时隔几个月终于找到搞清楚原理...
    99+
    2022-06-13
    sql注入 报错注入 SQL报错注入 sql注入报错
  • Spring高级之注解@PropertySource的原理
    目录定义/作用使用方式spring4.3之前spring4.3及之后读取XML文件自定义PropertySourceFactory解析YAML文件定义/作用 @PropertySou...
    99+
    2022-11-13
  • 详解Spring bean的注解注入之@Autowired的原理及使用
    一、@Autowired 概念: @Autowired 注释,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 通过 @Autowired的使用来消除 set ,ge...
    99+
    2022-11-12
  • spring注解的底层实现原理是什么
    Spring注解的底层实现原理主要依赖于Java的反射机制。在Spring中,通过使用注解来标识类、方法或字段,从而告诉Spring...
    99+
    2023-10-09
    spring
  • 注解处理器(APT)是什么
    目录一、定义二、生成注解处理器2.1 创建注解模块2.2 创建注解处理器模块2.3 创建注解处理器2.4 在app模块中引入注解处理器2.5 测试三、解析注解四、生成代码4.1 原始...
    99+
    2023-02-27
    APT注解处理器 注解处理器
  • java 什么是注解
    概念:说明程序的。给计算机看的。注释:用文字来描述程序,给程序员看的。定义:注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方...
    99+
    2017-12-23
    java教程 java 注解
  • 什么是java注解?
    自Java5.0版本引入注解之后,它就成为了Java平台中非常重要的一部分。开发过程中,我们也时常在应用代码中会看到诸如@Override,@Deprecated这样的注解。那么什么是注解?下面给大家介绍一下。什么是注解?注解也叫元数据,即...
    99+
    2014-10-03
    java教程 java 注解
  • SQL注入中sleep注入的原理是什么
    sleep()型的SQL注入是request发送后会产生一个timeout的delay,没有respond,相当于一种DDOS攻击,向数据库不停的发送request,不会很快释放连接,连接池会慢,导致数据库不响应,可以通过在mysql中禁止...
    99+
    2022-10-14
  • sql注入的原理是什么
    这篇文章主要介绍了sql注入的原理是什么,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。1. 首先了解SQL注入的原理:  SQL Injec...
    99+
    2022-10-18
  • dubbo之@Reference注解有什么作用
    这篇文章主要介绍“dubbo之@Reference注解有什么作用”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“dubbo之@Reference注解有什么作用”文章能帮助大家解决问题。目的看看dubb...
    99+
    2023-07-05
  • sql注入基本原理是什么
    这篇文章主要介绍了sql注入基本原理是什么,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。SQL注入基本原理WEB技术发展日新月异,但是徒手拼...
    99+
    2022-10-18
  • laravel依赖注入原理是什么
    Laravel的依赖注入原理是通过容器(Container)来实现的。容器是一个管理依赖关系的工具,它可以创建和解析对象,并自动解决...
    99+
    2023-09-06
    laravel
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作